mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +02:00
- design-consultation Phase 1 asks one brief that confirms context and decides
research; the confirm-only first question scored substance 2.
- document-release defines ship-owned inputs, exact steps and the JSON result,
and drops stale spawned-from-/ship text (judge actionability 3.67 -> 4/4/4).
- plan-design-with-ui accepts the Step 0D focus menu the same way the shared
picker does ("focus on specific ones?").
- plan-design-review plan-mode saves in three Edits instead of one final Write.
- QA functional annotations ask for the full 40-character revision.
- Outside-disabled attribution judges quoted prior-record data by its exact
timestamp or a dated, pre-existing-record sentence; four captured phrasings
replay clean and current claims still fail.
- --case can select autoplan-dual-voice by its literal test name.
258 lines
18 KiB
TypeScript
258 lines
18 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
// Execute the real paid registration with native observation mocked. The actual
|
|
// actor picker, exact seeding, owned binding, assertions and cleanup execute.
|
|
function exercise(mode: string) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'design-ui-free-'));
|
|
const script = path.join(directory, 'registration.test.ts');
|
|
const facts = path.join(directory, 'facts.json');
|
|
fs.writeFileSync(script, `
|
|
import { describe, expect, mock, spyOn } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
const mode = ${JSON.stringify(mode)};
|
|
const fixture = ${JSON.stringify(path.join(ROOT, 'test/fixtures/plans/ui-heavy-feature.md'))};
|
|
const template = fs.readFileSync(${JSON.stringify(path.join(ROOT, 'plan-design-review/SKILL.md.tmpl'))}, 'utf8');
|
|
const focus = template.match(/### 0D\\. Focus Areas\\nAskUserQuestion: "([^\\n]+)"/)![1]
|
|
.replace('{N}', '4').replace('{X, Y, Z}', 'hierarchy, spacing, contrast');
|
|
const { DESIGN_BOARD_ACTOR_PROTOCOL, createDesignReviewPicker: actualCreateDesignReviewPicker } = await import(${JSON.stringify(path.join(ROOT, 'test/helpers/plan-review-board-feedback.ts'))});
|
|
const outsideVoices = JSON.parse(fs.readFileSync(${JSON.stringify(path.join(ROOT, 'test/fixtures/design-outside-voices-question.json'))}, 'utf8'));
|
|
let pickerScope;
|
|
let pickerQuestion;
|
|
mock.module(${JSON.stringify(path.join(ROOT, 'test/helpers/plan-review-board-feedback.ts'))}, () => ({
|
|
DESIGN_BOARD_ACTOR_PROTOCOL,
|
|
createDesignReviewPicker: scope => {
|
|
pickerScope = scope;
|
|
const actualPicker = actualCreateDesignReviewPicker(scope);
|
|
return question => { pickerQuestion = question; return actualPicker(question); };
|
|
},
|
|
}));
|
|
const { createPlanCountFixture } = await import(${JSON.stringify(path.join(ROOT, 'test/helpers/plan-count-fixture.ts'))});
|
|
const { nativePlanCallFingerprint } = await import(${JSON.stringify(path.join(ROOT, 'test/helpers/claude-pty-runner.ts'))});
|
|
const fp = (id, question, preReview = true) => nativePlanCallFingerprint({
|
|
sessionId: 'owned-ui-fixture', toolUseId: id, answered: true, failed: false,
|
|
unansweredQuestionIndices: [],
|
|
questions: [{ header: 'Review', question, multiSelect: false, options: [
|
|
{ label: 'Review all 7 dimensions (recommended)', description: 'Review the supplied UI plan.' },
|
|
{ label: 'Review a different branch', description: 'That branch may have no UI scope; design review is not applicable there.' },
|
|
] }],
|
|
}, 1000, preReview);
|
|
const target = fp('target', 'D1 — What should I design-review?\\nProject/branch/task: gstack repo, no plan file drafted yet.\\nELI10: A design review needs a target. Pick what I should rate 0-10 across the 7 design dimensions.');
|
|
target.nativeCall.questions[0].header = 'Review target';
|
|
if (mode === 'target-menu-design-system') {
|
|
target.nativeCall.questions[0].question = 'What should I design-review? ELI10: Choose a plan, design system, or the current branch diff.';
|
|
target.promptSnippet = target.nativeCall.questions[0].question;
|
|
}
|
|
target.nativeCall.questions[0].options = [
|
|
{ label: 'B) A plan or design doc (recommended)', description: 'You need to paste the plan or point me to a path.' },
|
|
{ label: 'A) The current branch diff', description: 'The working tree is clean, so there may be no UI scope to review.' },
|
|
];
|
|
const paraphrase = fp('focus', 'D1 — Review all 7 design dimensions, or focus on specific areas?\\nELI10: I rated this plan 4/10.');
|
|
// Public titles/options projected from the retained Sep 11 native PostToolUse
|
|
// events: focus toolu_01XJZk6qbs3Fj3VRbm6sCNv2, setup toolu_01WkR4juMdcMxCTJe8FfRMVY,
|
|
// finding toolu_012EsyqshgBk2Ap7CfuzwhwQ. No transcript paths or private content.
|
|
const nativeFocus = fp('native-focus', mode === 'native-passes'
|
|
? 'D1 — Review all 7 design passes, or focus?'
|
|
: 'D1 — Review all 7 design dimensions, or focus?');
|
|
nativeFocus.nativeCall.questions[0].options = [
|
|
{ label: 'All 7 dimensions (recommended)', description: 'Full review: hierarchy, states, journey, specificity, AI slop, responsive, accessibility. Completeness 10/10.' },
|
|
{ label: 'States + hierarchy + a11y', description: 'Focus on the three weakest areas only. Completeness 7/10.' },
|
|
{ label: 'Visual + AI slop only', description: 'Focus on look and originality, skip state and interaction passes. Completeness 4/10.' },
|
|
];
|
|
const nativeSetup = fp('native-setup', 'D2 — Run outside design voices before the 7 passes?', false);
|
|
nativeSetup.nativeCall.questions[0].options = [
|
|
{ label: 'Yes, run outside voices (recommended)', description: 'Codex design critique + independent Claude subagent, run in parallel, synthesized into a litmus scorecard.' },
|
|
{ label: 'No, skip to the passes', description: 'Go straight to Pass 1 with my review only.' },
|
|
];
|
|
const nativeFinding = fp('native-finding', "D3 — Issue 1 [HARD REJECTION risk]: Which region is the dashboard's primary anchor, and how is the page composed?", false);
|
|
nativeFinding.nativeCall.questions[0].options = [
|
|
{ label: '1A Notifications-primary (recommended)', description: 'Status line + unread count anchors; notifications ~7/12, activity ~5/12; mobile order status, notifications, activity. Completeness 10/10.' },
|
|
{ label: '1B Activity-primary (Codex)', description: 'Activity dominant ~7/12, notifications right rail; primary action beside title. Completeness 10/10.' },
|
|
{ label: '1C Three peer regions, decide later', description: 'Keep the plan as written; hard rejection #1 stays open. Completeness 3/10.' },
|
|
];
|
|
const unnumberedFinding = fp('unnumbered-finding', "Which region is the dashboard's primary anchor, and how is the page composed?", false);
|
|
unnumberedFinding.nativeCall.questions[0].options = nativeFinding.nativeCall.questions[0].options;
|
|
const finding = fp('finding', 'Which loading feedback should Save show?', false);
|
|
finding.nativeCall.questions[0].options = [
|
|
{ label: 'Saving label and spinner (recommended)', description: 'Show in-flight feedback in the Save button.' },
|
|
{ label: 'Keep the current button', description: 'Keep the button label and current loading feedback.' },
|
|
];
|
|
let calls = 0;
|
|
fs.writeFileSync(${JSON.stringify(facts)}, JSON.stringify({ calls }));
|
|
mock.module(${JSON.stringify(path.join(ROOT, 'test/helpers/e2e-gate.ts'))}, () => ({
|
|
describeE2ETier: tier => { expect(tier).toBe('gate'); return describe; },
|
|
}));
|
|
mock.module(${JSON.stringify(path.join(ROOT, 'test/helpers/claude-pty-runner.ts'))}, () => ({
|
|
runPlanSkillCounting: async opts => {
|
|
calls++;
|
|
expect(opts.cwd).toBeUndefined();
|
|
const ownedFixture = createPlanCountFixture(opts.followUpPrompt, {nativeReviewOnly: true, files: opts.fixtureFiles});
|
|
const cwd = ownedFixture.cwd;
|
|
try {
|
|
expect(path.dirname(cwd)).toBe(${JSON.stringify(directory)});
|
|
expect(cwd).not.toBe(${JSON.stringify(ROOT)});
|
|
expect(opts.skillName).toBe('plan-design-review');
|
|
expect(opts).not.toHaveProperty('readDesignArtifacts');
|
|
expect(opts.slashCommand).toBe('/plan-design-review');
|
|
expect(opts.followUpPrompt).toContain(fs.readFileSync(${JSON.stringify(path.join(ROOT, 'test/fixtures/plans/ui-heavy-feature.md'))}, 'utf8'));
|
|
expect(opts.followUpPrompt).toContain(DESIGN_BOARD_ACTOR_PROTOCOL);
|
|
expect(opts.reviewCountCeiling).toBe(1);
|
|
expect(opts.observeSetupQuestions).toBe(true);
|
|
expect(opts.bindDesignBoardState).toBe(true);
|
|
expect(opts).not.toHaveProperty('questionPick');
|
|
expect(typeof opts.pickAUQ).toBe('function');
|
|
expect(opts).not.toHaveProperty('model');
|
|
expect(opts.timeoutMs).toBeGreaterThan(0);
|
|
expect(opts.timeoutMs).toBeLessThanOrEqual(600_000);
|
|
expect(fs.readFileSync(path.join(cwd, 'review-input.md'), 'utf8')).toBe(fs.readFileSync(fixture, 'utf8'));
|
|
const instructions = fs.readFileSync(path.join(cwd, 'CLAUDE.md'), 'utf8');
|
|
expect(instructions).toContain(DESIGN_BOARD_ACTOR_PROTOCOL);
|
|
expect(instructions).toContain('Decline the optional Design Outside Voices step');
|
|
expect(instructions).toContain('all ordinary Design review decisions still apply');
|
|
expect(execFileSync('git', ['show', 'HEAD:CLAUDE.md'], { cwd: cwd, encoding: 'utf8', timeout: 5000 })).toBe(instructions);
|
|
expect(execFileSync('git', ['diff', 'origin/main...HEAD'], { cwd: cwd, encoding: 'utf8', timeout: 5000 })).toBe('');
|
|
expect(execFileSync('git', ['show', 'HEAD:review-input.md'], { cwd: cwd, encoding: 'utf8', timeout: 5000 })).toBe(fs.readFileSync(fixture, 'utf8'));
|
|
expect(fs.readFileSync(path.join(cwd, 'CLAUDE.md'), 'utf8')).toContain('Read it before\\nchoosing review scope.');
|
|
expect(opts.isLastStep0AUQ(target)).toBe(false);
|
|
expect(opts.isLastStep0AUQ(fp('focus', focus))).toBe(true);
|
|
expect(opts.isLastStep0AUQ(paraphrase)).toBe(true);
|
|
// Census 36633323521 gate-census-7: the same Step 0D menu titled with "specific ones".
|
|
expect(opts.isLastStep0AUQ(fp('focus-ones', 'D1 — Review all 7 design dimensions, or focus on specific ones?'))).toBe(true);
|
|
if (mode.startsWith('native-')) expect(opts.isLastStep0AUQ(nativeFocus)).toBe(true);
|
|
for (const unrelated of ['Review all 4 design passes, or focus?',
|
|
'Review all 7 engineering passes, or focus?', 'Review all 7 passes, or focus?',
|
|
'Which plan should receive all 7 design passes?']) {
|
|
expect(opts.isLastStep0AUQ(fp('unrelated', unrelated))).toBe(false);
|
|
}
|
|
expect(opts.isReviewAUQ(target)).toBe(false);
|
|
expect(opts.isReviewAUQ(nativeFocus)).toBe(false);
|
|
expect(opts.isReviewAUQ(nativeSetup)).toBe(false);
|
|
expect(opts.isReviewAUQ(nativeFinding)).toBe(true);
|
|
expect(opts.isReviewAUQ(unnumberedFinding)).toBe(true);
|
|
expect(opts.isReviewAUQ(finding)).toBe(true);
|
|
// Titles and option-label prefixes projected from Sep 29 local captures of the
|
|
// real skill. The first four went unrecognized, so the run timed out after
|
|
// four answered findings; issue-numbered option sets identify each finding.
|
|
const labeled = (id, question, labels) => {
|
|
const call = fp(id, question, false);
|
|
call.nativeCall.questions[0].options = labels.map(label => ({ label, description: '' }));
|
|
return call;
|
|
};
|
|
for (const [question, labels] of [
|
|
['D7 — Issue 1: Codify the page hierarchy from approved Variant A in the plan?', ['1A) Full hierarchy with caps and overflo', '1B) Layout and read order only', '1C) Leave hierarchy out']],
|
|
['D8 — Issue 2: How does the dashboard fit the existing app shell and navigation?', ['2A) Reuse existing shell, Dashboard nav ', '2B) Standalone minimal header, no nav it', '2C) Leave the shell unspecified']],
|
|
['D9 — Issue 3: Add a user-visible interaction state table for every dashboard feature?', ['3A) Full state table with copy (recommen', '3B) Structure only, copy TBD', '3C) Leave states as listed']],
|
|
['D10 — Issue 4: Confirmation pattern and content for "Mark all as read"?', ['4A) Keep modal, specify copy and labels ', '4B) No modal; immediate action plus undo', '4C) Keep modal, leave copy to implemente']],
|
|
['Issue 1 — What does the user see first on a phone?', ['1A: Notifications first on sm, feed firs', '1B: Feed first everywhere', '1C: Bell badge + drawer on sm']],
|
|
]) expect(opts.isReviewAUQ(labeled('captured-issue', question, labels))).toBe(true);
|
|
for (const [question, labels] of [
|
|
['D11 — Update the approved mockups with these decisions?', ['A) Regenerate mockups (recommended)', 'B) Keep the current mockups']],
|
|
['D12 — Which follow-up should I record?', ['1A) Record a TODO', '2B) Skip it']],
|
|
['D13 — Which follow-up should I record?', ['1A) Record a TODO', '1A) Record it again']],
|
|
['D14 — Record this follow-up?', ['1A) Record a TODO']],
|
|
['D15 — Want outside design voices before the detailed review?', ['1A) Yes, run outside voices', '1B) No, proceed without']],
|
|
]) expect(opts.isReviewAUQ(labeled('not-a-finding', question, labels))).toBe(false);
|
|
const chosenFocus = mode.startsWith('native-') ? nativeFocus : mode === 'paraphrase' ? paraphrase : fp('focus', focus);
|
|
const pendingCall = {...chosenFocus.nativeCall, answered: false, unansweredQuestionIndices: [0]};
|
|
const pending = nativePlanCallFingerprint(pendingCall, 1000, true);
|
|
pending.nativeQuestionIndex = 0;
|
|
const context = Object.freeze({cwd, deadlineAt: Date.now() + opts.timeoutMs - 5000});
|
|
expect(opts.pickAUQ(pending, pending, context)).toBe(1);
|
|
expect(pickerScope).toEqual(context);
|
|
expect(pickerQuestion).toEqual(pendingCall.questions[0]);
|
|
expect(opts.pickAUQ(chosenFocus, chosenFocus, context)).toBeNull();
|
|
// Replay the retained public menu; this synthetic answer is not a rewrite
|
|
// of its historical Yes. No optional outside-review answer counts as a finding.
|
|
const voicesCall = { sessionId: 'owned-ui-fixture', toolUseId: 'outside-voices-replay',
|
|
answered: false, failed: false, unansweredQuestionIndices: [0],
|
|
questions: [outsideVoices.question] };
|
|
const pendingVoices = nativePlanCallFingerprint(voicesCall, 1000, false);
|
|
pendingVoices.nativeQuestionIndex = 0;
|
|
expect(opts.pickAUQ(pendingVoices, pendingVoices, context)).toBe(2);
|
|
expect(pickerQuestion).toEqual(outsideVoices.question);
|
|
expect(opts.isReviewAUQ(pendingVoices)).toBe(false);
|
|
const declinedVoices = nativePlanCallFingerprint({ ...voicesCall, answered: true,
|
|
unansweredQuestionIndices: [], answers: { [outsideVoices.question.question]: 'No, proceed without' } }, 1000, false);
|
|
expect(opts.isReviewAUQ(declinedVoices)).toBe(false);
|
|
expect(opts.pickAUQ(declinedVoices, declinedVoices, context)).toBeNull();
|
|
const explainedQuestion = outsideVoices.sameLineExplanation77.question;
|
|
const explainedCall = { ...voicesCall, toolUseId: 'outside-voices-explanation-replay', questions: [explainedQuestion] };
|
|
const explainedPending = nativePlanCallFingerprint(explainedCall, 1000, false);
|
|
explainedPending.nativeQuestionIndex = 0;
|
|
expect(opts.pickAUQ(explainedPending, explainedPending, context)).toBe(1);
|
|
expect(pickerQuestion).toEqual(explainedQuestion);
|
|
expect(opts.isReviewAUQ(explainedPending)).toBe(false);
|
|
const explainedDeclined = nativePlanCallFingerprint({ ...explainedCall, answered: true,
|
|
unansweredQuestionIndices: [], answers: { [explainedQuestion.question]: 'No, proceed without (recommended)' } }, 1000, false);
|
|
expect(opts.isReviewAUQ(explainedDeclined)).toBe(false);
|
|
expect(opts.pickAUQ(explainedDeclined, explainedDeclined, context)).toBeNull();
|
|
const unboundBoard = {...pending, nativeCall: undefined, options: [{index: 1, label: 'Submitted'}]};
|
|
expect(() => opts.pickAUQ(unboundBoard, unboundBoard, context)).toThrow('requires an owned native question');
|
|
fs.writeFileSync(${JSON.stringify(facts)}, JSON.stringify({ calls, cwd: cwd, seeded: true }));
|
|
if (mode === 'throw') throw new Error('controlled UI observation failure');
|
|
const observed = mode.startsWith('target-menu') ? [target, fp('other', 'Which artifact should I inspect?', false)]
|
|
: mode === 'early-exit' ? [] : mode === 'focus-only' ? [chosenFocus]
|
|
: mode === 'native-setup-only' ? [nativeFocus, nativeSetup]
|
|
: mode === 'native-voices-only' ? [nativeFocus, declinedVoices]
|
|
: mode.startsWith('native-') ? [nativeFocus, nativeSetup, mode === 'native-unnumbered' ? unnumberedFinding : nativeFinding]
|
|
: [chosenFocus, finding];
|
|
if (mode === 'unanswered-finding') observed.at(-1).nativeCall.answered = false;
|
|
if (mode === 'failed-finding') observed.at(-1).nativeCall.failed = true;
|
|
if (mode === 'no-native-finding') delete observed.at(-1).nativeCall;
|
|
return {
|
|
outcome: mode === 'early-exit' ? 'plan_ready' : mode === 'timeout' ? 'timeout' : mode === 'exited' ? 'exited' : 'ceiling_reached',
|
|
fingerprints: observed, step0Count: 1, reviewCount: 1, elapsedMs: 1000,
|
|
evidence: mode === 'early-exit' ? "This plan has no UI scope. A design review isn't applicable."
|
|
: 'Unselected alternative: The other branch may have no UI scope.',
|
|
};
|
|
} finally { ownedFixture.cleanup(); }
|
|
},
|
|
}));
|
|
if (mode === 'missing-fixture') {
|
|
const read = fs.readFileSync;
|
|
spyOn(fs, 'readFileSync').mockImplementation((file, ...args) => {
|
|
if (String(file) === fixture) throw Object.assign(new Error('UI fixture is missing'), { code: 'ENOENT' });
|
|
return read(file, ...args);
|
|
});
|
|
}
|
|
await import(${JSON.stringify(path.join(ROOT, 'test/skill-e2e-plan-design-with-ui.test.ts'))});
|
|
`);
|
|
try {
|
|
const child = Bun.spawnSync([process.execPath, 'test', script], {
|
|
cwd: ROOT, timeout: 10_000,
|
|
env: { PATH: process.env.PATH ?? '', HOME: directory, TMPDIR: directory, TEMP: directory, TMP: directory,
|
|
GIT_CONFIG_NOSYSTEM: '1', ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
|
|
});
|
|
const output = child.stdout.toString() + child.stderr.toString();
|
|
expect(child.signalCode ?? null, output).toBeNull();
|
|
const observed = JSON.parse(fs.readFileSync(facts, 'utf8'));
|
|
expect(observed.calls, output).toBe(mode === 'missing-fixture' ? 0 : 1);
|
|
if (mode !== 'missing-fixture') expect(observed.seeded, output).toBe(true);
|
|
expect(fs.readdirSync(directory).filter(name => name.startsWith('gstack-plan-count-') || name.startsWith('gstack-native-review-state-'))).toEqual([]);
|
|
return { code: child.exitCode, output };
|
|
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
|
}
|
|
|
|
test.each(['source', 'paraphrase', 'native-sequence', 'native-unnumbered', 'native-passes'])('UI gate seeds the exact target and accepts Design progress with an unselected no-UI alternative (%s)', mode => {
|
|
const result = exercise(mode);
|
|
expect(result.code, result.output).toBe(0);
|
|
}, 20_000);
|
|
|
|
test.each(['target-menu', 'target-menu-design-system', 'focus-only', 'native-setup-only', 'native-voices-only', 'early-exit', 'timeout', 'exited', 'unanswered-finding', 'failed-finding', 'no-native-finding'])('UI gate rejects %s and removes its fixture', mode => {
|
|
const result = exercise(mode);
|
|
expect(result.code, result.output).toBe(1);
|
|
expect(result.output).toContain('plan-design-review with UI scope FAILED');
|
|
}, 20_000);
|
|
|
|
test.each(['throw', 'missing-fixture'])('UI gate preserves %s failure and removes its fixture', mode => {
|
|
const result = exercise(mode);
|
|
expect(result.code, result.output).toBe(1);
|
|
expect(result.output).toContain(mode === 'throw' ? 'controlled UI observation failure' : 'UI fixture is missing');
|
|
}, 20_000);
|