mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
touchfiles.test.ts now checks, per key, that the paid file's static test/helpers and test/fixtures closure (plus fixture paths it names in string literals) is covered, and names the file, path, chain and key to fix when it is not. Free *.test.ts files are no longer touchfiles, so editing a free replay test stops selecting paid evals: 950 entries removed, 653 real closure paths added. The hand-copied inventories go: periodic-fixture-selection, fake-impeccable-touchfiles and 45 per-file selection examples. Selection for the sample edits (plan-eng-review template, claude-pty-runner, plan-count-fixture, gstack-config) loses no case under either profile. CONTRIBUTING documents the rule and its lower bound.
112 lines
7.0 KiB
TypeScript
112 lines
7.0 KiB
TypeScript
import {expect, test} from 'bun:test';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {ALL_HOST_CONFIGS} from '../hosts';
|
|
import {HOST_PATHS, type TemplateContext} from '../scripts/resolvers/types';
|
|
import {generatePreamble} from '../scripts/resolvers/preamble';
|
|
import {generatePreambleBash} from '../scripts/resolvers/preamble/generate-preamble-bash';
|
|
import {generatePlanModeInfo} from '../scripts/resolvers/preamble/generate-completion-status';
|
|
import {generateDesignHardRules} from '../scripts/resolvers/design';
|
|
import {E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES, selectTests} from './helpers/touchfiles';
|
|
|
|
const root = path.resolve(import.meta.dir, '..');
|
|
const announcement = 'Scope gate: plan mode — auto-selected B (reviewing <target>).';
|
|
const section = fs.readFileSync(path.join(root, 'plan-design-review/sections/review-sections.md.tmpl'), 'utf8');
|
|
function context(skillName: string, host: typeof ALL_HOST_CONFIGS[number]): TemplateContext {
|
|
return {skillName, tmplPath: `${skillName}/SKILL.md.tmpl`, host: host.name,
|
|
paths: HOST_PATHS[host.name]!, preambleTier: 3, interactive: true};
|
|
}
|
|
function assertScopedEntry(preamble: string) {
|
|
const command = preamble.indexOf('```bash');
|
|
expect(command).toBeGreaterThan(0);
|
|
const entry = preamble.slice(0, command);
|
|
expect(entry).toContain('## Preamble (after scope gate)');
|
|
expect(entry).toContain('resolve the Scope gate above');
|
|
expect(entry).toContain('If the gate asks a question, wait for its answer');
|
|
expect(preamble).not.toContain('## Preamble (run first)');
|
|
expect(preamble).toContain('starting from the Scope gate');
|
|
expect(preamble).not.toContain('starting from Step 0;');
|
|
}
|
|
|
|
test('Design and Eng resolve the existing gate at the first executable preamble in every host', () => {
|
|
for (const host of ALL_HOST_CONFIGS) for (const skill of ['plan-design-review', 'plan-eng-review']) {
|
|
const ctx = context(skill, host);
|
|
assertScopedEntry(generatePreamble(ctx));
|
|
const tmpl = fs.readFileSync(path.join(root, `${skill}/SKILL.md.tmpl`), 'utf8');
|
|
expect(tmpl.indexOf(announcement)).toBeLessThan(tmpl.indexOf('{{PREAMBLE}}'));
|
|
expect(tmpl).toContain('If the user explicitly named a DIFFERENT target');
|
|
expect(tmpl).toContain('If plan mode is indicated but no plan exists yet, ask as normal');
|
|
}
|
|
});
|
|
|
|
test('missing local checkpoint and competing Step 0 entry instructions fail the generation contract', () => {
|
|
const text = generatePreamble(context('plan-design-review', ALL_HOST_CONFIGS[0]!));
|
|
expect(() => assertScopedEntry(text.replace('resolve the Scope gate above', ''))).toThrow();
|
|
expect(() => assertScopedEntry(text.replace('## Preamble (after scope gate)', '## Preamble (run first)'))).toThrow();
|
|
expect(() => assertScopedEntry(text.replace('starting from the Scope gate', 'starting from Step 0; then the Scope gate'))).toThrow();
|
|
});
|
|
|
|
test('other skills keep their existing preamble entry and plan-mode starting point', () => {
|
|
for (const host of ALL_HOST_CONFIGS) for (const skill of ['plan-ceo-review', 'plan-devex-review', 'ship', 'review']) {
|
|
const ctx = context(skill, host);
|
|
expect(generatePreambleBash(ctx).startsWith('## Preamble (run first)\n\n```bash')).toBe(true);
|
|
expect(generatePlanModeInfo(ctx)).toContain('starting from Step 0;');
|
|
expect(generatePreambleBash(ctx)).not.toContain('Before the command below');
|
|
}
|
|
});
|
|
|
|
test('hard rules remain inside the scored fourth pass without changing other skills heading levels', () => {
|
|
const host = ALL_HOST_CONFIGS[0]!;
|
|
const rules = generateDesignHardRules(context('plan-design-review', host));
|
|
const rendered = section.replace('{{DESIGN_HARD_RULES}}', rules);
|
|
const start = rendered.indexOf('### Pass 4:');
|
|
const end = rendered.indexOf('### Pass 5:');
|
|
const pass = rendered.slice(start, end);
|
|
expect(pass.indexOf('**Pass 4 evaluation:**')).toBeLessThan(pass.indexOf('#### Design Hard Rules'));
|
|
expect(pass.match(/^### /gm)).toHaveLength(1);
|
|
expect(pass).toContain('An unresolved hard rejection caps this pass below 8');
|
|
expect(pass).toContain('Litmus answers support findings, not a separate numeric score.');
|
|
for (const skill of ['design-review', 'design-consultation', 'design-html']) {
|
|
expect(generateDesignHardRules(context(skill, host)).startsWith('### Design Hard Rules\n')).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('the decision table preserves first approval, inherited approvals, artifact and navigation boundaries', () => {
|
|
const cases = section.slice(section.indexOf('| Situation |'), section.indexOf('{{LEARNINGS_SEARCH}}'));
|
|
expect(cases).toContain('exact fix already has an individual user decision or a preamble-authorized per-issue auto-decision');
|
|
expect(cases).toContain('do not ask again');
|
|
expect(cases).toContain('copied unchanged into a required artifact');
|
|
expect(cases).toContain('it approves no new remedy');
|
|
expect(cases).toContain('no individual decision has approved its fix');
|
|
expect(cases).toContain('even if the input names the gap or DESIGN.md prescribes the exact token');
|
|
expect(cases).toContain('Keep the proposed remedy pending');
|
|
expect(cases).toContain('new tradeoff');
|
|
expect(section).toContain('Scope, focus, setup, and next-step choices approve no remedies.');
|
|
expect(section).toContain('Never edit first and ask afterward.');
|
|
expect(section).toContain('An unapproved violation still needs its first individual decision.');
|
|
});
|
|
|
|
test('scoring and section dependencies are explicit while existing clean and pass thresholds remain', () => {
|
|
expect(section).toContain('use the lowest of the six rated pass scores (1-6)');
|
|
expect(section).toContain('separately before and after approved fixes');
|
|
expect(section).toContain('Pass 7 is unscored');
|
|
expect(section.replace(/\s+/g, ' ')).toContain('An overall 8+ therefore means every rated pass is 8+; unresolved findings still prevent a clean review log');
|
|
expect(section).toContain('After Read-back, if all passes 8+');
|
|
expect(section).toContain('"clean" if overall score 8+ AND 0 unresolved');
|
|
expect(section).toContain('If DESIGN.md is absent, rate the plan\'s explicit token and component specifications');
|
|
expect(section).toContain('do not skip the score or assume alignment');
|
|
expect(section).toContain('Read `~/.claude/skills/gstack/plan-design-review/SKILL.md`');
|
|
expect(section).toContain('Use their existing results; do not restart the review.');
|
|
expect(section).toContain('After Pass 7: offer the mockup update below when applicable, resolve deferred TODO proposals, reconcile approvals, then synthesize tasks and the Completion Summary.');
|
|
});
|
|
|
|
test('new regression and changed preamble sources select all Design and Eng owners', () => {
|
|
for (const map of [E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES]) {
|
|
const affected = selectTests(['plan-design-review/SKILL.md.tmpl', 'plan-eng-review/SKILL.md.tmpl'], map, []).selected;
|
|
for (const source of ['scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts']) {
|
|
const selected = selectTests([source], map, []).selected;
|
|
for (const owner of affected) expect(selected).toContain(owner);
|
|
}
|
|
}
|
|
});
|