mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +02:00
* test: delete test-infrastructure dead code (G) - exit-propagation drives the runner's real strict verdict (BunTestOutputClassifier + strictTestExitCode); delete the unused shardRunLooksTruncated predicate. - delete skill-coverage-matrix registry + its gate (nothing reads it; the floor already iterates skillCensus()). - delete touchfiles-facade export-parity tests (Bun fails missing imports at link time) and the duplicated E2E_TIERS tier-value test. - delete brain-cache-spec TRANSPORT_DEFAULT_POLICY, SKILL_RUN_RETENTION_DAYS and the now-unused BrainTrustPolicy type with their literal tests. AUTOPLAN_PREFLIGHT_BUDGET_BYTES stays: skill-preflight-budget enforces it against real resolver output. - delete audit-compliance's JSDoc-comment grep. * test: replace product tests that fake the product with real-boundary tests (F) - design: serve.test.ts drove an inline mirror server; now two tests run the real serve() on an ephemeral port (reload confinement, submit exit 0). - setup-gbrain: rollback + voyage tests execute the template-extracted init blocks (3 sites) instead of drifted local bash copies. - terminal-agent: internalHandler source greps replaced by a behavioral /internal/grant + /internal/revoke auth matrix (no/wrong/valid token). - /health: server-security-surface and the server-auth / security-audit-r2 / sidebar-tabs source greps fold into one liveness-only check on the real body; the L4 sidecar wiring gets a behavioral /pty-inject-scan test. - delete tautologies (browser-manager onDisconnect, memory-command #12), ios swiftui tap fixture self-check, memory-ingest put_page grep, detach source greps, sidebar-agent absence pins, dead-CSS pins + the dead CSS, security-audit-r2 Task 1 + the test-only meta-commands re-export, duplicate generated-SKILL.md checks. - make-pdf coverage-gaps cases move into their owner test files. * test: delete tests of dead eval code (A) - A1: the retired Eng lexical oracle (evaluateEngSeedCoverage, isEngSeedDecisionAUQ), the completion-handoff detector and the retained corpus had no paid caller since v1.87.6; delete their 26 replay files, ~2.6k helper LOC and fixtures, and the dead blocks in 8 mixed files (live hasNativePlanTerminal / batching assertions stay). - A2: dead viewport approvers in autoplan-artifact-permission and their 11 replay files + fixtures; recorder/launcher cases stay. - A3: never-wired oracles and seeders (autoplan-phase-order, eng-finding-fixture, ceo-paired-fixture, design-ui-scope, plan-skill-completion, pty-current-screen, required-reads, transcript-section-logger); plan-seed-submission now decodes through the production createPtyScreen; section manifests name their actual guard. - A4: zero-reference helper exports, plus execGit and invokeAndObserve found by the reachability pass. - 52 fixtures orphaned by the deletions; touchfile and selection-table entries for every deleted path. * test: clean up the paid eval lane (B1-B4, B6, B7) - B1: delete paid files that assert nothing or cannot pass meaningfully: skill-llm-eval-spec and skill-e2e-spec-execute (test.todo), gemini-e2e (+ gemini-session-runner; no gemini CLI in CI), ship-idempotency (red since v1.63), the two opus-4-7 *-sonnet overlay wrappers, conductor-prose (+ its source-evaluation replay), codex-e2e-plan-format; drop their keys, scripts and census rows. - B2: skill-llm-eval grades browse/sections/command-list.md with one union judge that also carries the baseline score pin; regression-vs-baseline deleted (paid run: pass, c4/c4/a4). - B3: memory-pipeline, ios-qa, ios-qa-swift-build and plan-tune-cathedral make no model calls; renamed out of the paid glob so they run on every PR. Swift builds need GSTACK_TEST_SWIFT=1; device stub deleted. - B4: codex-e2e*, outside-voice, aside and ios-device cannot run in the CI image; excluded from the weekly lane with a tracked re-entry condition. - B6: fold opus-47's negative routing controls into skill-routing-e2e journey-negatives (paid run: 3/3 unrouted) and delete the file. - B7: delete the never-green brain-privacy-gate eval; a free gstack-skill-start test now proves consent precedes artifacts egress. * test: retire the finding-count cluster and trim its helpers (C) - C0/C1: the five never-green evals (skill-e2e-autoplan-chain and skill-e2e-plan-{ceo,eng,design,devex}-finding-count) failed on harness and budget, never on skill behavior; delete them, their touchfile/tier ids, AUTOPLAN_CHAIN_BUDGET and the dedicated eighth periodic slice (--slices 7). - C2: delete the helper groups whose only paid consumers were those files (11 modules), trim claude-pty-runner and eng-seeded-coverage to the paid closure, and delete the free replay tests whose assertions exercised only that dead code (89 files, 135 orphaned fixtures). Blocks that used dead code only as input for a live subject keep their assertions: the multiSelect default moved to plan-review-decisions, runner PTY tests use inline caller policies, and the timer-safe budget checks moved to eng-finding-retry-budget. - The eight production-touching files stay except ceo-current-decision-record (its template read only feeds the retired counter). - CARVE_GUARDS.autoplan is behavioral 'none'; TODOS records the lost chain and per-finding cadence coverage with their re-entry tests. * test: fold per-incident replay series into their detector owners (D) Twelve detector families move into one owner test each: 73 incident files become describe blocks in ceo-section-loading-fixture (stale-fill race), model-overlays, coverage-audit-evidence, autoplan-phase-observer, native-auto-decide, outside-voice-evidence, eng-first-review, plan-count-completion, plan-count-file-permission, ceo-mode-option, plan-scope-selection and plan-count-prerequisite. Each block keeps its original code and fixture, so every case still runs; only tests asserting the incident file's own touchfile registration are dropped (41). Touchfile lists that named an incident now name its owner. * test: start the plan-count history PTY on its readiness marker (H) The fake CLI prints a startup marker and the runner waits for it instead of the fixed 8 s startup sleep (8.6 s -> 0.9 s locally). eng-semantic-terminal's sleeping registration cases went with C; plan-count-timeout keeps the fixed wait because it asserts deadline behavior. * test: derive paid touchfiles from each eval's static closure (E) 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. * test: skip hollow tier shards and census judges in the paid planner (B5) A paid file is now skipped for a tier lane only when every E2E id it registers is known statically and none has that tier; ids come from the touchfile registrations and literal testName/*IfSelected arguments, so a comment or skill path that quotes another id cannot unschedule it, and computed names keep today's scheduling. --list and the manifest show each skip as "skipped: no E2E_TIERS id has tier <tier>". The weekly gate census drops the LLM judges (--skip-judges); they still run in the periodic census and PR gate lanes. Gate lane 52 -> 42 files, census 41; periodic 77 -> 69. * test: run seven paid evals on the current default capture model (B8) skill-e2e-{auq-matrix,plan-format,qa-bugs,retro,workflow} pinned claude-opus-4-7 and skill-e2e-office-hours plus -brain-writeback pinned claude-sonnet-4-6; none tests a historical model, so they now capture with resolveEvalModel('capture'), and the free harness tests that execute these registrations receive the same resolver. The paid re-pin run passed all of them. skill-e2e-{design,office-hours-phase4,plan-prosons,plan} keep claude-opus-4-7: six of their cases failed on the default model (three timeouts, a missing report file, a format miss and a posture score of 3), so per the plan's fallback they keep their pins with a TODOS entry. The pre-spend estimate and drop threshold are in docs/test-audit-2026-09.md. * test: guard the reduced suite against new test-of-test files - test/test-of-test-ratchet.test.ts records the 228 free tests that import only test/ code and fails on a new one, naming the owner test to extend instead; a stale baseline entry fails with the remove instruction. - test/helpers/resolve-repo-path.ts is the one specifier/literal resolver for the ratchet and the touchfile closure invariant, with its own unit tests. - CONTRIBUTING "Test tiers" describes the paid-failure workflow (fix, then one row in the detector's owner test) and the ratchet; TEST_PORTFOLIO gains the detector -> owner-test table and no longer claims an Autoplan chain eval. - TODOS: automatic exclusion policy for chronically red periodic files (P3), the deferred native-completion table collapse, the unused CEO payment seeder; the PTY readiness item is narrowed to the paid runner. - docs/test-audit-2026-09.md collects the triage, security mapping, inventories, selection proof, behavior-commit decisions and retained false positives. * v1.91.8.0 test: smaller suite, derived paid selection, retired never-green evals Release metadata for the test-reduction branch: VERSION 1.91.8.0 (1.91.7.0 is claimed by #2983), CHANGELOG with the measured before/after table and a contributor section, durations re-recorded on Ubicloud standard-16 (857 files, 0 failures), the agents digest, CONTRIBUTING's after-measurement row, the B8 fallback TODOS entry, and the after metrics, kept-vs-plan notes, B8 run and census estimate in docs/test-audit-2026-09.md. * fix(ubicloud): skip retrieval globs that match nothing instead of reporting a failed pull * test: count issue-numbered Design findings in the UI-scope gate eval
1175 lines
73 KiB
TypeScript
1175 lines
73 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';
|
||
import { findNativeAutoDecision } from './helpers/native-auto-decide';
|
||
import { classifyVisible } from './helpers/claude-pty-runner';
|
||
import { readPlanCountTranscript, type NativePublicToolEvent } from './helpers/plan-count-transcript';
|
||
import capture_auto_decide_current_declaration from './fixtures/auto-decide-current-declaration-6aef.json';
|
||
import capture_auto_decide_explanatory_mode from './fixtures/auto-decide-explanatory-mode-043a.json';
|
||
import captured749_auto_decide_explanatory_mode from './fixtures/auto-decide-explanatory-mode-749df.json';
|
||
import annotations_auto_decide_explanatory_mode from './fixtures/native-auto-decide-ag.json';
|
||
import capture_auto_decide_recommendation_scope from './fixtures/auto-decide-recommendation-361c.json';
|
||
import captured_auto_decide_saved_ai from './fixtures/auto-decide-saved-ai.json';
|
||
import retry_auto_decide_saved_ai from './fixtures/auto-decide-retry-ai.json';
|
||
import capture_auto_decide_structured from './fixtures/auto-decide-structured-77.json';
|
||
import completedModeCapture_auto_decide_structured from './fixtures/auto-decide-completed-mode-f359.json';
|
||
import capture_auto_decide_target_identity from './fixtures/auto-decide-target-361c.json';
|
||
import { bindAutoDecisionState } from './helpers/auto-decision-state';
|
||
import capture_auto_decision_state from './fixtures/auto-decide-state-cab3.json';
|
||
import { describe } from 'bun:test';
|
||
const captured = JSON.parse(fs.readFileSync(path.join(import.meta.dir,'fixtures/native-auto-decide-ag.json'),'utf8'));
|
||
const clone = (i=0) => structuredClone(captured.attempts[i]);
|
||
const verdict = (f=clone()) => findNativeAutoDecision(f.transcript,f.tools,f.options);
|
||
const ownedMessage = (f:any) => f.transcript.assistantMessages.find((m:any)=>m.sessionId===f.options.sessionId&&m.text.includes('Auto-decided'));
|
||
const literal = 'Auto-decided review mode → **HOLD SCOPE** (your preference). Change with /plan-tune.';
|
||
|
||
test('both exact native attempts preserve the asserted preference annotation despite terminal damage',()=>{
|
||
for(const [i,attempt] of captured.attempts.entries()){
|
||
const actual=verdict(attempt);expect(actual).not.toBeNull();expect(actual!.sessionId).toBe(attempt.options.sessionId);
|
||
expect(actual!.option).toBe('HOLD SCOPE');expect(actual!.annotation).toContain('(your preference). Change with /plan-tune.');
|
||
expect(ownedMessage(attempt).text).toContain(actual!.annotation);
|
||
if(i===1)expect(attempt.transcript.assistantMessages.some((m:any)=>m.sessionId!==attempt.options.sessionId&&m.text.includes('Auto-decided'))).toBe(true);
|
||
}
|
||
expect(classifyVisible('Auto-dcided review mode: HOLD SCOPE. DONE.')).toBeNull();
|
||
});
|
||
|
||
test('native annotation needs one successful post-command load in the owned session',()=>{
|
||
const mutations:Array<(f:any)=>void>=[
|
||
f=>{f.transcript.status='missing';},f=>{f.transcript.status='error';},f=>{f.options.sessionId='foreign';},
|
||
f=>{f.options.commandStartedAt=f.options.now;},f=>{f.options.commandStartedAt=NaN;},f=>{f.options.now=Infinity;},
|
||
f=>{f.options.now=f.options.commandStartedAt-1;},f=>{f.options.skillName='other-skill';},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');use.name='Read';},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');f.tools.push({...use});},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');f.tools=f.tools.filter((e:any)=>e.kind!=='result'||e.toolUseId!==use.toolUseId);},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');f.tools.find((e:any)=>e.kind==='result'&&e.toolUseId===use.toolUseId).isError=true;},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');const result=f.tools.find((e:any)=>e.kind==='result'&&e.toolUseId===use.toolUseId);f.tools.push({...result});},
|
||
f=>{const use=f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill');f.tools.find((e:any)=>e.kind==='result'&&e.toolUseId===use.toolUseId).timestamp=new Date(f.options.commandStartedAt-1).toISOString();},
|
||
f=>{ownedMessage(f).timestamp='invalid';},f=>{ownedMessage(f).timestamp=new Date(f.options.now+1).toISOString();},
|
||
f=>{ownedMessage(f).timestamp=new Date(f.options.commandStartedAt-1).toISOString();},
|
||
f=>{f.transcript.calls.push({sessionId:f.options.sessionId,toolUseId:'asked',questions:[{header:'Mode',question:'Choose mode?',options:[{label:'A'},{label:'B'}]}],answered:false});},
|
||
];
|
||
for(const mutate of mutations){const f=clone();mutate(f);expect(verdict(f)).toBeNull();}
|
||
const f=clone();f.tools.find((e:any)=>e.kind==='use'&&e.name==='Skill').input.skill='gstack:plan-ceo-review';expect(verdict(f)).not.toBeNull();
|
||
});
|
||
|
||
test('quotes, source introductions, conditions and incomplete template prose are not annotations',()=>{
|
||
for(const text of [
|
||
`> ${literal}`,` ${literal}`,`\t${literal}`,`"${literal}"`,`\`\`\`text\n${literal}\n\`\`\``,
|
||
`Example:\n\n${literal}`,`An unproven hypothesis.\n\n${literal}`,`Previous transcript:\n\n${literal}`,
|
||
`If approved, ${literal}`,literal.replace('(your preference)','(if you approve)'),literal.replace('Auto-decided','Auto-dcided'),
|
||
literal.replace('Change with /plan-tune.',''),`**Review mode: EXPANSION.**\n\n${literal}`,
|
||
`Example:\n\n**Review mode: HOLD SCOPE.**\n\n${literal}`,
|
||
'Auto-decided <summary> → <option> (your preference). Change with /plan-tune.',
|
||
'Auto-decided review mode → <selected option> (your preference). Change with /plan-tune.',
|
||
`**Review mode: HOLD SCOPE.**\n\n ${literal}`,`**Review mode: HOLD SCOPE.**\n\n\t${literal}`,
|
||
]){const f=clone();ownedMessage(f).text=text;expect(verdict(f),text).toBeNull();}
|
||
const f=clone(1);ownedMessage(f).text=ownedMessage(f).text.replace('Heads-up from gstack: this branch has unshipped work.', 'Heads-up from gstack: the following is a hypothetical example.');expect(verdict(f)).toBeNull();
|
||
});
|
||
|
||
test('same-session current retractions override an earlier annotation without borrowing quoted examples',()=>{
|
||
for(const text of [
|
||
'Correction: I withdraw this auto-decision.',
|
||
'The annotation is only an example.',
|
||
'This decision is withdrawn.',
|
||
'Correction: "I retract this decision."',
|
||
'I did not auto-decide the review mode.',
|
||
'Correction: that statement was only an example.',
|
||
'I did not make this choice.',
|
||
'**Review mode: SCOPE EXPANSION.**',
|
||
]){
|
||
const f=clone();ownedMessage(f).text+='\n\n'+text;expect(verdict(f),text).toBeNull();
|
||
const g=clone();g.options.now++;g.transcript.assistantMessages.push({sessionId:g.options.sessionId,timestamp:new Date(g.options.now).toISOString(),text});expect(verdict(g),text).toBeNull();
|
||
}
|
||
for(const text of ['> I withdraw this decision.','Quoted example: "I withdraw this decision."','```text\nI withdraw this decision.\n```','Example: "I did not make this choice."','Example: "that statement was only an example."']){
|
||
const f=clone();ownedMessage(f).text+='\n\n'+text;expect(verdict(f),text).not.toBeNull();
|
||
}
|
||
const f=clone();f.transcript.assistantMessages.push({sessionId:'foreign',timestamp:new Date(f.options.now).toISOString(),text:'I withdraw this decision.'});expect(verdict(f)).not.toBeNull();
|
||
});
|
||
|
||
test('the native reader cannot promote foreign cwd, child, user or tool-result text to an annotation',()=>{
|
||
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'auto-native-'));
|
||
try {
|
||
const config=path.join(dir,'config'),journals=path.join(config,'projects','fixture');fs.mkdirSync(journals,{recursive:true});
|
||
const start=Date.now()-100,session='owned';
|
||
const make=()=>[
|
||
{type:'assistant',isSidechain:false,cwd:dir,sessionId:session,timestamp:new Date(start+1).toISOString(),message:{role:'assistant',content:[{type:'tool_use',id:'load',name:'Skill',input:{skill:'plan-ceo-review'}}]}},
|
||
{type:'user',isSidechain:false,cwd:dir,sessionId:session,timestamp:new Date(start+2).toISOString(),message:{role:'user',content:[{type:'tool_result',tool_use_id:'load',content:'loaded',is_error:false}]}},
|
||
{type:'assistant',isSidechain:false,cwd:dir,sessionId:session,timestamp:new Date(start+3).toISOString(),message:{role:'assistant',content:[{type:'text',text:literal}]}},
|
||
];
|
||
const cases:Array<[(rows:any[])=>void,boolean]>=[
|
||
[()=>{},true],[rows=>{for(const r of rows)r.cwd=dir+'-foreign';},false],
|
||
[rows=>{rows[2].isSidechain=true;},false],[rows=>{rows[2].message.role='user';},false],
|
||
[rows=>{rows[2].message.content=[{type:'tool_result',tool_use_id:'other',content:literal}];},false],
|
||
];
|
||
for(const [mutate,expected]of cases){const rows=make();mutate(rows);fs.writeFileSync(path.join(journals,session+'.jsonl'),rows.map(r=>JSON.stringify(r)).join('\n')+'\n');
|
||
const tools:NativePublicToolEvent[]=[];const transcript=readPlanCountTranscript(config,dir,e=>tools.push(e));
|
||
expect(Boolean(findNativeAutoDecision(transcript,tools,{sessionId:session,skillName:'plan-ceo-review',commandStartedAt:start,now:Date.now()}))).toBe(expected);
|
||
}
|
||
}finally{fs.rmSync(dir,{recursive:true,force:true});}
|
||
});
|
||
describe('auto-decide-current-declaration', () => {
|
||
const capture = capture_auto_decide_current_declaration;
|
||
const clone = () => structuredClone(capture.retry) as any;
|
||
const decide = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
const message = (f: any) => f.transcript.assistantMessages.find((m: any) =>
|
||
m.timestamp === '2026-09-16T23:23:28.931Z');
|
||
const use = (f: any) => f.tools.find((e: any) => e.kind === 'use' &&
|
||
e.input?.command?.includes('gstack-skill-start'));
|
||
const ack = (f: any) => f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === use(f).toolUseId);
|
||
|
||
test('actual current Decision declaration completes the retained owned retry', () => {
|
||
const f = clone();
|
||
const result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.annotation).toBe(message(f).text);
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
expect(result?.preambleToolUseId).toBe(use(f).toolUseId);
|
||
expect(result?.skillToolUseId).toBeUndefined();
|
||
expect(result?.questionLogToolUseId).toBeUndefined();
|
||
});
|
||
|
||
test('literal first declaration form is supported by the authenticated retry context', () => {
|
||
const f = clone();
|
||
// This checks representation only. The first attempt's state was deleted;
|
||
// transplanting its text grants no first-attempt ownership or verdict credit.
|
||
message(f).text = capture.firstDeclaration.text;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
delete f.options.stateEvidence;
|
||
f.tools = [];
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
const modes = ['HOLD SCOPE', 'SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'SCOPE REDUCTION'];
|
||
for (const mode of modes) {
|
||
for (const label of ['Decision', 'Decision: review mode is', 'Mode']) {
|
||
for (const target of ['for this draft.', 'for the current review (saved preference).']) {
|
||
const text = `${label}${label.includes(':') ? '' : ':'} ${mode} ${target}`;
|
||
test(`one current full mode owns its target clause: ${text}`, () => {
|
||
const f = clone();
|
||
Object.assign(f.options.stateEvidence.records[0], { user_choice: mode, recommended: mode });
|
||
message(f).text = text;
|
||
expect(decide(f)?.option).toBe(mode);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const invalidDeclarations = [
|
||
'Decision: HOLD SCOPELESS for this draft.',
|
||
'Decision: HOLD for this draft.',
|
||
'Decision: SCOPE for this draft.',
|
||
'Decision: SELECTIVE for this draft.',
|
||
'Decision: review mode is CUSTOM MODE for this draft.',
|
||
'Decision: HOLD SCOPE?',
|
||
'Decision: HOLD SCOPE for',
|
||
'Decision: HOLD SCOPE for (',
|
||
'Decision: HOLD SCOPE for this draft (unfinished.',
|
||
'Decision: HOLD SCOPE for this draft (unbalanced)).',
|
||
'Decision: HOLD SCOPE for this draft or SCOPE EXPANSION.',
|
||
'Decision: HOLD SCOPE for this draft. Instead choose SCOPE REDUCTION.',
|
||
'Decision: HOLD SCOPE for this draft; SELECTIVE_EXPANSION.',
|
||
'Decision: HOLD SCOPE for this draft, if approved.',
|
||
'Decision: HOLD SCOPE for this draft, pending approval.',
|
||
'Decision: HOLD SCOPE for this draft, not yet selected.',
|
||
'Decision: HOLD SCOPE for this draft, withdrawn.',
|
||
'Decision: HOLD SCOPE for this draft; the selected mode is not HOLD SCOPE.',
|
||
'Decision: HOLD SCOPE for plan-eng-review.',
|
||
'Decision: HOLD SCOPE for another draft.',
|
||
'Decision: HOLD SCOPE for a future review.',
|
||
'Decision: HOLD SCOPE for this future review.',
|
||
'Decision pending: HOLD SCOPE for this draft.',
|
||
'Decision: review mode is not selected.',
|
||
'Decision: not HOLD SCOPE for this draft.',
|
||
];
|
||
for (const text of invalidDeclarations) {
|
||
test(`unsupported current declaration cannot complete a decision: ${text}`, () => {
|
||
const f = clone(); message(f).text = text;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`unsupported current declaration retracts the earlier decision: ${text}`, () => {
|
||
const f = clone(); message(f).text += `\n\nCorrection: ${text}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const prefix of ['> ', ' ', '"', '`']) {
|
||
test(`quoted current-mode syntax does not declare or retract: ${JSON.stringify(prefix)}`, () => {
|
||
const f = clone();
|
||
const quote = (value: string) => prefix + value + (['"', '`'].includes(prefix) ? prefix : '');
|
||
message(f).text = quote('Decision: HOLD SCOPE for this draft.');
|
||
expect(decide(f)).toBeNull();
|
||
message(f).text = clone().transcript.assistantMessages.at(-1).text + '\n\n' +
|
||
quote('Decision: SCOPE EXPANSION for this draft.');
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
}
|
||
for (const text of [
|
||
'Example:\nDecision: HOLD SCOPE for this draft.',
|
||
'Historical transcript:\nDecision: HOLD SCOPE for this draft.',
|
||
'Previous decision:\nDecision: HOLD SCOPE for this draft.',
|
||
'```text\nDecision: HOLD SCOPE for this draft.\n```',
|
||
'If approved, Decision: HOLD SCOPE for this draft.',
|
||
'Not a decision: HOLD SCOPE for this draft.',
|
||
]) test(`unasserted declaration provides no mode: ${JSON.stringify(text)}`, () => {
|
||
const f = clone(); message(f).text = text;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
for (const label of ['Decision', 'Decision: review mode is', 'Mode']) {
|
||
test(`later matching current declaration retains the owned mode: ${label}`, () => {
|
||
const f = clone(); message(f).text += `\n\nUpdate: ${label}${label.includes(':') ? '' : ':'} HOLD SCOPE for this draft.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`later conflicting current declaration retracts the owned mode: ${label}`, () => {
|
||
const f = clone(); message(f).text += `\n\nUpdate: ${label}${label.includes(':') ? '' : ':'} SCOPE EXPANSION for this draft.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
test('a separately scoped non-mode decision does not retract the review mode', () => {
|
||
const f = clone(); message(f).text += '\n\nDecision: publish the audit log.';
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
test('the completed native preference and log ACK retain their independent authority', () => {
|
||
const f = clone(); delete f.options.stateEvidence;
|
||
const result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.stateRecord).toBeUndefined();
|
||
expect(result?.preferenceToolUseId).toBeDefined();
|
||
expect(result?.questionLogToolUseId).toBeDefined();
|
||
});
|
||
|
||
test('target-clause capitalization and a negative non-mode explanation remain valid', () => {
|
||
const f = clone(); message(f).text = 'Decision: HOLD SCOPE For this draft, not for implementation.';
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
test('a named target must match the complete audit target, including dotted identifiers', () => {
|
||
const f = clone();
|
||
f.options.stateEvidence.records[0].question_summary = 'Select review mode for PLAN.md';
|
||
message(f).text = 'Decision: HOLD SCOPE for PLAN.md.';
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
message(f).text = 'Decision: HOLD SCOPE for PLAN.other.';
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
for (const target of ['a future review', 'the previous review', 'another draft', 'the next invocation', 'an example plan']) {
|
||
test(`even a matching audit cannot make an explicitly noncurrent target current: ${target}`, () => {
|
||
const f = clone();
|
||
f.options.stateEvidence.records[0].question_summary = `Select review mode for ${target}`;
|
||
message(f).text = `Decision: HOLD SCOPE for ${target}.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
for (const target of ['future.md', 'previous-review.md', 'another.plan.md']) {
|
||
test(`owned literal filename remains a current target: ${target}`, () => {
|
||
const f = clone();
|
||
f.options.stateEvidence.records[0].question_summary = `Select review mode for ${target}`;
|
||
message(f).text = `Decision: HOLD SCOPE for ${target}.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
}
|
||
|
||
for (const [name, mutate] of Object.entries({
|
||
'missing state and native log ACK': (f: any) => {
|
||
delete f.options.stateEvidence;
|
||
const log = f.tools.find((e: any) => e.kind === 'use' && e.input?.command?.includes('gstack-question-log'));
|
||
f.tools = f.tools.filter((e: any) => e.kind !== 'result' || e.toolUseId !== log.toolUseId);
|
||
},
|
||
'empty owned log': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'duplicate owned log': (f: any) => { f.options.stateEvidence.records.push({ ...f.options.stateEvidence.records[0] }); },
|
||
'wrong preference and native check': (f: any) => {
|
||
f.options.stateEvidence.preference = 'always-ask';
|
||
const check = f.tools.find((e: any) => e.kind === 'use' && e.input?.command?.includes('gstack-question-preference'));
|
||
f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === check.toolUseId).content = 'ASK\nEXIT: 0';
|
||
},
|
||
'foreign log session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'foreign log skill': (f: any) => { f.options.stateEvidence.records[0].skill = 'plan-eng-review'; },
|
||
'different logged choice': (f: any) => { f.options.stateEvidence.records[0].user_choice = 'SCOPE EXPANSION'; },
|
||
'different recommendation': (f: any) => { f.options.stateEvidence.records[0].recommended = 'SCOPE EXPANSION'; },
|
||
'nonautomatic log': (f: any) => { f.options.stateEvidence.records[0].auto_decided = false; },
|
||
'foreign native session': (f: any) => { f.options.sessionId = 'foreign'; },
|
||
'failed preamble': (f: any) => { ack(f).isError = true; },
|
||
'missing preamble ACK': (f: any) => { f.tools = f.tools.filter((e: any) => e !== ack(f)); },
|
||
'duplicate preamble ACK': (f: any) => { f.tools.push({ ...ack(f) }); },
|
||
'disabled tuning': (f: any) => { ack(f).content = ack(f).content.replace('QUESTION_TUNING: true', 'QUESTION_TUNING: false'); },
|
||
'old log': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.commandStartedAt - 1).toISOString(); },
|
||
'future log': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.now + 1).toISOString(); },
|
||
'public decision before log': (f: any) => { message(f).timestamp = new Date(Date.parse(f.options.stateEvidence.records[0].ts) - 1).toISOString(); },
|
||
'native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId }); },
|
||
'prose question': (f: any) => { f.options.proseQuestionObserved = true; },
|
||
'current withdrawal': (f: any) => { message(f).text += '\n\nI withdraw this decision.'; },
|
||
})) test(`current Decision syntax retains ${name} rejection`, () => {
|
||
const f = clone(); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('auto-decide-explanatory-mode', () => {
|
||
const capture = capture_auto_decide_explanatory_mode;
|
||
const captured749 = captured749_auto_decide_explanatory_mode;
|
||
const annotations = annotations_auto_decide_explanatory_mode;
|
||
const clone = () => structuredClone(capture) as any;
|
||
const declaration = (f: any) => f.transcript.assistantMessages.find((m: any) =>
|
||
m.text.startsWith('**Review mode: HOLD SCOPE** —'));
|
||
const decide = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
|
||
function witnessed() {
|
||
const f = clone();
|
||
const use = f.tools.find((e: any) => e.input?.command?.includes('gstack-question-log'));
|
||
// Synthetic owned-file witness from the exact literal request. The original
|
||
// file was not retained; its failed paid attempt remains failed.
|
||
const record = JSON.parse(/gstack-question-log '(\{[^\n]*\})'/.exec(use.input.command)![1]!);
|
||
record.source = 'agent';
|
||
record.ts = f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === use.toolUseId).timestamp;
|
||
f.options.stateEvidence = { questionId: 'plan-ceo-review-mode', preference: 'never-ask', records: [record] };
|
||
return f;
|
||
}
|
||
|
||
test('original public events alone cannot authenticate the unretained owned append', () => {
|
||
expect(decide(clone())).toBeNull();
|
||
});
|
||
|
||
test('exact completed announcement agrees with an authenticated owned append', () => {
|
||
const f = witnessed();
|
||
const result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.annotation).toBe(declaration(f).text);
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
expect(result?.questionLogToolUseId).toBeUndefined();
|
||
});
|
||
|
||
const separators = ['. ', ', ', '; ', ': ', ' — ', ' – ', ' - '];
|
||
for (const separator of separators) {
|
||
test(`complete mode with separated explanation ${JSON.stringify(separator)}`, () => {
|
||
const f = witnessed();
|
||
declaration(f).text = `Review mode: HOLD SCOPE${separator}selected from the saved preference.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`same later completed mode retains its explanation ${JSON.stringify(separator)}`, () => {
|
||
const f = witnessed();
|
||
declaration(f).text += `\n\nMode decision completed: HOLD SCOPE${separator}selected from the saved preference.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`different later completed mode withdraws the choice ${JSON.stringify(separator)}`, () => {
|
||
const f = witnessed();
|
||
declaration(f).text += `\n\nCorrection: Mode: SCOPE EXPANSION${separator}selected from the saved preference.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`conditional explanation never completes the mode ${JSON.stringify(separator)}`, () => {
|
||
const f = witnessed();
|
||
declaration(f).text = `Review mode: HOLD SCOPE${separator}if approved.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`later conditional explanation withdraws the choice ${JSON.stringify(separator)}`, () => {
|
||
const f = witnessed();
|
||
declaration(f).text += `\n\nMode: HOLD SCOPE${separator}pending approval.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const value of ['HOLD SCOPELESS', 'HOLD SCOPE SCOPE EXPANSION', 'HOLD SCOPE / SCOPE EXPANSION',
|
||
'HOLD SCOPE?', 'HOLD SCOPE selected from my preference', 'HOLD SCOPE—if approved', 'HOLD SCOPE - ']) {
|
||
test(`incomplete or ambiguous mode is not a declaration: ${value}`, () => {
|
||
const f = witnessed(); declaration(f).text = `Mode: ${value}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`incomplete current field retracts a previous mode: ${value}`, () => {
|
||
const f = witnessed(); declaration(f).text += `\n\nMode: ${value}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const [name, mutate] of Object.entries({
|
||
'missing append': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'foreign session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'different logged mode': (f: any) => { f.options.stateEvidence.records[0].user_choice = 'SCOPE EXPANSION'; },
|
||
'native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId }); },
|
||
'unfinished declaration': (f: any) => { declaration(f).text = 'Mode decision pending: HOLD SCOPE — saved preference.'; },
|
||
'withdrawn decision': (f: any) => { declaration(f).text += '\n\nI withdraw this decision.'; },
|
||
'quoted declaration': (f: any) => { declaration(f).text = '> Mode: HOLD SCOPE — saved preference.'; },
|
||
'example declaration': (f: any) => { declaration(f).text = 'Example:\nMode: HOLD SCOPE — saved preference.'; },
|
||
})) test(`explanatory announcement still rejects ${name}`, () => {
|
||
const f = witnessed(); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
test('quoted historical correction does not withdraw the current completed mode', () => {
|
||
const f = witnessed();
|
||
declaration(f).text += '\n\n> Mode: SCOPE EXPANSION — a historical example.';
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
test('generic Skill annotations retain their existing non-CEO mode vocabulary', () => {
|
||
const f = structuredClone(annotations.attempts[0]) as any;
|
||
f.options.skillName = 'office-hours';
|
||
f.tools.find((e: any) => e.kind === 'use' && e.name === 'Skill').input.skill = 'office-hours';
|
||
const message = f.transcript.assistantMessages.find((m: any) => m.text.startsWith('Auto-decided'));
|
||
message.text = 'Auto-decided workflow → **Builder** (your preference). Change with /plan-tune.\n\nMode: Builder (saved preference).';
|
||
expect(decide(f)?.option).toBe('Builder');
|
||
message.text += '\n\nMode: Startup (saved preference).';
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
test('retained retry messages alone cannot authenticate missing tool and file evidence', () => {
|
||
const retry = capture.retryObservation;
|
||
expect(findNativeAutoDecision(retry.transcript, [], retry.options)).toBeNull();
|
||
});
|
||
|
||
test('exact retry prose accepts the optional decision label in an owned context', () => {
|
||
const f = witnessed();
|
||
// Only the text is replayed. Session/time and owned witness belong to the
|
||
// first fixture; this is not a reconstruction or promotion of the retry.
|
||
declaration(f).text = capture.retryObservation.transcript.assistantMessages.find(m =>
|
||
m.text.startsWith('**Mode decision:'))!.text;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
for (const field of ['Mode', 'Mode decision', 'Review mode', 'Review mode decision']) {
|
||
test(`a completed field does not require a separate status word: ${field}`, () => {
|
||
const f = witnessed(); declaration(f).text = `${field}: HOLD SCOPE (saved preference).`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`a later matching field does not withdraw its choice: ${field}`, () => {
|
||
const f = witnessed(); declaration(f).text += `\n\n${field}: HOLD SCOPE (saved preference).`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`a conflicting later field still withdraws its choice: ${field}`, () => {
|
||
const f = witnessed(); declaration(f).text += `\n\n${field}: SCOPE EXPANSION (saved preference).`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
const clone749 = () => structuredClone(captured749) as any;
|
||
const declaration749 = (f: any) => f.transcript.assistantMessages.find((m: any) =>
|
||
m.timestamp === '2026-09-16T12:13:02.513Z');
|
||
|
||
test('actual 749 public declaration agrees with its retained owned append', () => {
|
||
// Exact public tools, final declaration and owned log were captured while the
|
||
// paid observer was still waiting. This free replay does not promote that run.
|
||
const f = clone749();
|
||
const result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.annotation).toBe(declaration749(f).text);
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
});
|
||
|
||
const explanatoryTails = [
|
||
' (saved preference; no prompt required). The review remains paused.',
|
||
' (saved preference (confirmed for this project); no prompt required). The review remains paused.',
|
||
' (saved preference), recorded for this invocation.',
|
||
' (saved preference): recorded for this invocation.',
|
||
' (saved preference) — recorded for this invocation.',
|
||
' (saved preference)\nThe review remains paused.',
|
||
'. Selected from the saved preference (recorded).',
|
||
'; selected from the saved preference (recorded).',
|
||
];
|
||
for (const tail of explanatoryTails) {
|
||
test(`balanced explanation with following prose is a complete declaration: ${JSON.stringify(tail)}`, () => {
|
||
const f = clone749(); declaration749(f).text = `Mode decision: HOLD SCOPE${tail}`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`matching later explanation preserves the current decision: ${JSON.stringify(tail)}`, () => {
|
||
const f = clone749(); declaration749(f).text += `\n\nMode: HOLD SCOPE${tail}`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
test(`conflicting later explanation withdraws the current decision: ${JSON.stringify(tail)}`, () => {
|
||
const f = clone749(); declaration749(f).text += `\n\nMode: SCOPE EXPANSION${tail}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
const incompleteFields = [
|
||
'Mode: HOLD SCOPE (saved preference; recorded.',
|
||
'Mode: HOLD SCOPE (saved preference (recorded).',
|
||
'Mode: HOLD SCOPE (saved preference)). Recorded.',
|
||
'Mode: HOLD SCOPE (saved preference)SCOPE EXPANSION',
|
||
'Mode: HOLD SCOPE. A following explanation (unfinished.',
|
||
'Mode: HOLD SCOPE (saved preference). If approved.',
|
||
'Mode: HOLD SCOPE (saved preference (if approved)). Recorded.',
|
||
'Mode: HOLD SCOPE (saved preference). Not yet selected.',
|
||
'Mode: HOLD SCOPE (saved preference). I did not auto-decide the review mode.',
|
||
'Mode: HOLD SCOPE (saved preference). This decision is withdrawn.',
|
||
'Mode decision pending: HOLD SCOPE (saved preference). Recorded.',
|
||
'Mode decision tentative: HOLD SCOPE (saved preference). Recorded.',
|
||
'Mode: CUSTOM MODE (saved preference). Recorded.',
|
||
'Mode: HOLD SCOPE / SCOPE EXPANSION (saved preference). Recorded.',
|
||
'Mode: HOLD SCOPE (saved preference).\nMode decision pending: HOLD SCOPE',
|
||
];
|
||
for (const field of incompleteFields) {
|
||
test(`explanatory prose cannot complete an unsupported field: ${JSON.stringify(field)}`, () => {
|
||
const f = clone749(); declaration749(f).text = field;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`later unsupported field retracts the earlier decision: ${JSON.stringify(field)}`, () => {
|
||
const f = clone749(); declaration749(f).text += `\n\n${field}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const [name, mutate] of Object.entries({
|
||
'missing owned append': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'foreign owned session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'duplicate owned append': (f: any) => { f.options.stateEvidence.records.push({ ...f.options.stateEvidence.records[0] }); },
|
||
'conflicting logged choice': (f: any) => { f.options.stateEvidence.records[0].user_choice = 'SCOPE EXPANSION'; },
|
||
'failed preamble': (f: any) => {
|
||
const preamble = f.tools.find((e: any) => e.kind === 'use' && e.input?.command?.includes('gstack-skill-start'));
|
||
f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === preamble.toolUseId).isError = true;
|
||
},
|
||
'native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId }); },
|
||
'declaration before owned append': (f: any) => { declaration749(f).timestamp = '2026-09-16T12:12:00.000Z'; },
|
||
'quoted declaration': (f: any) => { declaration749(f).text = '> Mode: HOLD SCOPE (saved preference). Recorded.'; },
|
||
'example declaration': (f: any) => { declaration749(f).text = 'Example:\nMode: HOLD SCOPE (saved preference). Recorded.'; },
|
||
})) test(`captured explanatory mode still requires ${name}`, () => {
|
||
const f = clone749(); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
const reviewModes = ['HOLD SCOPE', 'SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'SCOPE REDUCTION'];
|
||
for (const mode of reviewModes) {
|
||
for (const tail of [' (saved preference). Recorded for this invocation.',
|
||
' (saved preference (confirmed); recorded). No further mode decision.',
|
||
` (saved preference). ${mode} is recorded for this invocation.`]) {
|
||
test(`each complete review mode supports an unambiguous explanatory suffix: ${mode}${tail}`, () => {
|
||
const f = clone749();
|
||
Object.assign(f.options.stateEvidence.records[0], { user_choice: mode, recommended: mode });
|
||
declaration749(f).text = `Mode: ${mode}${tail}`;
|
||
expect(decide(f)?.option).toBe(mode);
|
||
});
|
||
}
|
||
for (const other of reviewModes.filter(value => value !== mode)) {
|
||
for (const connector of [' or ', ' versus ', ' vs. ', ' / ', ' | ', '; or ', ', choose ',
|
||
'. Alternatively, select ', ' — instead choose ', ' (otherwise choose ', ' rather than ']) {
|
||
test(`a second distinct mode in the suffix stays ambiguous: ${mode}${connector}${other}`, () => {
|
||
const f = clone749();
|
||
Object.assign(f.options.stateEvidence.records[0], { user_choice: mode, recommended: mode });
|
||
const tail = connector.startsWith(' (') ? ')' : '';
|
||
declaration749(f).text = `Mode: ${mode} (saved preference)${connector}${other}${tail}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
test('alternate current mode spellings remain ambiguous after an explanatory parenthetical', () => {
|
||
for (const alternative of ['scope expansion', 'SCOPE_EXPANSION', 'SCOPE EXPANSION']) {
|
||
const f = clone749(); declaration749(f).text = `Mode: HOLD SCOPE (saved preference); ${alternative}`;
|
||
expect(decide(f)).toBeNull();
|
||
}
|
||
});
|
||
|
||
test('generic annotation vocabulary retains its original parenthetical boundaries', () => {
|
||
for (const suffix of ['', ' or Startup', '; or Startup', ' versus Startup', '. Recorded for this invocation.']) {
|
||
const f = structuredClone(annotations.attempts[0]) as any;
|
||
f.options.skillName = 'office-hours';
|
||
f.tools.find((e: any) => e.kind === 'use' && e.name === 'Skill').input.skill = 'office-hours';
|
||
const message = f.transcript.assistantMessages.find((m: any) => m.text.startsWith('Auto-decided'));
|
||
message.text = `Auto-decided workflow → **Builder** (your preference). Change with /plan-tune.\n\nMode: Builder (saved preference)${suffix}`;
|
||
expect(decide(f)?.option ?? null).toBe(suffix ? null : 'Builder');
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('auto-decide-recommendation-scope', () => {
|
||
const capture = capture_auto_decide_recommendation_scope;
|
||
const clone = () => structuredClone(capture) as any;
|
||
const decide = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
const message = (f: any) => f.transcript.assistantMessages.find((m: any) =>
|
||
m.timestamp === '2026-09-17T02:23:11.495Z');
|
||
|
||
test('actual completed mode and recommendation commentary match the retained owned audit', () => {
|
||
const f = clone(), result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.annotation).toBe(message(f).text);
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
expect(result?.preambleToolUseId).toBe('toolu_01Ni4b9NZeiexmcAz1jRTUa4');
|
||
});
|
||
|
||
const modes = ['HOLD SCOPE', 'SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'SCOPE REDUCTION'];
|
||
for (const mode of modes) for (const commentary of [
|
||
'recommendation would have been the same',
|
||
'my recommendation might differ without the saved preference',
|
||
`the recommendation would still be ${mode}`,
|
||
'our recommendation will remain unchanged',
|
||
'recommendation stays the same unless the product context changes',
|
||
]) test(`completed ${mode} is separate from ${commentary}`, () => {
|
||
const f = clone();
|
||
Object.assign(f.options.stateEvidence.records[0], { user_choice: mode, recommended: mode });
|
||
message(f).text = `Decision: review mode is ${mode} (${commentary}).`;
|
||
expect(decide(f)?.option).toBe(mode);
|
||
});
|
||
|
||
for (const separator of ['; ', ', ', '. ', ' — ', ' – ', ' - ', ' (']) {
|
||
test(`recommendation assertion has an explicit boundary: ${JSON.stringify(separator)}`, () => {
|
||
const f = clone();
|
||
message(f).text = `Mode: HOLD SCOPE${separator}recommendation would have been unchanged${separator === ' (' ? ')' : ''}.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
}
|
||
|
||
const uncertain = [
|
||
'Mode: would choose HOLD SCOPE.',
|
||
'Mode: HOLD SCOPE if approved.',
|
||
'Mode: HOLD SCOPE unless you object.',
|
||
'Mode: HOLD SCOPE (I will make this selection).',
|
||
'Mode: HOLD SCOPE (this choice might change).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same; if approved).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same, unless you object).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same and I will select it later).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same but the choice might change).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same while we would still need approval).',
|
||
'Mode: HOLD SCOPE (recommendation would be the same; selection is pending).',
|
||
'Mode: HOLD SCOPE (recommendation says the decision would be conditional).',
|
||
'Mode: HOLD SCOPE (recommendation would still be SCOPE EXPANSION).',
|
||
'Mode: HOLD SCOPE (recommendation would be unchanged). Not yet selected.',
|
||
'Mode: HOLD SCOPE (recommendation would be unchanged). This decision is withdrawn.',
|
||
'Mode pending: HOLD SCOPE (recommendation would be unchanged).',
|
||
'Mode: not HOLD SCOPE (recommendation would be unchanged).',
|
||
'Mode: HOLD SCOPE for a future review (recommendation would be unchanged).',
|
||
'Mode: HOLD SCOPE for another draft (recommendation would be unchanged).',
|
||
'Mode: HOLD SCOPELESS (recommendation would be unchanged).',
|
||
'Mode: HOLD SCOPE (recommendation would be unchanged.',
|
||
];
|
||
for (const text of uncertain) {
|
||
test(`commentary does not authenticate an uncertain choice: ${text}`, () => {
|
||
const f = clone(); message(f).text = text;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`later uncertain choice retracts the original completed decision: ${text}`, () => {
|
||
const f = clone(); message(f).text += `\n\nCorrection: ${text}`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const [name, wrap] of [
|
||
['quoted', (s: string) => `> ${s}`],
|
||
['indented', (s: string) => ` ${s}`],
|
||
['fenced', (s: string) => `\`\`\`text\n${s}\n\`\`\``],
|
||
['historical', (s: string) => `Previous decision:\n${s}`],
|
||
['example', (s: string) => `Example:\n${s}`],
|
||
] as const) test(`recommendation commentary cannot authenticate ${name} declarations`, () => {
|
||
const f = clone(); message(f).text = wrap('Mode: HOLD SCOPE (recommendation would be unchanged).');
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
for (const [name, mutate] of Object.entries({
|
||
'missing owned log': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'duplicate owned log': (f: any) => { f.options.stateEvidence.records.push({ ...f.options.stateEvidence.records[0] }); },
|
||
'foreign audit session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'different selected choice': (f: any) => { f.options.stateEvidence.records[0].user_choice = 'SCOPE EXPANSION'; },
|
||
'different recommendation': (f: any) => { f.options.stateEvidence.records[0].recommended = 'SCOPE EXPANSION'; },
|
||
'nonautomatic record': (f: any) => { f.options.stateEvidence.records[0].auto_decided = false; },
|
||
'foreign native session': (f: any) => { f.options.sessionId = 'foreign'; },
|
||
'missing successful preamble': (f: any) => { f.tools = f.tools.filter((e: any) => e.toolUseId !== 'toolu_01Ni4b9NZeiexmcAz1jRTUa4'); },
|
||
'future audit': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.now + 1).toISOString(); },
|
||
'decision before completed log': (f: any) => { message(f).timestamp = new Date(Date.parse(f.options.stateEvidence.records[0].ts) - 1).toISOString(); },
|
||
'surfaced native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId }); },
|
||
'surfaced prose question': (f: any) => { f.options.proseQuestionObserved = true; },
|
||
})) test(`actual recommendation commentary retains ${name} rejection`, () => {
|
||
const f = clone(); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('auto-decide-saved-ai', () => {
|
||
const captured = captured_auto_decide_saved_ai;
|
||
const retry = retry_auto_decide_saved_ai;
|
||
const clone=()=>structuredClone(captured) as any;
|
||
const decision=(f=clone())=>findNativeAutoDecision(f.transcript,f.tools,f.options);
|
||
const message=(f:any)=>f.transcript.assistantMessages.find((m:any)=>m.text.includes('Auto-decided'));
|
||
|
||
test('actual saved mode preference annotation is a completed native auto-decision',()=>{
|
||
const f=clone(), result=decision(f);
|
||
expect(result).not.toBeNull();
|
||
expect(result!.option).toBe('HOLD SCOPE');
|
||
expect(message(f).text).toContain(result!.annotation);
|
||
expect(f.transcript.calls).toEqual([]);
|
||
});
|
||
|
||
test('saved preference is bound to this invoked skill and an agreeing current mode',()=>{
|
||
for(const change of [
|
||
(s:string)=>s.replace('`plan-ceo-review-mode`','`plan-design-review-mode`'),
|
||
(s:string)=>s.replace('`plan-ceo-review-mode`','`plan-ceo-review-routing`'),
|
||
(s:string)=>s.replace('**Review mode: HOLD SCOPE.**','**Review mode: SCOPE EXPANSION.**'),
|
||
(s:string)=>s.replace('**Review mode: HOLD SCOPE.**\n\n',''),
|
||
(s:string)=>s.replace('"Select review mode"','"Select report folder"'),
|
||
(s:string)=>s.replace('(your saved preference on','(a proposed preference on'),
|
||
(s:string)=>s.replace('Change with /plan-tune.',''),
|
||
(s:string)=>s.replace('Auto-decided','I will auto-decide'),
|
||
]) {const f=clone();message(f).text=change(message(f).text);expect(decision(f)).toBeNull();}
|
||
});
|
||
|
||
test('prefixed examples, quotations and hypothetical notices do not assert a current choice',()=>{
|
||
for(const change of [
|
||
(s:string)=>'Example:\n\n'+s,
|
||
(s:string)=>'```text\n'+s+'\n```',
|
||
(s:string)=>s.split('\n').map(l=>'> '+l).join('\n'),
|
||
(s:string)=>s.replace('Heads-up from the preamble: unshipped work on this branch','Heads-up from the preamble: a hypothetical example'),
|
||
(s:string)=>s.replace('Auto-decided "Select',' Auto-decided "Select'),
|
||
(s:string)=>s.replace('Auto-decided "Select','If approved, Auto-decided "Select'),
|
||
]) {const f=clone();message(f).text=change(message(f).text);expect(decision(f)).toBeNull();}
|
||
});
|
||
|
||
test('failed loads, foreign sessions, actual questions and later withdrawals retain precedence',()=>{
|
||
for(const mutate of [
|
||
(f:any)=>{f.options.sessionId='foreign';},
|
||
(f:any)=>{const use=f.tools.find((t:any)=>t.kind==='use'&&t.name==='Skill');f.tools.find((t:any)=>t.kind==='result'&&t.toolUseId===use.toolUseId).isError=true;},
|
||
(f:any)=>{f.transcript.calls.push({sessionId:f.options.sessionId,toolUseId:'actual-question'});},
|
||
(f:any)=>{f.options.now=Date.parse(message(f).timestamp)-1;},
|
||
(f:any)=>{message(f).text+='\n\nCorrection: I withdraw this decision.';},
|
||
(f:any)=>{message(f).text+='\n\n**Review mode: SCOPE EXPANSION.**';},
|
||
]) {const f=clone();mutate(f);expect(decision(f)).toBeNull();}
|
||
});
|
||
test('actual retry mode-decision heading retains its own annotation, excluding prior foreign text',()=>{
|
||
const f:any=structuredClone(retry), actual=decision(f);
|
||
expect(actual).not.toBeNull();expect(actual!.sessionId).toBe(f.options.sessionId);
|
||
expect(actual!.option).toBe('HOLD SCOPE');
|
||
expect(actual!.annotation).toContain('(your preference)');
|
||
const own=f.transcript.assistantMessages.filter((m:any)=>m.sessionId===f.options.sessionId);
|
||
f.transcript.assistantMessages=f.transcript.assistantMessages.filter((m:any)=>m.sessionId!==f.options.sessionId);
|
||
expect(decision(f)).toBeNull();expect(own.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('retry heading cannot supply a hypothetical, different decision, or withdrawn selection',()=>{
|
||
for(const change of [
|
||
(s:string)=>'Example:\n\n'+s,
|
||
(s:string)=>s.replace('Review mode for the deterministic','Review mode for the hypothetical'),
|
||
(s:string)=>s.replace('D1 — Review mode','D1 — Report destination'),
|
||
(s:string)=>s.replace('Auto-decided "Review mode:', 'Auto-decided "Report destination:'),
|
||
(s:string)=>s.replace('→ **HOLD SCOPE**','→ **Save a file**'),
|
||
(s:string)=>s+'\n\nCorrection: I withdraw this selection.',
|
||
(s:string)=>s+'\n\n**Review mode: SCOPE EXPANSION.**',
|
||
(s:string)=>s.replace('Heads-up from gstack: there is unshipped work on this branch','Heads-up from gstack: here is an example'),
|
||
]) {const f:any=structuredClone(retry),m=f.transcript.assistantMessages.find((m:any)=>m.sessionId===f.options.sessionId&&m.text.includes('Auto-decided'));m.text=change(m.text);expect(decision(f)).toBeNull();}
|
||
});
|
||
});
|
||
|
||
describe('auto-decide-structured', () => {
|
||
const capture = capture_auto_decide_structured;
|
||
const priorAnnotation = captured_auto_decide_saved_ai;
|
||
const completedModeCapture = completedModeCapture_auto_decide_structured;
|
||
const statusFixture = completedModeCapture_auto_decide_structured;
|
||
const clone = () => structuredClone(capture) as any;
|
||
const decision = (f = clone()) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
test('actual slash expansion with completed preference log and current mode is an auto-decision', () => {
|
||
const f = clone();
|
||
expect(f.tools.some((e: any) => e.name === 'Skill')).toBe(false);
|
||
expect(f.transcript.calls).toEqual([]);
|
||
const result = decision(f);
|
||
expect(result).not.toBeNull();
|
||
expect(result!.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
const use = (f: any, name: string) => f.tools.find((e: any) => e.kind === 'use' && e.input?.command?.includes(name));
|
||
const ack = (f: any, request: any) => f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === request.toolUseId);
|
||
const modeMessage = (f: any) => f.transcript.assistantMessages.find((m: any) => m.text.includes('**Mode:'));
|
||
const changeLog = (f: any, modify: (log: any) => void) => {
|
||
const request = use(f, 'gstack-question-log'), match = /'(\{.*\})'/.exec(request.input.command)!;
|
||
const value = JSON.parse(match[1]!); modify(value);
|
||
request.input.command = request.input.command.replace(match[1], JSON.stringify(value));
|
||
};
|
||
|
||
for (const [label, mutate] of Object.entries({
|
||
'missing transcript': (f: any) => { f.transcript.status = 'missing'; },
|
||
'foreign owned session': (f: any) => { f.options.sessionId = 'foreign'; },
|
||
'wrong invoked skill': (f: any) => { f.options.skillName = 'plan-eng-review'; },
|
||
'pre-command evidence': (f: any) => { f.options.commandStartedAt = Date.parse(modeMessage(f).timestamp); },
|
||
'future final statement': (f: any) => { f.options.now = Date.parse(modeMessage(f).timestamp) - 1; },
|
||
'invalid final timestamp': (f: any) => { modeMessage(f).timestamp = 'invalid'; },
|
||
'native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId, toolUseId: 'asked' }); },
|
||
'malformed native question tool': (f: any) => { f.tools.push({ ...use(f, 'gstack-question-log'), toolUseId: 'asked', name: 'mcp__ask__AskUserQuestion', input: {} }); },
|
||
'earlier visible prose question': (f: any) => { f.options.proseQuestionObserved = true; },
|
||
'public reply request': (f: any) => { modeMessage(f).text += '\nReply with A or B.'; },
|
||
'public option list': (f: any) => { modeMessage(f).text += '\nA) Hold scope\nB) Expand scope'; },
|
||
'no preamble': (f: any) => { const request = use(f, 'gstack-skill-start'); f.tools = f.tools.filter((e: any) => e.toolUseId !== request.toolUseId); },
|
||
'preamble failed': (f: any) => { ack(f, use(f, 'gstack-skill-start')).isError = true; },
|
||
'preamble missing ACK': (f: any) => { const request = use(f, 'gstack-skill-start'); f.tools = f.tools.filter((e: any) => e !== ack(f, request)); },
|
||
'preamble duplicate': (f: any) => { f.tools.push({ ...use(f, 'gstack-skill-start') }); },
|
||
'wrong preamble skill': (f: any) => { use(f, 'gstack-skill-start').input.command = use(f, 'gstack-skill-start').input.command.replace('--skill "plan-ceo-review"', '--skill "plan-eng-review"'); },
|
||
'question tuning disabled': (f: any) => { const result = ack(f, use(f, 'gstack-skill-start')); result.content = result.content.replace('QUESTION_TUNING: true', 'QUESTION_TUNING: false'); },
|
||
'ambiguous preamble session': (f: any) => { ack(f, use(f, 'gstack-skill-start')).content = 'SKILL_START_PROTO: 1\nQUESTION_TUNING: true\nSESSION_ID: duplicate\n' + ack(f, use(f, 'gstack-skill-start')).content; },
|
||
'nonzero preference': (f: any) => { ack(f, use(f, 'gstack-question-preference')).content = 'AUTO_DECIDE\nEXIT: 1'; },
|
||
'ASK preference': (f: any) => { ack(f, use(f, 'gstack-question-preference')).content = 'ASK\nEXIT: 0'; },
|
||
'preference error': (f: any) => { ack(f, use(f, 'gstack-question-preference')).isError = true; },
|
||
'wrong preference id': (f: any) => { use(f, 'gstack-question-preference').input.command = use(f, 'gstack-question-preference').input.command.replace('--check "plan-ceo-review-mode"', '--check "plan-ceo-review-other"'); },
|
||
'no preference check': (f: any) => { const request = use(f, 'gstack-question-preference'); f.tools = f.tools.filter((e: any) => e.toolUseId !== request.toolUseId); },
|
||
'unacknowledged log': (f: any) => { const request = use(f, 'gstack-question-log'); f.tools = f.tools.filter((e: any) => e !== ack(f, request)); },
|
||
'failed log': (f: any) => { ack(f, use(f, 'gstack-question-log')).isError = true; },
|
||
'fallback log result': (f: any) => { ack(f, use(f, 'gstack-question-log')).content = 'log unavailable (best-effort)'; },
|
||
'wrong log session': (f: any) => changeLog(f, log => { log.session_id = 'foreign'; }),
|
||
'wrong log skill': (f: any) => changeLog(f, log => { log.skill = 'plan-eng-review'; }),
|
||
'wrong log question id': (f: any) => changeLog(f, log => { log.question_id = 'plan-ceo-review-scope'; }),
|
||
'nonautomatic log': (f: any) => changeLog(f, log => { log.auto_decided = false; }),
|
||
'string automatic flag': (f: any) => changeLog(f, log => { log.auto_decided = 'true'; }),
|
||
'unmatched recommendation': (f: any) => changeLog(f, log => { log.recommended = 'SCOPE_EXPANSION'; }),
|
||
'different logged mode': (f: any) => changeLog(f, log => { log.recommended = log.user_choice = 'SCOPE_EXPANSION'; }),
|
||
'arbitrary logged value': (f: any) => changeLog(f, log => { log.recommended = log.user_choice = 'APPROVE_SCOPE'; }),
|
||
'nondecision summary': (f: any) => changeLog(f, log => { log.question_summary = ''; }),
|
||
'later checked preference': (f: any) => { ack(f, use(f, 'gstack-question-preference')).timestamp = modeMessage(f).timestamp; },
|
||
'mode before log ACK': (f: any) => { modeMessage(f).timestamp = use(f, 'gstack-question-log').timestamp; },
|
||
'reversed log ACK': (f: any) => { ack(f, use(f, 'gstack-question-log')).timestamp = use(f, 'gstack-question-preference').timestamp; },
|
||
'duplicate log ACK': (f: any) => { f.tools.push({ ...ack(f, use(f, 'gstack-question-log')) }); },
|
||
'foreign log ACK': (f: any) => { ack(f, use(f, 'gstack-question-log')).sessionId = 'foreign'; },
|
||
'missing current statement': (f: any) => { modeMessage(f).text = 'Done. Waiting for your next instruction.'; },
|
||
})) test(`structured current mode rejects ${label}`, () => {
|
||
const f = clone(); mutate(f); expect(decision(f)).toBeNull();
|
||
});
|
||
|
||
for (const name of ['gstack-skill-start', 'gstack-question-preference', 'gstack-question-log']) {
|
||
for (const [label, change] of Object.entries({
|
||
'echoed source': (s: string) => `echo '${s.replaceAll("'", "'\\''")}'`,
|
||
'conditional command': (s: string) => `false && ${s}`,
|
||
'commented source': (s: string) => `# ${s}`,
|
||
'extra prefix command': (s: string) => `true; ${s}`,
|
||
'extra suffix command': (s: string) => `${s}; true`,
|
||
'command substitution': (s: string) => `echo "$(${s})"`,
|
||
})) test(`${name} cannot authenticate ${label}`, () => {
|
||
const f = clone(); use(f, name).input.command = change(use(f, name).input.command); expect(decision(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const [label, text] of Object.entries({
|
||
'plain current field': 'Mode: HOLD SCOPE.',
|
||
'parenthetical explanation with punctuation': 'Mode: HOLD SCOPE (saved preference, confirmed).',
|
||
'parenthetical review explanation': '**Review mode: HOLD SCOPE (saved preference; confirmed).**',
|
||
'current review field': '**Review mode: HOLD SCOPE.**',
|
||
'compact completion': '**STATUS: DONE**\n\nMode: HOLD SCOPE',
|
||
'bullet conclusion': 'The requested routing decision is complete.\n\n- **Mode: HOLD SCOPE**, using the saved preference.\n\nThe substantive review is deferred.',
|
||
'quoted historical contradiction': 'Mode: HOLD SCOPE.\n\nEarlier example: "Review mode: SCOPE EXPANSION."',
|
||
})) test(`completed structured log supports ${label} without exact annotation prose`, () => {
|
||
const f = clone(); modeMessage(f).text = text;
|
||
const result = decision(f); expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.skillToolUseId).toBeUndefined();
|
||
expect(result?.preambleToolUseId).toBe(use(f, 'gstack-skill-start').toolUseId);
|
||
expect(result?.annotation).toBe(text);
|
||
});
|
||
|
||
for (const text of [
|
||
'> Mode: HOLD SCOPE.', ' Mode: HOLD SCOPE.', '`Mode: HOLD SCOPE.`',
|
||
'```text\nMode: HOLD SCOPE.\n```', 'Example:\n\nMode: HOLD SCOPE.',
|
||
'Previous transcript:\n\nMode: HOLD SCOPE.', 'If approved, Mode: HOLD SCOPE.',
|
||
'Mode: HOLD SCOPE, if you approve.', 'Mode: HOLD SCOPE, pending approval.',
|
||
'Mode: HOLD SCOPE?', 'Mode: HOLD SCOPELESS.',
|
||
'Mode: HOLD SCOPE (withdrawn).', 'Mode: HOLD SCOPE (retracted).',
|
||
'Mode: HOLD SCOPE.\n\nMode: HOLD SCOPE (pending approval).',
|
||
'Mode: HOLD SCOPE.\n\nCorrection: I withdraw this decision.',
|
||
'Mode: HOLD SCOPE.\n\nI did not auto-decide the review mode.',
|
||
'Mode: HOLD SCOPE.\n\nCorrection: Mode: SCOPE EXPANSION.',
|
||
'Mode: HOLD SCOPE.\n\nMode: SCOPE EXPANSION.',
|
||
]) test(`quoted, conditional or withdrawn mode has no completed choice: ${JSON.stringify(text)}`, () => {
|
||
const f = clone(); modeMessage(f).text = text; expect(decision(f)).toBeNull();
|
||
});
|
||
|
||
test('the same command contracts also support direct literal invocations and quiet ACKs', () => {
|
||
const f = clone();
|
||
use(f, 'gstack-skill-start').input.command = '"$HOME/.claude/skills/gstack/bin/gstack-skill-start" --model claude --skill plan-ceo-review --parent-pid "$PPID"';
|
||
use(f, 'gstack-question-preference').input.command = '~/.claude/skills/gstack/bin/gstack-question-preference --check plan-ceo-review-mode';
|
||
ack(f, use(f, 'gstack-question-preference')).content = 'AUTO_DECIDE\n';
|
||
use(f, 'gstack-question-log').input.command = use(f, 'gstack-question-log').input.command.split(' 2>/dev/null')[0];
|
||
ack(f, use(f, 'gstack-question-log')).content = '';
|
||
expect(decision(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
for (const status of ['undecided', 'not selected', 'pending approval', 'none']) {
|
||
test(`later Review mode: ${status} withdraws both existing annotation and structured decision`, () => {
|
||
const previous: any = structuredClone(priorAnnotation);
|
||
previous.transcript.assistantMessages.find((m: any) => m.text.includes('Auto-decided')).text += `\n\nReview mode: ${status}.`;
|
||
expect(findNativeAutoDecision(previous.transcript, previous.tools, previous.options)).toBeNull();
|
||
const f = clone(); modeMessage(f).text += `\n\nReview mode: ${status}.`;
|
||
expect(decision(f)).toBeNull();
|
||
});
|
||
test(`later Mode: ${status} withdraws a structured decision`, () => {
|
||
const f = clone(); modeMessage(f).text += `\n\n- **Mode: ${status}.**`;
|
||
expect(decision(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const name of ['gstack-question-preference', 'gstack-question-log']) test(`${name} cannot borrow an earlier success after a contradictory current call`, () => {
|
||
const f = clone(), request = structuredClone(use(f, name)), result = structuredClone(ack(f, request));
|
||
request.toolUseId += '-later'; result.toolUseId = request.toolUseId;
|
||
request.timestamp = result.timestamp = new Date(Date.parse(modeMessage(f).timestamp) - 1).toISOString();
|
||
if (name === 'gstack-question-preference') result.content = 'ASK\nEXIT: 0';
|
||
else request.input.command = request.input.command.replace('"auto_decided":true', '"auto_decided":false');
|
||
f.tools.push(request, result); expect(decision(f)).toBeNull();
|
||
});
|
||
|
||
test('a literal command cannot treat a physical newline as argument whitespace', () => {
|
||
const f = clone();
|
||
use(f, 'gstack-question-log').input.command = use(f, 'gstack-question-log').input.command.replace("gstack-question-log '", "gstack-question-log\n'");
|
||
expect(decision(f)).toBeNull();
|
||
});
|
||
|
||
for (const fallback of ['"LOGGED"', '" LOGGED "', '"\\x4cOGGED"', '-e "\\x4cOGGED"'])
|
||
test(`a failure branch cannot impersonate the question-log success marker: ${fallback}`, () => {
|
||
const f = clone(), request = use(f, 'gstack-question-log');
|
||
request.input.command = request.input.command.replace('"log unavailable (best-effort)"', fallback);
|
||
expect(decision(f)).toBeNull();
|
||
});
|
||
{
|
||
const copy=()=>structuredClone(completedModeCapture);
|
||
const check=(f:any)=>findNativeAutoDecision(f.transcript,f.tools,f.options);
|
||
const message=(f:any)=>f.transcript.assistantMessages.find((m:any)=>m.text.includes('Mode decision done:'));
|
||
const logUse=(f:any)=>f.tools.find((t:any)=>t.kind==='use'&&t.input?.command?.includes('gstack-question-log'));
|
||
test('actual owned public attempt fails original and completes mode-only with full acknowledged authority',()=>{
|
||
const f=copy();const v=check(f);expect(v?.option).toBe('HOLD SCOPE');expect(v?.questionLogToolUseId).toBe(logUse(f).toolUseId);
|
||
});
|
||
const mutations:Record<string,(f:any)=>void>={
|
||
'unlogged':f=>{const id=logUse(f).toolUseId;f.tools=f.tools.filter((t:any)=>t.toolUseId!==id)},
|
||
'failed log':f=>{f.tools.find((t:any)=>t.kind==='result'&&t.toolUseId===logUse(f).toolUseId).isError=true},
|
||
'masked log failure':f=>{logUse(f).input.command=logUse(f).input.command.replace('&& echo','; echo')},
|
||
'wrong returned marker':f=>{f.tools.find((t:any)=>t.kind==='result'&&t.toolUseId===logUse(f).toolUseId).content='LOG_FAILED (best-effort)'},
|
||
'unmatched quote':f=>{logUse(f).input.command=logUse(f).input.command.replace('"LOGGED"','"LOGGED')},
|
||
'foreign session':f=>{f.options.sessionId='foreign'},
|
||
'wrong mode':f=>{message(f).text=message(f).text.replace('done: HOLD SCOPE','done: SCOPE EXPANSION')},
|
||
'unfinished':f=>{message(f).text=message(f).text.replace('Mode decision done:','Mode decision pending:')},
|
||
'late declaration':f=>{message(f).timestamp=new Date(f.options.now+1000).toISOString()},
|
||
'prior declaration':f=>{message(f).timestamp=new Date(f.options.commandStartedAt-1000).toISOString()},
|
||
'cancelled':f=>{message(f).text+='\n\nI cancel this decision.'},
|
||
'wrong later completed mode':f=>{message(f).text+='\n\nMode decision done: SCOPE EXPANSION'},
|
||
'quoted declaration':f=>{message(f).text='> '+message(f).text},
|
||
'hypothetical':f=>{message(f).text='Example:\n'+message(f).text},
|
||
'conditional':f=>{message(f).text=message(f).text.replace('done: HOLD SCOPE','done: HOLD SCOPE (if approved)')},
|
||
'native question surfaced':f=>{f.transcript.calls.push({sessionId:f.options.sessionId})},
|
||
'wrong logged mode':f=>{logUse(f).input.command=logUse(f).input.command.replace('"user_choice":"HOLD SCOPE"','"user_choice":"SCOPE EXPANSION"')},
|
||
};
|
||
for(const [name,mutate] of Object.entries(mutations))test(name,()=>{const f=copy();mutate(f);expect(check(f)).toBeNull()});
|
||
|
||
for(const completion of ['done','complete','completed']) {
|
||
test(`completed mode class ${completion}`,()=>{const f=copy();message(f).text=message(f).text.replace('decision done:','decision '+completion+':');expect(check(f)?.option).toBe('HOLD SCOPE')});
|
||
test(`conflicting later completed mode ${completion}`,()=>{const f=copy();message(f).text+='\n\nMode decision '+completion+': SCOPE EXPANSION';expect(check(f)).toBeNull()});
|
||
test(`unfinished completed mode ${completion}`,()=>{const f=copy();message(f).text=message(f).text.replace('done: HOLD SCOPE',completion+': HOLD SCOPE (pending approval)');expect(check(f)).toBeNull()});
|
||
}
|
||
test('paired single-quoted success token retains exact shell ACK',()=>{const f=copy();logUse(f).input.command=logUse(f).input.command.replace('"LOGGED"',"'LOGGED'");expect(check(f)?.option).toBe('HOLD SCOPE')});
|
||
test('unpaired single-quoted success token cannot authenticate log',()=>{const f=copy();logUse(f).input.command=logUse(f).input.command.replace('"LOGGED"',"'LOGGED");expect(check(f)).toBeNull()});
|
||
|
||
}
|
||
{
|
||
const fixture=statusFixture;
|
||
const fixed=findNativeAutoDecision;
|
||
const copy=()=>structuredClone(fixture) as any;
|
||
const message=(f:any)=>f.transcript.assistantMessages.find((m:any)=>m.text.includes('Mode decision done:'));
|
||
const check=(f:any)=>fixed(f.transcript,f.tools,f.options);
|
||
test('current pending status retracts the completed owned mode',()=>{const f=copy();message(f).text+='\n\nMode decision pending: HOLD SCOPE';expect(check(f)).toBeNull()});
|
||
for(const status of ['pending','pending approval','unfinished','incomplete','cancelled','canceled','withdrawn','retracted','revoked','undecided','proposed','not selected','not decided','not yet complete','in progress','on hold','unknown']){
|
||
test(`unfinished declaration ${status}`,()=>{const f=copy();message(f).text=message(f).text.replace('decision done:','decision '+status+':');expect(check(f)).toBeNull()});
|
||
test(`later unfinished status ${status}`,()=>{const f=copy();message(f).text+='\n\nMode decision '+status+': HOLD SCOPE';expect(check(f)).toBeNull()});
|
||
test(`quoted historical status ${status}`,()=>{const f=copy();message(f).text+='\n\n> Historical example:\n> Mode decision '+status+': HOLD SCOPE';expect(check(f)?.option).toBe('HOLD SCOPE')});
|
||
}
|
||
for(const status of ['done','complete','completed']){
|
||
test(`same current completed field ${status}`,()=>{const f=copy();message(f).text+='\n\nMode decision '+status+': HOLD SCOPE';expect(check(f)?.option).toBe('HOLD SCOPE')});
|
||
test(`completed conflicting field ${status}`,()=>{const f=copy();message(f).text+='\n\nMode decision '+status+': SCOPE EXPANSION';expect(check(f)).toBeNull()});
|
||
}
|
||
for(const status of ['unfinished','incomplete','pending approval','cancelled','not completed'])test(`unfinished value suffix ${status}`,()=>{const f=copy();message(f).text+='\n\nMode decision done: HOLD SCOPE ('+status+')';expect(check(f)).toBeNull()});
|
||
for(const text of ['Historical example: Mode decision pending: HOLD SCOPE','```\nMode decision pending: HOLD SCOPE\n```','"Mode decision cancelled: HOLD SCOPE"'])test(`unasserted historical field ${text}`,()=>{const f=copy();message(f).text+='\n\n'+text;expect(check(f)?.option).toBe('HOLD SCOPE')});
|
||
}
|
||
});
|
||
|
||
describe('auto-decide-target-identity', () => {
|
||
const capture = capture_auto_decide_target_identity;
|
||
const clone = () => structuredClone(capture) as any;
|
||
const message = (f: any) => f.transcript.assistantMessages.at(-1);
|
||
const decide = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
|
||
test('actual quoted current title and completed owned audit produce the original mode decision', () => {
|
||
const f = clone(), result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.annotation).toBe(message(f).text);
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
expect(result?.preambleToolUseId).toBe('toolu_01KbsH6ybJxbNozwbSXywVbb');
|
||
});
|
||
|
||
const title = 'deterministic skill-list ordering';
|
||
const modes = ['HOLD SCOPE', 'SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'SCOPE REDUCTION'];
|
||
for (const mode of modes) for (const quote of [(s: string) => `"${s}"`, (s: string) => `“${s}”`, (s: string) => `\`${s}\``]) {
|
||
for (const wrapper of ['', ' draft', ' plan']) test(`${mode} quoted title agrees with one audit wrapper: ${quote(title)}${wrapper}`, () => {
|
||
const f = clone();
|
||
Object.assign(f.options.stateEvidence.records[0], { user_choice: mode, recommended: mode, question_summary: `Select review mode for ${title}${wrapper}` });
|
||
message(f).text = `Decision: ${mode} for ${quote(title)}.\n\nMode: ${mode}, auto-selected using the saved preference.`;
|
||
expect(decide(f)?.option).toBe(mode);
|
||
expect(decide(f)?.annotation).toBe(message(f).text);
|
||
});
|
||
}
|
||
for (const [declared, recorded] of [
|
||
[`"${title}" draft`, `"${title}"`],
|
||
[`"${title}" plan`, `${title} draft`],
|
||
[title, `${title} draft`],
|
||
[`${title} draft`, title],
|
||
['"release plan"', 'release plan draft'],
|
||
['"what if ordering"', 'what if ordering draft'],
|
||
['"ordering v2. current"', '"ordering v2. current" draft'],
|
||
]) test(`exact title identity with syntactic wrapper: ${declared} / ${recorded}`, () => {
|
||
const f = clone(); f.options.stateEvidence.records[0].question_summary = `Select mode for ${recorded}`;
|
||
message(f).text = `Decision: HOLD SCOPE for ${declared}.`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
test('quoted target and mode labels remain case insensitive', () => {
|
||
const f = clone(); message(f).text = `decision: hold scope FOR "${title}".`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
|
||
const negatives: Array<[string, string]> = [
|
||
['"deterministic skill-list sorting"', `${title} draft`],
|
||
['"skill-list ordering"', `${title} draft`],
|
||
[`"${title}-v2"`, `${title} draft`],
|
||
[`"${title} extra"`, `${title} draft`],
|
||
['"release"', '"release draft"'],
|
||
['"release draft"', '"release"'],
|
||
['"release plan"', 'release draft'],
|
||
['release plan', 'release draft'],
|
||
['"release draft plan"', 'release plan'],
|
||
['"release plan draft"', '"release plan"'],
|
||
['"draft release"', 'release'],
|
||
['""', 'draft'],
|
||
['" "', 'plan'],
|
||
[`"${title}" or "foreign"`, `${title} draft`],
|
||
[`"${title}" and another plan`, `${title} draft`],
|
||
[`"${title}`, `${title} draft`],
|
||
[`${title}"`, `${title} draft`],
|
||
['"future plan"', 'future plan'],
|
||
['future', 'future plan'],
|
||
['previous', 'previous draft'],
|
||
['"previous draft"', 'previous draft'],
|
||
['"another draft"', 'another draft'],
|
||
['"next plan"', 'next plan'],
|
||
];
|
||
for (const [declared, recorded] of negatives) {
|
||
test(`target cannot borrow a named or historical match: ${declared} / ${recorded}`, () => {
|
||
const f = clone(); f.options.stateEvidence.records[0].question_summary = `Select mode for ${recorded}`;
|
||
message(f).text = `Decision: HOLD SCOPE for ${declared}.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
test(`later agreeing Mode does not erase invalid target: ${declared} / ${recorded}`, () => {
|
||
const f = clone(); f.options.stateEvidence.records[0].question_summary = `Select mode for ${recorded}`;
|
||
message(f).text = `Decision: HOLD SCOPE for ${declared}.\n\nMode: HOLD SCOPE, auto-selected.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
}
|
||
|
||
for (const wrap of [
|
||
(s: string) => `"${s}"`, (s: string) => `“${s}”`, (s: string) => `\`${s}\``,
|
||
(s: string) => `> ${s}`, (s: string) => ` ${s}`, (s: string) => `\`\`\`text\n${s}\n\`\`\``,
|
||
(s: string) => `Example:\n${s}`, (s: string) => `Previous review:\n${s}`,
|
||
]) test(`only an asserted field can own a quoted target: ${wrap('Decision')}`, () => {
|
||
const f = clone(); message(f).text = wrap(`Decision: HOLD SCOPE for "${title}".`);
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
for (const value of [
|
||
`HOLD SCOPE for "${title}" if approved`, `HOLD SCOPE for "${title}", pending approval`,
|
||
`not HOLD SCOPE for "${title}"`, `HOLD SCOPE for "${title}"; SCOPE EXPANSION`,
|
||
`HOLD SCOPE for "${title}" (withdrawn)`, `HOLD SCOPE for "${title}" (I will select it)`,
|
||
]) test(`quoted name cannot hide a lifecycle veto: ${value}`, () => {
|
||
const f = clone(); message(f).text = `Decision: ${value}.\n\nMode: HOLD SCOPE.`;
|
||
expect(decide(f)).toBeNull();
|
||
});
|
||
for (const suffix of [
|
||
'\n\nCorrection: Mode: SCOPE EXPANSION.',
|
||
'\n\nCorrection: I withdraw this decision.',
|
||
`\n\nDecision: HOLD SCOPE for "foreign target".`,
|
||
'\n\nMode pending: HOLD SCOPE.',
|
||
]) test(`a later contradiction remains effective: ${suffix}`, () => {
|
||
const f = clone(); message(f).text += suffix; expect(decide(f)).toBeNull();
|
||
});
|
||
for (const [name, mutate] of Object.entries({
|
||
'missing owned log': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'duplicate owned log': (f: any) => { f.options.stateEvidence.records.push({ ...f.options.stateEvidence.records[0] }); },
|
||
'foreign audit session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'different audit choice': (f: any) => { f.options.stateEvidence.records[0].user_choice = 'SCOPE EXPANSION'; },
|
||
'wrong preference': (f: any) => { f.options.stateEvidence.preference = 'ask'; },
|
||
'missing preamble ACK': (f: any) => { f.tools = f.tools.filter((e: any) => !(e.kind === 'result' && e.toolUseId === 'toolu_01KbsH6ybJxbNozwbSXywVbb')); },
|
||
'native question': (f: any) => { f.transcript.calls.push({sessionId:f.options.sessionId}); },
|
||
'prose question': (f: any) => { f.options.proseQuestionObserved = true; },
|
||
'decision before log': (f: any) => { message(f).timestamp = new Date(Date.parse(f.options.stateEvidence.records[0].ts) - 1).toISOString(); },
|
||
'wrong native session': (f: any) => { f.options.sessionId = 'foreign'; },
|
||
'future log': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.now + 1).toISOString(); },
|
||
})) test(`actual quoted target retains ${name} boundary`, () => {
|
||
const f = clone(); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
for (const preposition of ['for', 'FOR']) test(`a quoted lifecycle word belongs to its title with ${preposition}`, () => {
|
||
const f = clone(); f.options.stateEvidence.records[0].question_summary = 'Select mode for Pending notifications draft';
|
||
message(f).text = `Decision: HOLD SCOPE ${preposition} "Pending notifications".`;
|
||
expect(decide(f)?.option).toBe('HOLD SCOPE');
|
||
});
|
||
});
|
||
|
||
describe('auto-decision-state', () => {
|
||
const capture = capture_auto_decision_state;
|
||
const clone = () => structuredClone(capture) as any;
|
||
const qid = 'plan-ceo-review-mode';
|
||
function state(f: any) {
|
||
const use = f.tools.find((e: any) => e.input?.command?.includes('gstack-question-log'));
|
||
// Synthetic file witness, built from the actual literal request. The original
|
||
// run did not retain this file, and is still a failed paid attempt.
|
||
const record = JSON.parse(/gstack-question-log '(\{[^\n]*\})'/.exec(use.input.command)![1]!);
|
||
record.source = 'agent';
|
||
record.ts = f.tools.find((e: any) => e.kind === 'result' && e.toolUseId === use.toolUseId).timestamp;
|
||
return { questionId: qid, preference: 'never-ask' as const, records: [record] };
|
||
}
|
||
const decide = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||
const mode = (f: any) => f.transcript.assistantMessages.find((m: any) => m.text.startsWith('**Mode:'));
|
||
|
||
test('original captured retry cannot prove a masked log succeeded', () => {
|
||
expect(decide(clone())).toBeNull();
|
||
});
|
||
test('actual retry declaration plus a completed owned append proves the chosen mode', () => {
|
||
const f = clone(); f.options.stateEvidence = state(f);
|
||
const result = decide(f);
|
||
expect(result?.option).toBe('HOLD SCOPE');
|
||
expect(result?.stateRecord).toEqual(f.options.stateEvidence.records[0]);
|
||
expect(result?.questionLogToolUseId).toBeUndefined();
|
||
});
|
||
|
||
for (const [name, mutate] of Object.entries({
|
||
'foreign record session': (f: any) => { f.options.stateEvidence.records[0].session_id = 'foreign'; },
|
||
'wrong question': (f: any) => { f.options.stateEvidence.questionId = 'wrong'; },
|
||
'wrong skill': (f: any) => { f.options.stateEvidence.records[0].skill = 'plan-eng-review'; },
|
||
'nonautomatic record': (f: any) => { f.options.stateEvidence.records[0].auto_decided = false; },
|
||
'string flag': (f: any) => { f.options.stateEvidence.records[0].auto_decided = 'true'; },
|
||
'wrong source': (f: any) => { f.options.stateEvidence.records[0].source = 'hook'; },
|
||
'different preference': (f: any) => { f.options.stateEvidence.preference = 'always-ask'; },
|
||
'missing append': (f: any) => { f.options.stateEvidence.records = []; },
|
||
'duplicate append': (f: any) => { f.options.stateEvidence.records.push({ ...f.options.stateEvidence.records[0] }); },
|
||
'contradictory recommendation': (f: any) => { f.options.stateEvidence.records[0].recommended = 'SCOPE EXPANSION'; },
|
||
'empty summary': (f: any) => { f.options.stateEvidence.records[0].question_summary = ''; },
|
||
'old record': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.commandStartedAt - 1).toISOString(); },
|
||
'future record': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(f.options.now + 1).toISOString(); },
|
||
'record after declaration': (f: any) => { f.options.stateEvidence.records[0].ts = new Date(Date.parse(mode(f).timestamp) + 1).toISOString(); },
|
||
'invalid timestamp': (f: any) => { f.options.stateEvidence.records[0].ts = 'invalid'; },
|
||
'actual native question': (f: any) => { f.transcript.calls.push({ sessionId: f.options.sessionId }); },
|
||
'actual prose question': (f: any) => { f.options.proseQuestionObserved = true; },
|
||
'failed preamble': (f: any) => { f.tools.find((e: any) => e.kind === 'result' && e.content?.includes('SKILL_START_PROTO')).isError = true; },
|
||
'quoted declaration': (f: any) => { mode(f).text = '> Mode: HOLD SCOPE (saved preference).'; },
|
||
'conditional declaration': (f: any) => { mode(f).text = 'Mode: HOLD SCOPE (if approved).'; },
|
||
'later withdrawal': (f: any) => { mode(f).text += '\n\nCorrection: I withdraw this decision.'; },
|
||
'later different mode': (f: any) => { mode(f).text += '\n\nMode: SCOPE EXPANSION (saved preference).'; },
|
||
})) test(`owned log witness rejects ${name}`, () => {
|
||
const f = clone(); f.options.stateEvidence = state(f); mutate(f); expect(decide(f)).toBeNull();
|
||
});
|
||
|
||
function withState(check: (x: { root: string; project: string; pref: string; log: string; bind: () => ReturnType<typeof bindAutoDecisionState> }) => void) {
|
||
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'auto-state-')));
|
||
const project = path.join(root, 'projects', 'fixture'); fs.mkdirSync(project, { recursive: true });
|
||
const pref = path.join(project, 'question-preferences.json'), log = path.join(project, 'question-log.jsonl');
|
||
fs.writeFileSync(pref, JSON.stringify({ [qid]: 'never-ask' }));
|
||
const bind = () => bindAutoDecisionState({ stateRoot: root, projectSlug: 'fixture' }, { GSTACK_STATE_ROOT: root }, 'plan-ceo-review');
|
||
try { check({ root, project, pref, log, bind }); } finally { fs.rmSync(root, { recursive: true, force: true }); }
|
||
}
|
||
test('state witness binds before launch and observes only completed owned file contents', () => withState(({ log, bind }) => {
|
||
const read = bind(); expect(read()).toBeUndefined();
|
||
const record = state(clone()).records[0]; fs.writeFileSync(log, JSON.stringify(record) + '\n');
|
||
expect(read()?.records).toEqual([record]);
|
||
}));
|
||
for (const scenario of ['existing-log', 'preference-change', 'malformed-log', 'log-symlink', 'preference-symlink', 'wrong-root', 'path-escape'])
|
||
test(`state binding rejects ${scenario}`, () => withState(({ root, pref, log, bind }) => {
|
||
if (scenario === 'wrong-root' || scenario === 'path-escape') {
|
||
expect(() => bindAutoDecisionState({ stateRoot: root, projectSlug: scenario === 'path-escape' ? '../fixture' : 'fixture' },
|
||
{ GSTACK_STATE_ROOT: scenario === 'wrong-root' ? root + '-other' : root }, 'plan-ceo-review')).toThrow(); return;
|
||
}
|
||
if (scenario === 'existing-log') { fs.writeFileSync(log, '{}\n'); expect(bind).toThrow('fresh attempt'); return; }
|
||
const read = bind();
|
||
if (scenario === 'preference-change') fs.writeFileSync(pref, JSON.stringify({ [qid]: 'always-ask' }));
|
||
if (scenario === 'malformed-log') fs.writeFileSync(log, '{');
|
||
if (scenario === 'log-symlink') fs.symlinkSync(pref, log);
|
||
if (scenario === 'preference-symlink') { fs.renameSync(pref, pref + '.real'); fs.symlinkSync(pref + '.real', pref); }
|
||
expect(read()).toBeUndefined();
|
||
}));
|
||
});
|