Files
gstack/test/doctor-components.test.ts
T
Garry Tanandcapy-ai[bot] 3742ee9527 feat: cut standalone /plan-eng-review; /autoplan keeps its eng phase (GST-1698) (#4108)
* feat: cut standalone /plan-eng-review; /autoplan keeps its eng phase (GST-1698)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: lower shrunk SKILL.md size pins; keep the retired methodology's pins measured beside /autoplan

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: carry /autoplan's plan-eng-review.md into Kiro installs

Kiro's per-skill loop copied only SKILL.md and sections, so ~/.kiro/skills/gstack-autoplan lacked the Phase 3 methodology file its SKILL.md reads. Generated sibling *.md files now travel with SKILL.md under the same ownership guards.

Review-Finding: p1/specialist-batched#1 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: file /plan-eng-review retirement follow-ups; scope the changelog claim to ./setup

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: route architecture-review requests to /autoplan by its description (journey-plan-eng)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-07 04:17:35 +00:00

89 lines
4.3 KiB
TypeScript

/**
* The doctor's component table: one source (lib/doctor-components.ts), a
* committed Bash render the doctor sources without Bun
* (bin/gstack-doctor-components.sh) that must stay fresh, every skill
* directory classified, and every result code anchored in
* docs/troubleshooting.md through the same render.
*/
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import {
COMPONENT_IDS, CORE_IDS, DOCTOR_COMPONENTS, INSTALL_PROFILES, SKILL_REQUIREMENTS, renderDoctorComponentsSh,
} from '../lib/doctor-components';
import { RESULT_CODES } from '../lib/result-codes';
import { DOCTOR_COMPONENTS_RELPATH } from '../scripts/gen-doctor-components';
import { RETIRED_SKILLS } from '../scripts/discover-skills';
const ROOT = path.resolve(import.meta.dir, '..');
const bash = (script: string) => spawnSync('bash', ['-c', `. "${path.join(ROOT, DOCTOR_COMPONENTS_RELPATH)}" && ${script}`], { encoding: 'utf8', timeout: 20_000 });
describe('doctor component table', () => {
test('the committed Bash render is fresh (matches the generator)', () => {
expect(fs.readFileSync(path.join(ROOT, DOCTOR_COMPONENTS_RELPATH), 'utf8')).toBe(renderDoctorComponentsSh());
});
test('ids are unique kebab-case and every skill requirement names known ids', () => {
expect(new Set(COMPONENT_IDS).size).toBe(COMPONENT_IDS.length);
for (const id of COMPONENT_IDS) expect(id).toMatch(/^[a-z][a-z-]*$/);
for (const [skill, req] of Object.entries(SKILL_REQUIREMENTS)) {
for (const id of [...req.requires, ...req.optional]) expect(COMPONENT_IDS, `${skill} -> ${id}`).toContain(id);
for (const id of req.requires) expect(CORE_IDS, `${skill} lists core row ${id}; core rows are implied`).not.toContain(id);
expect(req.requires.filter(id => req.optional.includes(id))).toEqual([]);
}
});
test('every skill directory in the repo is classified, and no classified skill is missing (install profiles are the only other rows)', () => {
const skills = fs.readdirSync(ROOT).filter(d => fs.existsSync(path.join(ROOT, d, 'SKILL.md.tmpl')) && !RETIRED_SKILLS.includes(d)).sort();
expect(skills.length).toBeGreaterThan(40);
expect(Object.keys(SKILL_REQUIREMENTS).filter(s => !INSTALL_PROFILES.includes(s)).sort()).toEqual(skills);
expect(INSTALL_PROFILES).toEqual(['review-voice']);
for (const profile of INSTALL_PROFILES) {
expect(skills, `${profile} is an install profile, not a skill`).not.toContain(profile);
expect(SKILL_REQUIREMENTS[profile]?.requires).toEqual(['helpers']);
}
});
test('the Bash render answers the same questions as the table, and refuses unknown names', () => {
const r = bash([
'gstack_doctor_skill_requires autoplan; echo',
'gstack_doctor_skill_optional autoplan; echo',
'gstack_doctor_skill_requires qa; echo',
'gstack_doctor_component_kind codex-cli; echo',
'gstack_doctor_component_title pins; echo',
'gstack_result_anchor REVISION_UNMET; echo',
'gstack_doctor_skill_requires no-such-skill; echo "rc=$?"',
'gstack_doctor_component_kind nope; echo "rc=$?"',
'echo "$GSTACK_DOCTOR_CORE"',
].join('; '));
expect(r.status, r.stderr).toBe(0);
expect(r.stdout.split('\n')).toEqual([
SKILL_REQUIREMENTS.autoplan.requires.join(' '),
SKILL_REQUIREMENTS.autoplan.optional.join(' '),
SKILL_REQUIREMENTS.qa.requires.join(' '),
'optional',
'project pins',
RESULT_CODES.REVISION_UNMET.anchor,
'rc=1',
'rc=1',
CORE_IDS.join(' '),
'',
]);
});
test('the render is Bash 3.2 syntax: no arrays, no [[ ]], no ${var//}', () => {
const text = renderDoctorComponentsSh();
expect(text).not.toMatch(/\[\[|\]\]|\$\{[^}]*\/\/|=\(/);
for (const c of DOCTOR_COMPONENTS) expect(text).toContain(`${c.id}) printf '%s' '${c.kind}'`);
});
test('the Astra-patch and browser rows are the installer contract', () => {
expect(SKILL_REQUIREMENTS.autoplan.requires).toEqual(['claude', 'codex', 'patch']);
expect(SKILL_REQUIREMENTS.autoplan.optional).toContain('codex-cli');
expect(SKILL_REQUIREMENTS.browse.requires).toContain('browser');
expect(SKILL_REQUIREMENTS.codex.requires).toContain('codex-cli');
expect(SKILL_REQUIREMENTS.cso.requires).toContain('cso');
});
});