Files
gstack/test/qa-lazy-sections.test.ts
Garry Tanandcapy-ai[bot] 6c5b2d4612 v1.91.260.0 feat(design-review,plan-design-review): craft audit and Persuade plan checks; craft tells as checker-only catalog entries (GSTA-92 phase 2) (#3433)
* feat(design-html): craft section and free craft check, adapted from Elaya design skills (GSTA-92)

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

* fix(GSTA-92): restore the full CHANGELOG under the Unreleased entry; drop a $-digit literal from craft.md; count only craft-check executions in design-html-slop-gate

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

* feat(design-shotgun,design-html): carry the chosen archetype into approved.json; persist accepted craft exceptions in finalized.json (GSTA-92 phase 2)

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

* feat(design-review,plan-design-review): craft audit carved on every host, Persuade plan checks, craft tells as catalog entries (GSTA-92 phase 2)

Size pins and caps raised to measured sizes only: design-review (13 hosts), plan-design-review (12), design-consultation (9), design-consultation maxSizeRatio, and the new design-review parity guard. Garry 2026-10-25 "Do it" (GSTA-92).

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

* refactor(design-catalog): craft tells are checker-only entries; trim design-review and plan-design-review growth to the planned size (GSTA-92)

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

* test(touchfiles): design-html-slop-gate selects on the craft check it runs (GSTA-92)

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

* test(touchfiles): design-review and plan-design-review cases select on the craft resolver; slop gate on the catalog (GSTA-92)

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

* test(placeholders): allow the craft audit page path placeholder (GSTA-92)

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

* fix: pre-landing review fixes (GSTA-92)

A malformed or missing --accepted exceptions file now gets the full craft
check with nothing accepted, after a warning, still exiting 2. Before, it
printed nothing, and the craft section treats a missing CRAFT_RESULT as
'check unavailable', so one bad finalized.json switched the gate off
(found by both adversarial passes). The header comment and the craft
section now document the accepted-count suffix on CRAFT_RESULT. Tests
cover duplicate carried-forward exception ids and the slop-gate touchfiles
selection.

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

* fix: scope --accepted to the same page; record current counts (GSTA-92)

The craft section passed --accepted whenever a finalized.json sat beside
the page, so a same-day Start fresh build inherited the old page's
exceptions. It now passes them only when evolving or rerunning a page it
finalized, records the count the check reports now, and writes [] when
nothing was accepted instead of the template's placeholder entry. A test
pins exit 2 for a clean page with a broken exceptions file.

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

* docs: note the archetype handoff in the shotgun and design-html loops (GSTA-92)

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

* fix: craft audit capture, exceptions and trigger from /ship review (GSTA-92)

- craft-audit: no CRAFT_RESULT line means unavailable (exit 2 after a
  malformed --accepted record still printed findings); --accepted only for a
  /design-html finalized page, never for needs-proof or lorem; never add
  [Needs proof] markers to a live page; Aside capture via pg.evaluate; the
  saved raw DOM is deleted in the checking block and never reported
- manifest trigger covers an explicit craft audit (skeleton -6 bytes; pins
  lowered to measured size, union ratio back under its cap)
- autoFixEntries reads RENDERED; DESIGN_CRAFT_RULE needs exactly one lead
- tests: checkerOnly contract, resolver throw paths, Persuade checks in both
  skills, resolver touchfile selection, exception state cells
- CHANGELOG: host counts and the audit's new rules

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

* fix: craft audit live-page limits, claim handling and guards (GSTA-92)

- craft-audit: linked CSS/JS is unchecked on a saved page (judge by eye);
  a mount-node shell is framework source; --accepted takes that screen's
  finalized.json or none; on a live page a claim without proof is filed and
  asked about, never marked or cut; the saved page is removed whatever the
  exit code; shorter fix-order and Tailwind prose keeps the union under cap
- tests: exact-lead red cases, autoFixEntries in the checkerOnly union, the
  audit's outcome/exception/session-data clauses; carve guard checks the
  session-data rule and records measured sizes (skeleton cap 137_767)

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

* fix: craft check skips mount-node shells and script bodies; audit capture order (GSTA-92)

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

* revert: craft checker back to bed645a's reviewed behavior; textless mount-node shell judged by eye (GSTA-92)

cycle-cap exception (GSTA-92 lane, 2026-10-25): reverted 216987e's checker half to bed645a's reviewed behavior; byte-neutral prose reword; no new logic

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

* fix: craft audit judges any mount-node shell by eye; pin the clause (GSTA-92)

second cycle-cap exception: GStack Minder's decision under Garry's 'do it' on phase 2 (GSTA-92, 2026-10-25): one deleted word plus one test

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

* docs: craft resolver placeholders, carved-on-every-host list, design-craft resolver (GSTA-92 /ship docs audit)

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

* fix: pre-landing review fixes after main merge: claim rule covers repo files, craft temp var, guard tests (GSTA-92)

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

* fix: craft audit files failed Persuade checks as deferred High, judges source tells by eye; stderr pin (GSTA-92)

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

* fix(design-review): never cut a claim a rule or printed fix says to drop

Pass R3 (native adversarial, Medium): the CRAFT_CHECK fix text for
round-number and needs-proof says to drop or remove the claim, which
could lead Phase 8 to delete a real claim. The audit sentence now
covers both a rule and the printed fix: defer the claim and ask for
the proof. Byte-neutral (80 -> 80); design-review/SKILL.md 137,767,
union 145,037, unchanged. The test pin moves with the sentence; no
carve-guard mustContain held this sentence.

third cycle-cap exception (Captain, 2026-10-26): zero-byte reword closing the content-deletion path; test pin and carve-guard mustContain moved

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

* design-craft: reconcile ids with the design lint (layout-transition is static-tier; legacy transition-all exceptions map to it)

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-10-27 01:14:07 +00:00

698 lines
47 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ALL_HOST_CONFIGS } from '../hosts';
import { runGeneration, type GenerationResult } from '../scripts/gen-skill-docs';
import { discoverSectionTemplates } from '../scripts/discover-skills';
import { SECTION, SECTION_INDEX, sectionPath, usesLazySections } from '../scripts/resolvers/sections';
import { generateQAMethodReads, generateQAResource } from '../scripts/resolvers/qa';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
import { runBashScript } from './helpers/bash-script';
import { PARITY_INVARIANTS, runParityChecks } from './helpers/parity-harness';
import { expectMentions, expectTokens } from './helpers/prompt-structure';
const ROOT = path.resolve(import.meta.dir, '..');
const owned = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-qa-sections-'));
const rendered = path.join(owned, 'rendered');
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
const QA_SKILLS = ['qa', 'qa-only'];
const REPORT_TEMPLATE = fs.readFileSync(path.join(ROOT, 'qa/templates/functional-report-template.md'), 'utf8');
const GENERATED_REPORT = '<!-- AUTO-GENERATED from qa/templates/functional-report-template.md — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->\n' + REPORT_TEMPLATE;
let generated: GenerationResult;
let fixture: typeof import('../scripts/resolvers/sections');
function put(file: string, content: string) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
function context(host: string, skillName: string): TemplateContext {
return { host, skillName, tmplPath: '', paths: HOST_PATHS[host] };
}
function setupFunction(name: string): string {
const start = setup.indexOf(`${name}() {`);
const end = setup.indexOf('\n}\n', start);
if (start < 0 || end < 0) throw new Error(`Missing setup function ${name}`);
return setup.slice(start, end + 2);
}
beforeAll(async () => {
generated = await runGeneration({ host: 'all', outputRoot: rendered });
expect(generated.exitCode, JSON.stringify(generated.diagnostics)).toBe(0);
const fixtureRoot = path.join(owned, 'fixture');
put(path.join(fixtureRoot, 'scripts/resolvers/sections.ts'), fs.readFileSync(path.join(ROOT, 'scripts/resolvers/sections.ts'), 'utf8'));
for (const skill of [...QA_SKILLS, 'ship']) {
put(path.join(fixtureRoot, skill, 'sections/manifest.json'), JSON.stringify({
skill, sections: [{ id: 'native', file: 'contract-probes.md', title: 'Native probes', trigger: 'probing a native contract' }],
}));
put(path.join(fixtureRoot, skill, 'sections/contract-probes.md.tmpl'), 'PRIVATE_SECTION_BODY\n{{INVOKE_SKILL:investigate}}\n');
}
fixture = await import(path.join(fixtureRoot, 'scripts/resolvers/sections.ts'));
}, 120_000);
afterAll(() => fs.rmSync(owned, { recursive: true, force: true }));
describe('QA-only cross-host lazy rendering', () => {
test('Codex review defines omitted-Army records without waiving native or QA completion', () => {
const codex = ALL_HOST_CONFIGS.find(host => host.name === 'codex')!;
const text = fs.readFileSync(path.join(rendered, codex.hostSubdir, 'skills/gstack-review/SKILL.md'), 'utf8');
const record = text.slice(text.indexOf('## Step 5.8: Persist Eng Review result')).replace(/\s+/g, ' ');
expect(text).not.toContain('### Dispatch specialists');
expect(text).not.toContain('### Step 4.6: Collect and merge findings');
expect(text).not.toContain('SPECIALIST REVIEW: N findings');
expectTokens(record, ['`specialists: {}`'], 'record');
expectMentions(record, [['without', 'specialist', 'claiming']], 'record');
expect(record).toContain('`10.0` when small-diff specialists were skipped or this host omits Review Army');
expectMentions(record, [['not', 'completion', 'evidence']], 'record');
expectMentions(record, [['not', 'inconclusive', 'required']], 'record');
expect(record).toContain('`/ship` named-risk acceptance cannot complete `/review`');
expectTokens(record, ['`issues_found`'], 'record');
expect(record).toContain('State INCOMPLETE if `COMPLETED` is false, even when N=0');
});
for (const host of ALL_HOST_CONFIGS) {
test(`${host.name} review loads QA methods before static review and reuses them before probes`, () => {
const directory = host.name === 'claude' ? 'review' : `${host.hostSubdir}/skills/gstack-review`;
const text = fs.readFileSync(path.join(rendered, directory, 'SKILL.md'), 'utf8');
const start = text.indexOf('## Step 4: Critical pass (core review)');
const core = text.indexOf('Apply both checklist passes in order', start);
const exploration = text.indexOf('### Step 4.7: Exploratory QA', core);
expect(start).toBeGreaterThan(-1);
expect(core).toBeGreaterThan(start);
expect(exploration).toBeGreaterThan(core);
const preparation = text.slice(start, core);
const loop = preparation.indexOf('sections/exploratory.md');
expect(loop).toBeGreaterThan(-1);
expect(preparation).toContain('Step 4 is read-only: defer charters, setup and probes to Step 4.7');
expect(preparation).not.toContain('sections/system-functional.md');
const qaDirectory = host.name === 'claude' ? 'qa' : `${host.hostSubdir}/skills/gstack-qa`;
const shared = fs.readFileSync(path.join(rendered, qaDirectory, 'sections/exploratory.md'), 'utf8');
const scope = shared.indexOf('Read `sections/scope.md`');
const selection = shared.indexOf('in full and select the surfaces');
const methods = shared.indexOf('Read `sections/system-functional.md`');
expect(scope).toBeGreaterThan(-1);
expect(selection).toBeGreaterThan(scope);
expect(methods).toBeGreaterThan(selection);
expect(shared).toContain('**Browser surfaces only:**');
expect(preparation).not.toContain('sections/browser-setup.md');
expect(shared).toContain('sections/qa-patterns.md');
expect(preparation.replace(/\s+/g, ' ')).toContain('Step 4 is read-only: defer charters, setup and probes to Step 4.7');
expect(shared.indexOf('Write a **charter**')).toBeGreaterThan(methods);
expectMentions(shared, [['do not', 'invocation', 'completed']], 'shared');
const qa = text.slice(exploration, text.indexOf('## Step 5: Fix-First Review', exploration));
const charter = qa.indexOf('**1. Set the charter and isolation.**');
const readiness = qa.indexOf('**2. Check readiness and list required checks.**');
const setup = qa.indexOf("Read QA's `sections/browser-setup.md`");
const probes = qa.indexOf('**3. Run smoke and plan checks.**');
expect(charter).toBeGreaterThan(-1);
expect(readiness).toBeGreaterThan(charter);
expect(setup).toBeGreaterThan(readiness);
expect(probes).toBeGreaterThan(setup);
expectMentions(qa.slice(charter, readiness).replace(/\s+/g, ' '), [['before', 'isolation/permission', 'preflight']], 'section');
const flat = qa.replace(/\s+/g, ' ');
expectMentions(flat, [['only', 'tools/session/target/ownership', 'otherwise']], 'flat');
expectMentions(flat, [['never', 'bootstrap', 'install']], 'flat');
expect(flat).toContain('Functional-only skips browser setup');
if (usesLazySections(host.name, 'review')) {
const index = text.slice(text.indexOf('## Section index'), text.indexOf('## Step 1:'));
expect(index.indexOf('Select surfaces and read QA methods')).toBeLessThan(index.indexOf('sections/review-army.md'));
} else {
expect(text).not.toContain('## Section index');
}
});
for (const skill of QA_SKILLS) {
test(`${host.name} ${skill} resolves a passive manifest without inlining its body`, () => {
const ctx = context(host.name, skill);
expect(usesLazySections(host.name, skill)).toBe(true);
const reference = fixture.sectionPath(ctx, skill, 'native');
expect(reference).toContain('`sections/contract-probes.md`');
expect(reference).toContain('installed');
expect(reference).toContain('SKILL.md directory');
expect(reference).toContain(host.name === 'claude' ? `\`${skill}\`` : `\`gstack-${skill}\``);
const pointer = fixture.SECTION(ctx, ['native']);
expect(pointer).toContain(reference);
expect(pointer).toContain('probing a native contract');
expect(pointer).toContain('never the product working directory');
expect(pointer).toContain('missing or unreadable');
expect(pointer).toContain('QA setup blocker');
expect(pointer).not.toContain('PRIVATE_SECTION_BODY');
expect(pointer).not.toContain('$GSTACK_ROOT');
expect(fixture.SECTION_INDEX(ctx)).toContain(reference);
expect(fixture.SECTION_INDEX(context(host.name, 'ship'), [skill])).toContain(reference);
expect(fixture.sectionPath(context(host.name, 'review'), skill, 'native')).toBe(reference);
expect(fixture.sectionPath(context(host.name, 'ship'), skill, 'native')).toBe(reference);
});
}
test(`${host.name} generates all QA section templates and preserves other skills' section policy`, () => {
const sections = discoverSectionTemplates(ROOT);
const targets = sections.filter(section => QA_SKILLS.includes(section.skillDir));
expect(targets.length).toBeGreaterThan(0);
for (const section of targets) {
const dir = host.name === 'claude' ? section.skillDir : `${host.hostSubdir}/skills/gstack-${section.skillDir}`;
const file = `${dir}/sections/${path.basename(section.output)}`;
expect(generated.artifacts).toContainEqual({ relativePath: file, kind: 'section', host: host.name });
const body = fs.readFileSync(path.join(rendered, file), 'utf8');
expect(body).toContain('AUTO-GENERATED');
expect(body).not.toMatch(/\{\{[A-Z_]+(?::[^}]*)?\}\}/);
expect(fs.readFileSync(path.join(rendered, dir, 'SKILL.md'), 'utf8')).not.toContain(body.split('\n').slice(2).join('\n').trim());
}
const outside = generated.artifacts.filter(artifact => artifact.host === host.name && artifact.kind === 'section'
&& !/^(?:qa|qa-only|ship|plan-ceo-review|office-hours|design-review)\//.test(artifact.relativePath)
&& !/\/gstack-(?:qa(?:-only)?|ship|plan-ceo-review|office-hours|design-review)\//.test(artifact.relativePath));
expect(outside.length > 0).toBe(host.name === 'claude');
// C4: ship is carved on every host; external pointers are relative to the installed skill.
const ctx = context(host.name, 'ship');
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'ship/sections/manifest.json'), 'utf8'));
const entry = manifest.sections[0];
expect(usesLazySections(host.name, 'ship')).toBe(true);
const pointer = host.name === 'claude'
? `\`~/.claude/skills/gstack/ship/sections/${entry.file}\``
: `\`sections/${entry.file}\` relative to the installed \`gstack-ship\` SKILL.md directory`;
expect(SECTION(ctx, [entry.id])).toBe(`> **STOP.** Before ${entry.trigger}, Read ${pointer} and execute it\n> in full. Do not work from memory — that section is the source of truth for this step.`);
// Skills carved only on Claude still inline their sections elsewhere.
const reviewCtx = context(host.name, 'review');
const reviewEntry = JSON.parse(fs.readFileSync(path.join(ROOT, 'review/sections/manifest.json'), 'utf8')).sections[0];
expect(usesLazySections(host.name, 'review')).toBe(host.name === 'claude');
if (host.name !== 'claude') {
expect(SECTION(reviewCtx, [reviewEntry.id])).toBe(fs.readFileSync(path.join(ROOT, 'review/sections', `${reviewEntry.file}.tmpl`), 'utf8').trimEnd());
expect(SECTION_INDEX(reviewCtx)).toBe('');
}
});
test(`${host.name} packages the authored functional report beside the QA entrypoint`, () => {
const dir = host.name === 'claude' ? 'qa' : `${host.hostSubdir}/skills/gstack-qa`;
const relativePath = `${dir}/templates/functional-report-template.md`;
expect(generated.artifacts.find(artifact => artifact.relativePath === relativePath))
.toEqual({ relativePath, kind: 'asset', host: host.name });
expect(fs.readFileSync(path.join(rendered, relativePath), 'utf8'))
.toBe(host.name === 'claude' ? REPORT_TEMPLATE : GENERATED_REPORT);
});
test(`${host.name} qa-only reads shared browser setup directly without a redirect section`, () => {
const dir = host.name === 'claude' ? 'qa-only' : `${host.hostSubdir}/skills/gstack-qa-only`;
const entry = fs.readFileSync(path.join(rendered, dir, 'SKILL.md'), 'utf8');
const browserRead = generateQAResource(context(host.name, 'qa-only'), ['browser-setup']);
expect(entry).toContain(browserRead);
expect(entry.indexOf('**Browser surface only:**')).toBeLessThan(entry.indexOf(browserRead));
expect(browserRead).toContain(sectionPath(context(host.name, 'qa-only'), 'qa', 'browser-setup'));
expect(browserRead).toContain("this host's installed caller skill");
expect(browserRead).toContain('No product-directory or cross-host substitutes');
expect(browserRead).toContain('Missing/unreadable assets block required QA');
expect(browserRead).toContain('continue other safe probes');
expect(generated.artifacts.some(artifact => artifact.relativePath === `${dir}/sections/browser-setup.md`)).toBe(false);
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'qa-only/sections/manifest.json'), 'utf8'));
expect(manifest.sections.some((section: { id: string }) => section.id === 'browser-setup')).toBe(false);
for (const name of ['browser-setup.md.tmpl', 'browser-setup.md']) {
expect(fs.existsSync(path.join(ROOT, 'qa-only/sections', name))).toBe(false);
}
});
test(`${host.name} both QA entrypoints reach shared functional modes before probes`, () => {
const qaDirectory = host.name === 'claude' ? 'qa' : `${host.hostSubdir}/skills/gstack-qa`;
const functional = fs.readFileSync(path.join(rendered, qaDirectory, 'sections/system-functional.md'), 'utf8');
const modes = functional.slice(functional.indexOf('## Functional modes'), functional.indexOf('## Contract map'));
for (const contract of [
'**Full** (default)', 'every applicable documented contract', '**Quick** (`--quick`)',
'success and the highest-risk changed edge', 'contracts not run',
'**Regression** (`--regression <previous-report>`)', 'before probes, read the supplied',
'functional report and linked replay evidence', 'missing, unreadable or wrong-target',
'baseline blocks regression mode', 'browser-only `baseline.json` is not a functional',
'Re-establish owned setup', 'replay prior failed probes against the documented',
'never recorded buggy output', 'changed adjacent behavior', 'Preserve the prior report',
'fixed, still failing and new findings', 'Missing safe replay inputs block affected probes',
'never count as passes', "Mixed runs apply each surface's mode separately",
"bounded smoke and explicit plan checks, not Full exploration",
]) expect(modes).toContain(contract);
expect(functional.indexOf('## Functional modes')).toBeLessThan(functional.indexOf('## Execute and retain evidence'));
for (const skill of QA_SKILLS) {
const dir = host.name === 'claude' ? skill : `${host.hostSubdir}/skills/gstack-${skill}`;
const entry = fs.readFileSync(path.join(rendered, dir, 'SKILL.md'), 'utf8');
expect(entry).toContain('| Mode | full | `--quick`, `--regression <previous-report-or-baseline>` |');
expect(entry).toContain(sectionPath(context(host.name, skill), skill, 'exploratory'));
expect(entry).not.toContain('## Functional modes');
const explorer = fs.readFileSync(path.join(rendered, dir, 'sections/exploratory.md'), 'utf8');
const functionalRead = 'Read `sections/system-functional.md` in full.';
expect(entry).not.toContain(functionalRead);
expect(entry).toContain(skill === 'qa' ? "Follow the shared section's ordered preparation" : 'Load the shared preparation gate now');
expect(explorer).toContain(generateQAMethodReads(context(host.name, skill)));
const stages = ['1. Read `sections/scope.md`', 'in full and select the surfaces', functionalRead,
'Read `sections/qa-patterns.md` in full.', 'Write a **charter**', '1. First demonstrate success'];
const positions = stages.map(stage => explorer.indexOf(stage));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
expect(explorer.split(functionalRead)).toHaveLength(2);
expectMentions(explorer, [['do not', 'invocation', 'completed']], 'explorer');
if (skill === 'qa') {
expect(entry).toMatch(/`--quick`[^\n]*Quick exploration/i);
expect(entry).toContain('`--exhaustive` changes only the fix tier');
for (const tier of ['**Quick:** Fix critical + high severity only', '**Standard:** + medium severity (default)', '**Exhaustive:** + low/cosmetic severity']) expect(entry).toContain(tier);
} else {
expectMentions(entry, [['never', 'product', 'write']], 'entry');
const browserSetup = entry.indexOf('## Browser Setup (conditional)');
const selectedChecks = entry.indexOf('## Run the Selected Checks');
expect(browserSetup).toBeGreaterThan(0);
expect(selectedChecks).toBeGreaterThan(browserSetup);
expect(entry.lastIndexOf(sectionPath(context(host.name, skill), skill, 'exploratory'))).toBeLessThan(browserSetup);
expect(entry.slice(browserSetup, selectedChecks)).not.toContain(functionalRead);
expectMentions(entry, [['do not', 'preparation', 'section']], 'entry');
expect(entry).toContain('Defer charters, clocks and probes to');
expectMentions(explorer, [['before', 'complete', 'charters']], 'explorer');
expect(explorer).toContain('## 3. Parent handoff');
expect(explorer).toContain('Return test_stub proposals');
expectMentions(explorer, [['never', 'create', 'freeze']], 'explorer');
expect(explorer).not.toMatch(/before repair|For \/review and \/ship|every new \/ship/);
}
}
const browser = fs.readFileSync(path.join(rendered, qaDirectory, 'sections/qa-patterns.md'), 'utf8');
expect(browser).toContain('### Regression (`--regression <baseline>`)');
expect(browser).not.toContain('## Functional modes');
});
test(`${host.name} caller-relative QA resources resolve within the generated installation`, () => {
const base = host.name === 'claude' ? rendered : path.join(rendered, host.hostSubdir, 'skills');
const prefix = host.name === 'claude' ? '' : 'gstack-';
for (const caller of ['review', 'ship']) {
const dir = path.join(base, `${prefix}${caller}`);
const body = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8')
+ (caller === 'ship' // C4: ship is carved on every host
? fs.readFileSync(path.join(dir, 'sections/review-army.md'), 'utf8') : '');
expect(body).toContain(`From the installed /${caller} SKILL.md's directory`);
expect(body).toContain(`Read \`../${prefix}qa/sections/exploratory.md\` in full`);
const scopeTarget = path.resolve(dir, `../${prefix}qa/sections/scope.md`);
expect(fs.realpathSync(scopeTarget)).toBe(path.join(base, `${prefix}qa/sections/scope.md`));
const target = path.resolve(dir, `../${prefix}qa/sections/exploratory.md`);
expect(fs.realpathSync(target)).toBe(path.join(base, `${prefix}qa/sections/exploratory.md`));
expect(fs.readFileSync(target, 'utf8')).toContain('# Shared exploratory QA');
expect(fs.readFileSync(target, 'utf8')).toContain(sectionPath(context(host.name, 'qa'), 'qa', 'scope'));
expect(fs.readFileSync(scopeTarget, 'utf8')).toContain('Select **browser**, **functional**');
if (host.name === 'claude') {
expect(body).toContain(`If the caller directory is prefixed \`gstack-${caller}\``);
expect(body).toContain('use `../gstack-qa/sections/exploratory.md` instead');
if (caller === 'review') {
expectMentions(body, [['do not', 'installation', 'unresolved']], 'body');
expectMentions(body, [['never', 'installation', 'product']], 'body');
expect(body).toContain('Missing/unreadable assets block required QA');
}
const registry = path.join(owned, 'prefixed-callers', caller);
fs.mkdirSync(path.join(registry, `gstack-${caller}`), { recursive: true });
fs.cpSync(path.join(base, 'qa'), path.join(registry, 'gstack-qa'), { recursive: true });
const prefixedTarget = path.resolve(registry, `gstack-${caller}`, '../gstack-qa/sections/exploratory.md');
const prefixedScope = path.resolve(registry, `gstack-${caller}`, '../gstack-qa/sections/scope.md');
expect(fs.readFileSync(prefixedScope, 'utf8')).toBe(fs.readFileSync(scopeTarget, 'utf8'));
expect(fs.readFileSync(prefixedTarget, 'utf8')).toBe(fs.readFileSync(target, 'utf8'));
expect(fs.existsSync(path.resolve(registry, `gstack-${caller}`, '../qa/sections/exploratory.md'))).toBe(false);
}
}
});
}
test('freshly rendered QA modes retain the fixed parity and prompt-size limits', () => {
const baseline = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/parity-baseline-v1.64.1.0.json'), 'utf8'));
const report = runParityChecks({ repoRoot: rendered, baseline,
invariants: PARITY_INVARIANTS.filter(invariant => QA_SKILLS.includes(invariant.skill)) });
expect(report.totalChecks).toBe(2);
expect(report.details.filter(detail => !detail.passed)).toEqual([]);
});
test('invalid IDs and missing source assets fail rather than producing a usable pointer', () => {
for (const host of ALL_HOST_CONFIGS) {
expect(() => fixture.SECTION(context(host.name, 'qa'), [])).toThrow('requires a section id');
expect(() => fixture.sectionPath(context(host.name, 'ship'), 'qa', 'unknown')).toThrow('no section "unknown"');
}
const template = path.join(owned, 'fixture/qa-only/sections/contract-probes.md.tmpl');
fs.unlinkSync(template);
try {
for (const host of ALL_HOST_CONFIGS) {
expect(() => fixture.SECTION(context(host.name, 'qa-only'), ['native'])).toThrow('contract-probes.md.tmpl');
}
} finally {
fs.writeFileSync(template, 'PRIVATE_SECTION_BODY\n');
}
});
test('dry-run reports missing QA sections and authored report assets without recreating them', async () => {
const artifacts = generated.artifacts.filter(artifact =>
artifact.kind === 'section' && /^(?:\.[^/]+\/skills\/gstack-)?qa\/sections\//.test(artifact.relativePath)
|| artifact.kind === 'asset' && artifact.relativePath.endsWith('/templates/functional-report-template.md'));
expect(artifacts.length).toBeGreaterThanOrEqual(ALL_HOST_CONFIGS.length);
const originals = artifacts.map(artifact => ({ ...artifact, body: fs.readFileSync(path.join(rendered, artifact.relativePath)) }));
for (const artifact of originals) fs.unlinkSync(path.join(rendered, artifact.relativePath));
try {
const result = await runGeneration({ host: 'all', outputRoot: rendered, dryRun: true });
expect(result.exitCode).toBe(1);
expect(result.diagnostics.filter(diagnostic => diagnostic.kind === 'error')).toEqual([]);
expect(result.diagnostics.filter(diagnostic => diagnostic.kind === 'stale').map(diagnostic => diagnostic.relativePath).sort())
.toEqual(artifacts.map(artifact => artifact.relativePath).sort());
for (const artifact of artifacts) expect(fs.existsSync(path.join(rendered, artifact.relativePath))).toBe(false);
} finally {
for (const artifact of originals) fs.writeFileSync(path.join(rendered, artifact.relativePath), artifact.body);
}
});
});
describe('installed QA pointers', () => {
test('QA-only reads methods and finalization before their dependent writes', () => {
const source = fs.readFileSync(path.join(ROOT, 'qa-only/SKILL.md.tmpl'), 'utf8');
const stages = ['Load the shared preparation gate now', '{{SECTION:exploratory}}',
'## Prepare Report Artifacts', '## Run the Selected Checks', 'With its required Reads complete and report ownership resolved, Write the charters',
'### Assemble the report', '{{SECTION:reporting}}', '### Write the checked report'];
const positions = stages.map(stage => source.indexOf(stage));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
expect(source.indexOf('Write identical content')).toBeGreaterThan(source.indexOf('{{SECTION:reporting}}'));
expect(source).not.toContain('{{SLUG_SETUP}}');
expect(source).not.toContain('{{SLUG_EVAL}}');
expectMentions(source, [['not', 'finalization', 'preparation']], 'source');
expectTokens(source, ['**Charters**'], 'source');
expectMentions(source, [['before', 'preserve', 'charters']], 'source');
});
test('QA-only defines standalone permissions, mixed modes and outside changes', () => {
const source = ['qa-only/SKILL.md.tmpl', 'qa-only/sections/reporting.md.tmpl']
.map(file => fs.readFileSync(path.join(ROOT, file), 'utf8')).join('\n').replace(/\s+/g, ' ');
expect(source).toContain('**caller** means this /qa-only workflow');
expect(source).toContain('**Owned** means created for this run or explicitly assigned to it, not merely writable');
expectMentions(source, [['does not', 'permission', 'invoking']], 'source');
expectMentions(source, [['do not', 'forbidden', 'create']], 'source');
expectMentions(source, [['unless', 'selected', 'surfaces']], 'source');
expectMentions(source, [['only', 'source/diff', 'changes']], 'source');
expectTokens(source, ['`TODOS.md`'], 'source');
expect(source).toContain('defaulting to functional then browser');
expectMentions(source, [['does not', 'switching', 'absolute']], 'source');
expect(source).toContain('CLI executable basename');
expect(source).toContain('**No explicit permission:** skip learning-store writes and continue to the report');
expect(source).toContain('**Explicit permission:** Read the named store first');
expect(source).toContain('Do not run logging helpers');
expect(source).not.toContain('{{LEARNINGS_LOG}}');
for (const host of ALL_HOST_CONFIGS) {
const directory = host.name === 'claude' ? 'qa-only' : `${host.hostSubdir}/skills/gstack-qa-only`;
const explorer = fs.readFileSync(path.join(rendered, directory, 'sections/exploratory.md'), 'utf8').replace(/\s+/g, ' ');
expectMentions(explorer, [['do not', 'yourself', 'product']], 'explorer');
expect(explorer).toContain('Keep the original limits/notes');
}
});
test('report-only scope, modes and output overrides precede browser setup', () => {
const source = fs.readFileSync(path.join(ROOT, 'qa-only/SKILL.md.tmpl'), 'utf8');
const stages = ['## Request Parameters', '## Test Plan Context', '{{LEARNINGS_SEARCH}}',
'## Select Surfaces and Isolation', '{{SECTION:exploratory}}', '## Prepare Report Artifacts',
'## Browser Setup (conditional)', '{{QA_RESOURCE:browser-setup}}', '## Run the Selected Checks'];
const positions = stages.map(stage => source.indexOf(stage));
for (const position of positions) expect(position).toBeGreaterThan(-1);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
expect(source).not.toContain('{{SECTION:browser-setup}}');
expect(source).not.toContain('{{QA_METHOD_READS}}');
expect(source).toContain('Load the shared preparation gate now');
expectMentions(source, [['does not', 'parsing', 'records']], 'source');
expectTokens(source, ['`--quick`', '`--regression`'], 'source');
expectMentions(source, [['ask', 'supplied', 'choose']], 'source');
expect(source).toContain('$REPORT_DIR/qa-report-{target}-{YYYY-MM-DD}.md');
expect(source).not.toContain('qa-report-{domain}');
expect(source.indexOf('Set `REPORT_FILE`')).toBeLessThan(source.indexOf('## Browser Setup (conditional)'));
expectTokens(source, ['`REPORT_FILE`'], 'source');
expectMentions(source.replace(/\s+/g, ' '), [['not', 'charters', 'findings']], 'source.replace(/\s+/g, )');
const browser = fs.readFileSync(path.join(ROOT, 'qa/sections/browser-setup.md.tmpl'), 'utf8');
expectMentions(browser, [['do not', 'setup/install', 'cookie-import']], 'browser');
expect(browser).toContain('scope section\'s ownership rules apply even to LOCAL browser targets');
});
test('QA entrypoints preserve previous artifacts before browser setup without expanding caller authority', () => {
for (const skill of QA_SKILLS) {
const source = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md.tmpl'), 'utf8');
const browser = skill === 'qa' ? '{{SECTION:browser-setup}}' : '{{QA_RESOURCE:browser-setup}}';
const setup = source.slice(0, source.indexOf(browser)).replace(/\s+/g, ' ');
expect(setup).toContain('prior report');
expect(setup).toMatch(/baseline paths.*before writing/);
expectMentions(setup, [['only', 'subdirectory', 'otherwise']], 'setup');
if (skill === 'qa-only') {
expectMentions(setup, [['never', 'overwrite', 'artifacts']], 'setup');
} else {
expectMentions(setup, [['never', 'screenshots', 'exploration']], 'setup');
}
expect(setup).toMatch(/impossible.*(?:output blocker|blocker)/);
expect(setup).toMatch(/(?:rather than expanding|do not expand) write authority/);
}
const source = fs.readFileSync(path.join(ROOT, 'qa-only/SKILL.md.tmpl'), 'utf8').replace(/\s+/g, ' ');
expectMentions(source, [['no', 'established', 'revalidate']], 'source');
expectMentions(source, [['never', 'destination', 'suffixed']], 'source');
});
test('mixed report labels, metadata and baselines have one explicit assembly rule', () => {
const source = fs.readFileSync(path.join(ROOT, 'qa-only/SKILL.md.tmpl'), 'utf8').replace(/\s+/g, ' ');
for (const contract of [
'`mixed-{project-label}`', 'sanitizing the repository name', '`mixed-target`',
'List the individual targets', 'common metadata once', '**Browser:**', '**Functional:**',
'`templates/qa-report-template.md`', '`templates/functional-report-template.md`',
'without duplicating the shared title or metadata',
'Browser scores apply only to browser coverage; never combine them with functional outcomes',
'current baseline or replay evidence and checkpoints',
'Regression also links the prior input baseline/report',
'Prior baselines are not applicable to Full/Quick',
'for functional regression the report plus replay evidence is the baseline',
'Report-only repair/test fields contain proposals or not-run status, never claims of edits',
]) expect(source).toContain(contract);
});
test('QA-only distinguishes shared browser artifacts from per-surface clocks and checkpoints', () => {
for (const host of ALL_HOST_CONFIGS) {
const directory = host.name === 'claude' ? 'qa-only' : `${host.hostSubdir}/skills/gstack-qa-only`;
const source = fs.readFileSync(path.join(rendered, directory, 'SKILL.md'), 'utf8');
const start = source.indexOf('### Output Structure');
expect(start).toBeGreaterThan(-1);
const layout = source.slice(start, source.indexOf('\n---', start));
expect(layout).toContain('`REPORT_DIR` stays the report root throughout the run');
expectTokens(layout.replace(/\s+/g, ' '), ['`$REPORT_DIR/screenshots/`', '`$REPORT_DIR/baseline.json`'], 'layout.replace(/\s+/g, )');
expectMentions(layout.replace(/\s+/g, ' '), [['only', 'browser-only', 'screenshots']], 'layout.replace(/\s+/g, )');
expectMentions(layout, [['only', 'mixed-surface', 'checkpoints']], 'layout');
expect(layout).toContain('| One surface (browser or functional) | `$REPORT_DIR` |');
expect(layout).toContain('| Mixed: browser probes | `$REPORT_DIR/browser` |');
expect(layout).toContain('| Mixed: functional probes | `$REPORT_DIR/functional` |');
expect(layout.replace(/\s+/g, ' ')).toContain('only when timed, `deadline.json`');
expect(layout).toContain('Caller-fixed paths override this layout');
expectTokens(layout.replace(/\s+/g, ' '), ['`REPORT_DIR`'], 'layout.replace(/\s+/g, )');
expectMentions(layout.replace(/\s+/g, ' '), [['do not', 'directory', 'reassign']], 'layout.replace(/\s+/g, )');
}
});
test('QA-only persists its plan before probes and separates observed facts from hypotheses', () => {
for (const host of ALL_HOST_CONFIGS) {
const directory = host.name === 'claude' ? 'qa-only' : `${host.hostSubdir}/skills/gstack-qa-only`;
const entry = fs.readFileSync(path.join(rendered, directory, 'SKILL.md'), 'utf8');
const reporting = fs.readFileSync(path.join(rendered, directory, 'sections/reporting.md'), 'utf8');
const directive = SECTION(context(host.name, 'qa-only'), ['reporting']);
expect(entry).toContain(directive);
expect(entry.indexOf(directive)).toBeGreaterThan(entry.indexOf('### Assemble the report'));
expectMentions(entry, [['does not', 'authorize', 'restart']], 'entry');
expect(entry).toContain('Do not preload reporting');
expect(directive).toContain('Read `sections/reporting.md`');
expect(directive).toContain('Missing/unreadable assets block required QA');
expect(entry).not.toContain('## 1. Establish each finding once');
const source = (entry + '\n' + reporting).replace(/\s+/g, ' ');
expectMentions(source, [['wait', 'successful', 'ownership']], 'source');
expect(source).toContain('A failed baseline contract stays failed');
expectMentions(source, [['does not', 'establish', 'exception']], 'source');
expectMentions(source, [['does not', 'inaccessible', 'page-text']], 'source');
expectMentions(source, [['not', 'measurement', 'latency']], 'source');
expect(source).toContain('**Probe budget** (configured limit)');
expect(source).toContain('**Guarded command time** (sum of measured child spans)');
expect(source).toContain('**Total session elapsed**: `unmeasured` for the invocation whose report is being written');
expectMentions(source, [['not', 'deadline', 'window']], 'source');
expectMentions(source, [['do not', 'status/write', 'receipts']], 'source');
expectMentions(source, [['only', 'dispatched', 'follow-up']], 'source');
expectMentions(source, [['not', 'acknowledgement', 'finished']], 'source');
expectTokens(source, ['**Measured interval**'], 'source');
expectMentions(source, [['must', 'boundaries', 'start/end']], 'source');
expectMentions(source, [['not', 'exception-shaped', 'operation']], 'source');
expectMentions(source, [['not', 'confirmation', 'completion']], 'source');
expectMentions(source, [['do not', 'conservative', 'paraphrase']], 'source');
const exploratory = fs.readFileSync(path.join(rendered, directory, 'sections/exploratory.md'), 'utf8').replace(/\s+/g, ' ');
expectMentions(exploratory, [['not', 'separator', 'child']], 'exploratory');
expectMentions(source, [['do not', 'exception-only', 'console-only']], 'source');
expectMentions(source, [['only', 'consistency', 'report']], 'source');
expectMentions(source, [['only', 'caller-authorized', 'destination']], 'source');
expectTokens(source, ['`REPORT_FILE`'], 'source');
expectMentions(source, [['do not', 'automatic', 'learning']], 'source');
const stages = ['## 1. Establish each finding once', '## 2. Fill timing fields', '## 3. Assemble and check'];
const positions = stages.map(stage => reporting.indexOf(stage));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
}
});
const installers = new Set(['claude', 'codex', 'factory', 'kiro', 'opencode', 'cursor']);
const helpers = [
'_link_or_copy', '_print_windows_copy_note_once', '_link_skill_runtime_assets', '_gstack_link_target_abs', '_gstack_target_is_ours',
'_gstack_generated_header', '_claude_entry_owned_strongly', '_claude_entry_is_ours', '_write_owned_marker',
'_backup_skill_md', '_cleanup_weak_dir', '_gstack_dir_only_links', '_cleanup_linked_dir',
'_owned_for_windows_refresh', '_sidecar_root_user_owned', '_prune_stale_generated', '_skill_source_exists',
'_link_runtime_dists', '_copy_runtime_skill_refs', '_copy_skill_md', '_skill_copy_hash', '_skill_copy_unmodified', '_preserve_skill_copy_edits', '_record_skill_copies',
].map(setupFunction).join('\n')
// Install-registry rows (setup's _setup_arm_* / _setup_row) are not under test here.
+ '\n_setup_arm_begin() { :; }\n_setup_arm_publish() { :; }\n_setup_row() { :; }';
const kiroStart = setup.indexOf('# 6. Install for Kiro CLI');
const kiroBlock = setup.slice(kiroStart, setup.indexOf('# 6b.', kiroStart));
for (const host of ALL_HOST_CONFIGS) {
for (const copy of process.platform === 'win32' ? [true] : [false, true]) {
test(`${host.name} ${copy ? 'copies' : 'links'} resolve in global/local ${installers.has(host.name) ? 'setup installs' : 'rendered layouts (no setup arm)'}`, () => {
const base = fs.mkdtempSync(path.join(owned, `${host.name}-`));
const source = path.join(base, 'payload');
const native = host.name === 'claude' ? source : path.join(source, host.hostSubdir, 'skills');
const names = QA_SKILLS.filter(skill => fs.existsSync(path.join(ROOT, skill, 'sections/manifest.json')));
expect(names).toContain('qa');
for (const skill of names) {
const name = host.name === 'claude' ? skill : `gstack-${skill}`;
const original = host.name === 'claude' ? path.join(rendered, skill) : path.join(rendered, host.hostSubdir, 'skills', name);
fs.cpSync(original, path.join(native, name), { recursive: true });
put(path.join(source, skill, 'SKILL.md.tmpl'), `---\nname: ${skill}\n---\n`);
}
const homes = [path.join(base, 'home', path.dirname(host.globalRoot)), path.join(base, 'repo', path.dirname(host.localSkillRoot))];
if (host.name === 'codex') homes.push(path.join(base, 'custom-codex-home/skills'));
for (const registry of homes) {
for (const prefix of host.name === 'claude' ? [0, 1] : [1]) {
fs.mkdirSync(registry, { recursive: true });
let install: string;
if (host.name === 'kiro') {
for (const file of ['bin/tool', 'lib/helper', 'browse/dist/browse', 'browse/bin/helper']) put(path.join(source, file), 'fixture');
for (const name of ['gstack', 'gstack-upgrade']) put(path.join(native, name, 'SKILL.md'), '<!-- AUTO-GENERATED from SKILL.md.tmpl -->\n<!-- Regenerate: bun run gen:skill-docs -->\n');
install = `INSTALL_KIRO=1\nKIRO_SKILLS="$REGISTRY"\n${kiroBlock}`;
} else if (installers.has(host.name)) {
const name = `link_${host.name}_skill_dirs`;
install = `${setupFunction(name)}\n${name} "$SOURCE_GSTACK_DIR" "$REGISTRY"`;
} else {
install = names.map(skill => `_link_or_copy "$NATIVE/gstack-${skill}" "$REGISTRY/gstack-${skill}"`).join('\n');
}
const script = [
'set -e', `IS_WINDOWS=${copy ? 1 : 0}`, `SKILL_PREFIX=${prefix}`, 'QUIET=1',
'_FOREIGN_SKIPPED_ENTRIES=()', '_BACKED_UP_SKILL_MDS=()', '_SKILL_BACKUP_ROOT="$HOME/backups"',
'GSTACK_USER_RENDER_DIR="$HOME/absent-render"',
'GSTACK_STATE_ROOT="$HOME/.gstack"', '_SKILL_COPIES_FILE="$GSTACK_STATE_ROOT/skill-copies.tsv"', helpers,
'log() { :; }', '_browser_hint() { :; }', 'bun_cmd() { :; }', install,
].join('\n');
const runInstall = () => runBashScript(script, {
cwd: base, timeout: 30_000,
env: { ...process.env, HOME: path.join(base, 'isolated-home'), REGISTRY: registry, SOURCE_GSTACK_DIR: source, NATIVE: native },
});
const result = runInstall();
expect(result.status, result.stderr).toBe(0);
expect(result.stderr).not.toContain('command not found');
for (const skill of names) {
const name = host.name === 'claude' && !prefix ? skill : `gstack-${skill}`;
const entry = path.join(registry, name, 'SKILL.md');
const body = fs.readFileSync(entry, 'utf8');
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, skill, 'sections/manifest.json'), 'utf8'));
for (const section of manifest.sections) {
const ref = sectionPath(context(host.name, skill), skill, section.id);
expect(body).toContain(ref);
const relative = ref.match(/^`([^`]+)`/)![1];
const installed = path.resolve(path.dirname(entry), relative);
const resolved = fs.realpathSync(installed);
expect(resolved.startsWith(fs.realpathSync(base) + path.sep)).toBe(true);
expect(fs.readFileSync(installed, 'utf8')).toBe(fs.readFileSync(path.join(native, host.name === 'claude' ? skill : `gstack-${skill}`, relative), 'utf8'));
expect(fs.existsSync(path.resolve(base, relative))).toBe(false);
fs.renameSync(resolved, `${resolved}.absent`);
try {
expect(() => fs.readFileSync(installed, 'utf8')).toThrow('ENOENT');
expect(body).toContain('missing or unreadable');
expect(body).toContain('QA setup blocker');
} finally {
fs.renameSync(`${resolved}.absent`, resolved);
}
}
const explorer = fs.readFileSync(path.join(path.dirname(entry), 'sections/exploratory.md'), 'utf8');
expect(body).not.toContain(generateQAMethodReads(context(host.name, skill)));
expect(body).toContain(skill === 'qa' ? "Follow the shared section's ordered preparation" : 'Load the shared preparation gate now');
expect(explorer).toContain(generateQAMethodReads(context(host.name, skill)));
expect(explorer.indexOf('in full and select the surfaces')).toBeLessThan(explorer.indexOf('Read `sections/system-functional.md`'));
expect(explorer.indexOf('Read `sections/system-functional.md`')).toBeLessThan(explorer.indexOf('Write a **charter**'));
const qaName = host.name === 'claude' && !prefix ? 'qa' : 'gstack-qa';
const qaDirectory = path.join(registry, qaName);
const functional = fs.readFileSync(path.join(qaDirectory, 'sections/system-functional.md'), 'utf8');
const reportReference = functional.match(/Use `([^`]+)` relative to the installed QA SKILL\.md\./);
expect(reportReference).not.toBeNull();
const report = path.resolve(qaDirectory, reportReference![1]);
const resolvedReport = fs.realpathSync(report);
expect(resolvedReport.startsWith(fs.realpathSync(base) + path.sep)).toBe(true);
expect(fs.readFileSync(report, 'utf8'))
.toBe(host.name === 'claude' ? REPORT_TEMPLATE : GENERATED_REPORT);
fs.renameSync(resolvedReport, `${resolvedReport}.absent`);
try {
expect(() => fs.readFileSync(report, 'utf8')).toThrow('ENOENT');
expectMentions(explorer, [['not', 'prerequisites', 'independent']], 'explorer');
expectMentions(explorer, [['no', 'current-input', 'contracts']], 'explorer');
} finally {
fs.renameSync(`${resolvedReport}.absent`, resolvedReport);
}
}
if (host.name === 'kiro') {
const templates = path.join(registry, 'gstack-qa/templates');
const report = path.join(templates, 'functional-report-template.md');
const sourceReport = path.join(native, 'gstack-qa/templates/functional-report-template.md');
for (const candidate of [templates, report, sourceReport]) {
expect(fs.realpathSync(candidate).startsWith(fs.realpathSync(base) + path.sep)).toBe(true);
}
const original = fs.readFileSync(sourceReport, 'utf8');
const custom = path.join(templates, 'custom.md');
fs.writeFileSync(custom, 'unrelated user template');
try {
const updated = original + '\nUpdated report instructions.\n';
fs.writeFileSync(sourceReport, updated);
if (!copy) {
const previous = path.join(source, 'previous-report.md');
fs.writeFileSync(previous, original);
fs.unlinkSync(report);
fs.symlinkSync(previous, report);
}
const refreshed = runInstall();
expect(refreshed.status, refreshed.stderr).toBe(0);
expect(fs.readFileSync(report, 'utf8')).toBe(updated);
expect(fs.readFileSync(custom, 'utf8')).toBe('unrelated user template');
fs.unlinkSync(report);
fs.writeFileSync(report, 'foreign authored report');
const fileCollision = runInstall();
expect(fileCollision.status, fileCollision.stderr).toBe(0);
expect(fileCollision.stderr).toContain('existing file is not gstack-managed');
expect(fs.readFileSync(report, 'utf8')).toBe('foreign authored report');
const foreign = fs.mkdtempSync(path.join(base, 'foreign-'));
const foreignReport = path.join(foreign, 'functional-report-template.md');
fs.writeFileSync(foreignReport, 'foreign link target');
fs.unlinkSync(report);
fs.symlinkSync(foreignReport, report);
const fileLinkCollision = runInstall();
expect(fileLinkCollision.status, fileLinkCollision.stderr).toBe(0);
expect(fileLinkCollision.stderr).toContain('file link is not gstack-managed');
expect(fs.readlinkSync(report)).toBe(foreignReport);
expect(fs.readFileSync(foreignReport, 'utf8')).toBe('foreign link target');
fs.renameSync(templates, `${templates}.saved`);
fs.symlinkSync(foreign, templates);
const directoryLinkCollision = runInstall();
expect(directoryLinkCollision.status, directoryLinkCollision.stderr).toBe(0);
expect(directoryLinkCollision.stderr).toContain('directory link is not gstack-managed');
expect(fs.readlinkSync(templates)).toBe(foreign);
expect(fs.readFileSync(foreignReport, 'utf8')).toBe('foreign link target');
expect(fs.readFileSync(`${templates}.saved/custom.md`, 'utf8')).toBe('unrelated user template');
fs.unlinkSync(templates);
fs.writeFileSync(templates, 'foreign file at templates directory');
const directoryFileCollision = runInstall();
expect(directoryFileCollision.status, directoryFileCollision.stderr).toBe(0);
expect(directoryFileCollision.stderr).toContain('existing entry is not a directory');
expect(fs.readFileSync(templates, 'utf8')).toBe('foreign file at templates directory');
} finally {
fs.writeFileSync(sourceReport, original);
}
}
}
}
}, 60_000);
}
}
});