Files
gstack/test/workflow-boundaries-fixture.test.ts
Garry Tanandcapy-ai[bot] 3766edac8d v1.91.144.0 feat(evals): Opus 5.5 is the paid gate's model under test; judge stays Fable (GSTA-49 TM1) (#3275)
* feat(evals): Opus 5.5 is the paid gate's model under test; judge stays Fable (GSTA-49 TM1)

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

* v1.91.140.0 chore: stamp TM1 (Opus 5.5 model under test) and CHANGELOG

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

* fix(evals): repair the five Opus gate reds from run 39161502626

- coverage audit: owned cd; followed by one && chain, wc -l on in-cwd files
- shared-libs: Claude Code 2.1.292 git --version probe, exact argv only
- ship hook: accept 'integrate the hook yourself' / 'combine ... by hand'
- qa-only: deadline starts only after the charter Write completes
- plan floor: tool-only parent session identifies the owned Create grant;
  floor runner fails fast with PERMISSION_DIALOG_UNANSWERED

Each repair replays its captured run as a free regression with negative
controls. HARNESS_VERSION stays 6 (re-recorded from main's 5).

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

* fix(evals): ship hook manual-integration sentence rule; qa-only inspection uses Read/Glob; tool-only permission tests join their owner file

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

* fix(evals): tpa-absent-linux accepts instructions the user follows themselves as the manual option

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

* fix(evals): owned plan Edit menu binds to exactly one pending Edit, including unpublished ones

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

* fix(evals): shared-libs read evidence counts a literal path loop whose output names each file

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

* chore: restamp to v1.91.142.0 (queue slot moved)

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

* fix(evals): investigate boundary fixture identifies the owner question by its continue/abort options

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

* fix(evals): plan-design-with-ui focus check accepts the skill's design-completeness rating in the question body

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

* fix(evals): phrasing sweep across the 108 gate rule cases; replay captured Opus paraphrases with negative controls

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

* fix(evals): command-detector sweep — exact-count anchor, label slot, 2.1.292 worktree probe

- coverage audit (gate 39207508651 plan-eng-coverage-audit): output of exactly
  known length (wc -l over literal in-cwd files) anchors the next owned read
  at that line; a label's slot counts from the end and a later complete copy
  voids the credit (review/specialists/testing.md prints '---')
- shared-libs-unsupported-git (39161502626): Claude Code 2.1.292's askPass-first
  'worktree list --porcelain -z' cwd probe is Claude's own; the read-only Git
  check is shared (repoGitRequests, isReadOnlyRepoGitRequest) and the free
  regression replays every Git request of that capture

Each fix replays its captured command with negative controls. No threshold,
trial count, case kind or judge changed. HARNESS_VERSION stays 6.

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-19 07:26:49 +00:00

439 lines
29 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import type { Query, SDKMessage } from '@anthropic-ai/claude-agent-sdk';
import type { QueryProvider } from './helpers/agent-sdk-runner';
import { resolveClaudeBinary } from '../lib/claude-bin';
import type { EvalTestEntry } from './helpers/eval-store';
import { acknowledgesEndingError, createBoundaryFixture, runBoundaryActor, type BoundaryCase } from './helpers/workflow-boundaries-fixture';
import { E2E_TIERS, E2E_TOUCHFILES } from './helpers/touchfiles-data';
import { PR_PROFILE_CASE_IDS, PR_PROFILE_FILES } from '../scripts/test-pr-profile';
import { applyHollowShardGuard, buildRunManifest, computePaidCaseSelection, DEFAULT_SHARD_TIMEOUT_MS, expectedPrCaseCount, prProfileTestNamePattern, resolvePaidShardBudget, retriesForFiles, verifySliceResults, type SliceResult } from '../scripts/test-paid-shards';
import { CAPTURE_MS } from './helpers/eval-budgets';
import capturedQuestions from './fixtures/investigate-owned-question-39195396438.json';
import capturedFinals from './fixtures/investigate-owned-final-texts-opus.json';
const cases: BoundaryCase[] = ['investigate-owned-completion', 'investigate-owned-abort', 'investigate-owned-ending-error'];
const files = ['test/skill-e2e-investigate-owned-completion.test.ts', 'test/skill-e2e-investigate-owned-termination.test.ts'];
for (const attack of ['read-state', 'cat-state', 'bash-edit', 'receipt-write']) test(`fixture denies undeclared interaction: ${attack}`, async () => {
const fixture = createBoundaryFixture('investigate-owned-completion');
try {
const tool = attack === 'read-state' ? 'Read' : 'Bash';
const input = attack === 'read-state' ? { file_path: fixture.boundary }
: { command: attack === 'cat-state' ? `cat '${fixture.boundary}'`
: attack === 'bash-edit' ? 'printf "arbitrary source" > src/value.js' : `printf FREEZE_RELEASED > '${fixture.receipts}'` };
const decision = await fixture.canUseTool(tool, input, { signal: new AbortController().signal, toolUseID: attack });
expect(decision.behavior).toBe('deny');
} finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
});
function protocolControl(id: BoundaryCase, faults: { skipCleanup?: boolean; duplicateQuestion?: boolean; failVerification?: boolean; rateLimit?: boolean; undeclared?: string; deniedBash?: string; includeAvailability?: boolean; misleadingFinal?: boolean; splitFinal?: boolean; reverify?: 'pass' | 'fail-last'; finalText?: string } = {}) {
let directory = '';
let calls = 0;
const provider: QueryProvider = input => {
calls++;
const options = input.options!;
directory = options.cwd!;
const env = options.env!;
expect(options.permissionMode).toBe('default');
expect(options.allowDangerouslySkipPermissions).toBe(false);
expect(options.settingSources).toEqual([]);
expect(options.maxTurns).toBe(12);
expect(options.allowedTools).toEqual([]);
expect(options.hooks?.PreToolUse).toHaveLength(1);
expect(options.pathToClaudeCodeExecutable).toBe(resolveClaudeBinary() ?? undefined);
expect(fs.realpathSync(env.GSTACK_HOME!)).toBe(path.join(path.dirname(directory), 'state'));
expect(env.HOME).toBe(path.join(path.dirname(directory), 'home'));
const executeTool = async (tool: string, input: Record<string, unknown>) => {
const decision = await options.hooks!.PreToolUse![0].hooks[0]({
hook_event_name: 'PreToolUse', session_id: 'fixture', transcript_path: path.join(directory, 'transcript'),
cwd: directory, tool_name: tool, tool_input: input, tool_use_id: `fixture-${calls}`,
}, `fixture-${calls}`, { signal: new AbortController().signal });
if ('async' in decision) throw new Error('fixture hook must be synchronous');
expect(decision.hookSpecificOutput?.hookEventName).toBe('PreToolUse');
const hook = decision.hookSpecificOutput as { permissionDecision?: string; updatedInput?: Record<string, unknown> };
if (hook.permissionDecision === 'deny') throw new Error(`native fixture guard denied ${tool}`);
expect(hook.permissionDecision).toBe(tool === 'AskUserQuestion' ? 'ask' : 'allow');
return hook.updatedInput ?? input;
};
const shell = async (command: string) => {
const approved = await executeTool('Bash', { command });
expect(approved.timeout).toBe(10000);
expect(approved.run_in_background).toBe(false);
return spawnSync('bash', ['-c', approved.command as string], { cwd: directory, env, encoding: 'utf8', timeout: 10000 });
};
const ask = async (question: string, labels: string[]) => {
const input = { questions: [{ header: 'Fixture', question, options: labels.map(label => ({ label, description: label })), multiSelect: false }] };
await executeTool('AskUserQuestion', input);
return options.canUseTool!('AskUserQuestion', input, { signal: new AbortController().signal, toolUseID: `fixture-${calls}` });
};
return {
async *[Symbol.asyncIterator]() {
yield { type: 'system', subtype: 'init', session_id: 'fixture-session' } as SDKMessage;
if (faults.misleadingFinal) yield { type: 'assistant', session_id: 'fixture-session', parent_tool_use_id: null,
message: { id: 'message-early', role: 'assistant', content: [{ type: 'text', text: 'I will ask whether to continue or abort, and stop if the verifier reports an error.' }] } } as SDKMessage;
if (faults.undeclared) {
const state = path.join(env.GSTACK_HOME!, 'freeze-dir.txt');
const receipt = path.join(path.dirname(directory), 'receipts');
await executeTool(faults.undeclared === 'read-state' ? 'Read' : 'Bash', faults.undeclared === 'read-state' ? { file_path: state } : {
command: faults.undeclared === 'cat-state' ? `cat '${state}'`
: faults.undeclared === 'bash-edit' ? 'printf "arbitrary source" > src/value.js' : `printf FREEZE_RELEASED > '${receipt}'`,
});
}
expect(fs.readFileSync(path.join(directory, 'workflow.md'), 'utf8')).toContain('Terminal cleanup');
expect(fs.existsSync(path.join(env.GSTACK_HOME!, 'freeze-dir.txt'))).toBe(false);
if (faults.deniedBash) {
await expect(executeTool('Bash', { command: faults.deniedBash })).rejects.toThrow('native fixture guard denied Bash');
const receipts = path.join(path.dirname(directory), 'receipts');
expect(fs.existsSync(receipts) ? fs.readFileSync(receipts, 'utf8') : '').toBe('');
expect(fs.existsSync(path.join(env.GSTACK_HOME!, 'freeze-dir.txt'))).toBe(false);
}
if (faults.includeAvailability) {
const workflow = fs.readFileSync(path.join(directory, 'workflow.md'), 'utf8');
const availability = workflow.match(/```bash\n([\s\S]*?)```/)![1].trim();
const checked = await shell(availability);
expect(checked.status).toBe(0);
expect(checked.stdout).toContain('FREEZE_AVAILABLE');
}
const acquisition = await shell('bash "$HOME/.claude/skills/gstack/freeze/bin/freeze-state.sh" acquire "src"');
expect(acquisition.status).toBe(0);
const owner = acquisition.stdout.match(/FREEZE_OWNER=([a-f0-9]{32})/)![1];
if (faults.rateLimit && calls === 1) {
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Attempt one acquired scope.' }] } } as SDKMessage;
throw Object.assign(new Error('rate limit'), { status: 429 });
}
const decision = await ask('Continue this investigation or abort?', ['Continue', 'Abort']);
expect(decision.behavior).toBe('allow');
if (decision.behavior !== 'allow') throw new Error('fixture refused its declared question');
if (faults.duplicateQuestion) await ask('Continue this investigation or abort?', ['Continue', 'Abort']);
const answer = (decision.updatedInput!.answers as Record<string, string>)['Continue this investigation or abort?'];
if (answer === 'Continue') {
await executeTool('Edit', { file_path: path.join(directory, 'src/value.js'), old_string: 'return 1', new_string: 'return 2' });
expect(fs.realpathSync(path.join(directory, 'src/value.js')).startsWith(directory + path.sep)).toBe(true);
fs.writeFileSync(path.join(directory, 'src/value.js'), 'export function value() { return 2; }\n');
if (faults.failVerification) {
const file = path.join(directory, 'verify.sh');
expect(fs.realpathSync(file).startsWith(directory + path.sep)).toBe(true);
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('value() !== 2', 'value() !== 3'));
}
const verified = await shell('bash ./verify.sh');
expect(verified.status).toBe(id === 'investigate-owned-ending-error' ? 69 : faults.failVerification ? 1 : 0);
if (faults.reverify) {
if (faults.reverify === 'fail-last') {
const file = path.join(directory, 'verify.sh');
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('value() !== 2', 'value() !== 3'));
}
await shell('bash ./verify.sh');
}
} else expect(answer).toBe('Abort');
if (!faults.skipCleanup) expect((await shell(`bash "$HOME/.claude/skills/gstack/freeze/bin/freeze-state.sh" release "${owner}"`)).status).toBe(0);
const finalText = faults.finalText ?? (faults.misleadingFinal ? 'The fix is complete and verification succeeded.'
: id === 'investigate-owned-abort' ? 'Aborted at your request; no edit was made and the owned boundary was released.'
: id === 'investigate-owned-ending-error' ? 'The verifier is unavailable; the investigation ended with an error and the owned boundary was released.'
: 'The correction passed verification and the owned boundary was released.');
yield { type: 'assistant', session_id: 'fixture-session', parent_tool_use_id: null,
message: { id: 'message-final', role: 'assistant', content: [{ type: 'text', text: finalText }] } } as SDKMessage;
if (faults.splitFinal) yield { type: 'assistant', session_id: 'fixture-session', parent_tool_use_id: null,
message: { id: 'message-final', role: 'assistant', content: [{ type: 'text', text: 'No further action was taken.' }] } } as SDKMessage;
yield { type: 'result', subtype: 'success', num_turns: 1, total_cost_usd: 0 } as SDKMessage;
},
} as Query;
};
return { provider, directory: () => directory, calls: () => calls };
}
for (const id of cases) test(`fixture protocol control: ${id} persists receipts after cleanup`, async () => {
const driver = protocolControl(id);
const records: EvalTestEntry[] = [];
await runBoundaryActor(id, entry => records.push(entry), driver.provider);
expect(records).toHaveLength(1);
expect(records[0].passed).toBe(true);
expect(records[0].cost_usd).toBe(0);
expect(fs.existsSync(path.dirname(driver.directory()))).toBe(false);
const retained = JSON.parse(records[0].output!);
expect(retained.attempts).toHaveLength(1);
expect(retained.attempts[0].events).toHaveLength(3);
expect(retained.evidence.changedProtectedFiles).toEqual([]);
expect(retained.evidence.receipts).toContain('FREEZE_RELEASED');
expect(retained.evidence.interactions).toHaveLength(1);
});
test('captured commentless availability is denied, while the complete block is executable', async () => {
const command = `_FREEZE_SCRIPT="$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
[ -x "$_FREEZE_SCRIPT" ] && echo "FREEZE_AVAILABLE" || echo "FREEZE_UNAVAILABLE"`;
const driver = protocolControl('investigate-owned-completion', { deniedBash: command, includeAvailability: true });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider)).rejects.toThrow('undeclared interaction');
expect(records[0]).toMatchObject({ passed: false, exit_reason: 'assertion_failed' });
const evidence = JSON.parse(records[0].output!).evidence;
expect(evidence.executions.find((event: { input: { command?: string } }) => event.input.command === command).allowed).toBe(false);
expect(evidence.executions.some((event: { allowed: boolean; input: { command?: string } }) => event.allowed && event.input.command?.includes('FREEZE_AVAILABLE'))).toBe(true);
expect(evidence.source).toBe('export function value() { return 2; }\n');
expect(evidence.receipts).toContain('FREEZE_RELEASED');
expect(evidence.boundary).toBe('');
const clean = protocolControl('investigate-owned-completion', { includeAvailability: true });
const passing: EvalTestEntry[] = [];
await runBoundaryActor('investigate-owned-completion', entry => passing.push(entry), clean.provider);
expect(passing[0].passed).toBe(true);
expect(JSON.parse(passing[0].output!).evidence.executions.some((event: { allowed: boolean; input: { command?: string } }) => event.allowed && event.input.command?.includes('FREEZE_AVAILABLE'))).toBe(true);
});
for (const attack of ['read-state', 'cat-state', 'bash-edit', 'receipt-write']) test(`registered native hook rejects and retains denied ${attack}`, async () => {
const driver = protocolControl('investigate-owned-completion', { undeclared: attack });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider)).rejects.toThrow('native fixture guard denied');
expect(records[0].passed).toBe(false);
const evidence = JSON.parse(records[0].output!).evidence;
expect(evidence.executions).toHaveLength(1);
expect(evidence.executions[0].allowed).toBe(false);
expect(evidence.source).toBe('export function value() { return 1; }\n');
expect(evidence.receipts).toBe('');
expect(fs.existsSync(path.dirname(driver.directory()))).toBe(false);
});
test('duplicate owner questions are rejected by the registered native hook', async () => {
const driver = protocolControl('investigate-owned-completion', { duplicateQuestion: true });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider)).rejects.toThrow('native fixture guard denied AskUserQuestion');
const evidence = JSON.parse(records[0].output!).evidence;
expect(evidence.interactions.filter((event: { disposition: string }) => event.disposition === 'continue-investigation')).toHaveLength(1);
expect(evidence.interactions.at(-1).disposition).toBe('unsupported-tool');
expect(evidence.executions.at(-1).allowed).toBe(false);
});
test('fixture rejects a successful actor result when owned cleanup is missing', async () => {
const driver = protocolControl('investigate-owned-abort', { skipCleanup: true });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-abort', entry => records.push(entry), driver.provider)).rejects.toThrow('owned boundary remains');
expect(records[0].passed).toBe(false);
expect(records[0].exit_reason).toBe('assertion_failed');
expect(JSON.parse(records[0].output!).evidence.boundary).toContain('gstack-freeze-v1:');
expect(fs.existsSync(path.dirname(driver.directory()))).toBe(false);
});
test('fixture records failed verification rather than crediting successful cleanup', async () => {
const driver = protocolControl('investigate-owned-completion', { failVerification: true });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider)).rejects.toThrow('verification did not succeed');
expect(JSON.parse(records[0].output!).evidence.receipts).toContain('VERIFY_STATUS:1');
});
test('a passing re-verification is not a protocol failure', async () => {
const driver = protocolControl('investigate-owned-completion', { reverify: 'pass' });
const records: EvalTestEntry[] = [];
await runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider);
expect(records[0].passed).toBe(true);
expect(JSON.parse(records[0].output!).evidence.receipts.match(/^VERIFY$/gm)).toHaveLength(2);
});
test('a failing final re-verification still fails after an earlier success', async () => {
const driver = protocolControl('investigate-owned-completion', { reverify: 'fail-last' });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider)).rejects.toThrow('final verification did not succeed');
expect(records[0].passed).toBe(false);
});
test('retrying the verifier after its ending error still fails', async () => {
const driver = protocolControl('investigate-owned-ending-error', { reverify: 'pass' });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor('investigate-owned-ending-error', entry => records.push(entry), driver.provider)).rejects.toThrow('without retry');
});
test('rate-limit retry restores the fixture and retains both native event streams', async () => {
const driver = protocolControl('investigate-owned-completion', { rateLimit: true });
const records: EvalTestEntry[] = [];
await runBoundaryActor('investigate-owned-completion', entry => records.push(entry), driver.provider);
expect(driver.calls()).toBe(2);
const retained = JSON.parse(records[0].output!);
expect(retained.attempts).toHaveLength(2);
expect(retained.attempts[0].evidence.boundary).toContain('gstack-freeze-v1:');
expect(retained.attempts[0].events.find((event: SDKMessage) => event.type === 'assistant').message.content[0].text).toContain('Attempt one');
expect(retained.attempts[1].evidence.boundary).toBe('');
});
for (const id of ['investigate-owned-abort', 'investigate-owned-ending-error'] as const) {
test(`${id}: an earlier acknowledgment cannot cover a misleading final response`, async () => {
const driver = protocolControl(id, { misleadingFinal: true });
const records: EvalTestEntry[] = [];
await expect(runBoundaryActor(id, entry => records.push(entry), driver.provider)).rejects.toThrow(/acknowledge/);
expect(records[0].passed).toBe(false);
const retained = JSON.parse(records[0].output!);
expect(retained.evidence.receipts).toContain('FREEZE_RELEASED');
expect(retained.evidence.boundary).toBe('');
expect(retained.assistant).toContain('whether to continue or abort');
expect(retained.assistant).toContain('The fix is complete and verification succeeded.');
expect(fs.existsSync(path.dirname(driver.directory()))).toBe(false);
});
test(`${id}: the actual final response can span multiple native fragments`, async () => {
const driver = protocolControl(id, { splitFinal: true });
const records: EvalTestEntry[] = [];
await runBoundaryActor(id, entry => records.push(entry), driver.provider);
expect(records[0].passed).toBe(true);
});
}
test('fixture binds the configured CLI executable rather than the SDK bundled version', async () => {
const previous = process.env.GSTACK_CLAUDE_BIN;
process.env.GSTACK_CLAUDE_BIN = process.execPath;
try {
const driver = protocolControl('investigate-owned-abort');
const records: EvalTestEntry[] = [];
await runBoundaryActor('investigate-owned-abort', entry => records.push(entry), driver.provider);
expect(records[0].passed).toBe(true);
} finally {
if (previous === undefined) delete process.env.GSTACK_CLAUDE_BIN;
else process.env.GSTACK_CLAUDE_BIN = previous;
}
});
test('fixture refuses a skill registration escaping the temporary root before writing', () => {
const root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'gbound-link-'));
const outside = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'gbound-out-'));
try {
fs.symlinkSync(outside, path.join(root, 'home'));
expect(() => createBoundaryFixture('investigate-owned-completion', root)).toThrow('fixture write escapes');
expect(fs.readdirSync(outside)).toEqual([]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
fs.rmSync(outside, { recursive: true, force: true });
}
});
test('F9 actors are registered in the gate PR profile with exact file ownership', () => {
for (const id of cases) {
expect(E2E_TIERS[id], id).toBe('gate');
expect(PR_PROFILE_CASE_IDS as readonly string[]).toContain(id);
expect(E2E_TOUCHFILES[id]).toContain('test/helpers/workflow-boundaries-fixture.ts');
}
expect(PR_PROFILE_FILES[files[0]]).toEqual([cases[0]]);
expect(PR_PROFILE_FILES[files[1]]).toEqual(cases.slice(1));
});
test('F9 changed-input selection produces three cases with exact patterns and counts', () => {
const selected = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['freeze/bin/freeze-state.sh'] });
expect(selected.coverage?.mode).toBe('pr');
expect(selected.coverage?.unknownFiles).toEqual([]);
expect(selected.selection.e2e).toEqual([...cases].sort());
expect(files.map(file => expectedPrCaseCount(file, selected.selection))).toEqual([1, 2]);
expect(prProfileTestNamePattern(files[0], selected.selection)).toBe('(?:^|\\s)(?:investigate-owned-completion)$');
expect(prProfileTestNamePattern(files[1], selected.selection)).toBe('(?:^|\\s)(?:investigate-owned-abort|investigate-owned-ending-error)$');
});
test('both F9 files fit the existing wall with their one run and reserve', () => {
for (const file of files) {
const source = fs.readFileSync(path.join(import.meta.dir, '..', file), 'utf8');
const count = PR_PROFILE_FILES[file].length;
expect([...source.matchAll(/\}, CAPTURE_MS\);/g)]).toHaveLength(count);
expect(retriesForFiles([file])).toBe(0);
const budget = resolvePaidShardBudget([file]);
expect(budget).toEqual({ timeoutMs: DEFAULT_SHARD_TIMEOUT_MS, source: 'default', policyId: null });
expect(count * CAPTURE_MS * (retriesForFiles([file]) + 1) + 120000).toBeLessThanOrEqual(budget.timeoutMs);
}
});
test('F9 manifest rejects skipped, missing and hollow actor coverage', () => {
const manifest = buildRunManifest({ tier: 'gate', profile: 'pr', sliceCount: 1,
evalsAll: false, env: {}, changedFiles: ['freeze/bin/freeze-state.sh'], discovered: files });
expect(manifest.entries.filter(entry => entry.status === 'planned').map(entry => entry.file).sort()).toEqual(files);
const result: SliceResult = {
version: 1, tier: 'gate', profile: 'pr', selection: manifest.selection, sliceIndex: 1, sliceCount: 1,
outcomes: manifest.entries.map(entry => ({ files: [entry.file], status: 'passed', exitCode: 0, elapsedMs: 1,
executedTests: expectedPrCaseCount(entry.file, manifest.selection!), skippedTests: 0,
...(entry.budget ? { budget: entry.budget } : {}),
})),
};
expect(verifySliceResults(manifest, [result]).ok).toBe(true);
for (const mutation of ['skip', 'empty', 'missing']) {
const invalid = structuredClone(result);
if (mutation === 'skip') invalid.outcomes[1].skippedTests = 1;
if (mutation === 'empty') invalid.outcomes[1].executedTests = 0;
if (mutation === 'missing') invalid.outcomes.pop();
expect(verifySliceResults(manifest, [invalid]).ok).toBe(false);
}
const hollow = { ...result.outcomes[1], executedTests: 0, skippedTests: 2, shard: 1, groupPid: null };
expect(applyHollowShardGuard([hollow], { evalsAll: false, requireExecuted: true })[0].status).toBe('passed-empty');
});
// Gate 39195396438 slice 14: each actor asked its single continue/abort question in its own
// words, with exactly a Continue and an Abort option.
describe('captured owner questions from run 39195396438', () => {
const declared = (id: 'investigate-owned-abort' | 'investigate-owned-ending-error') => {
const fixture = createBoundaryFixture(id);
const acquire = () => spawnSync('bash', ['-c', 'bash "$HOME/.claude/skills/gstack/freeze/bin/freeze-state.sh" acquire "src"'],
{ cwd: fixture.repo, env: fixture.env, encoding: 'utf8', timeout: 10000 });
const ask = (input: Record<string, unknown>) => fixture.canUseTool('AskUserQuestion', input, { signal: new AbortController().signal, toolUseID: 'captured' });
const [first, second] = capturedQuestions.cases[id] as Array<Record<string, any>>;
return { fixture, acquire, ask, first: structuredClone(first), second: structuredClone(second),
close: () => fs.rmSync(fixture.root, { recursive: true, force: true }) };
};
for (const [id, answer] of [['investigate-owned-abort', 'Abort'], ['investigate-owned-ending-error', 'Continue']] as const)
test(`${id}: the captured question is answered once and the re-ask is refused`, async () => {
const f = declared(id);
try {
expect(f.acquire().status).toBe(0);
const decision = await f.ask(f.first);
expect(decision.behavior).toBe('allow');
if (decision.behavior !== 'allow') return;
expect(decision.updatedInput!.answers).toEqual({ [f.first.questions[0].question]: answer });
const hook = await f.fixture.preToolUse({ hook_event_name: 'PreToolUse', session_id: 'captured', transcript_path: path.join(f.fixture.root, 'transcript'),
cwd: f.fixture.repo, tool_name: 'AskUserQuestion', tool_input: f.second, tool_use_id: 'second' }, 'second', { signal: new AbortController().signal });
expect((hook as any).hookSpecificOutput.permissionDecision).toBe('deny');
expect((await f.ask(f.second)).behavior).toBe('deny');
} finally { f.close(); }
});
test.each(['before acquisition', 'no abort choice', 'three options', 'swapped meanings'])('%s is still refused', async shape => {
const f = declared('investigate-owned-abort');
try {
if (shape !== 'before acquisition') expect(f.acquire().status).toBe(0);
const options = f.first.questions[0].options;
if (shape === 'no abort choice') options[1] = { label: 'Proceed later', description: 'Change the value after the verifier' };
if (shape === 'three options') options.push({ label: 'Skip', description: 'Leave the lock in place' });
if (shape === 'swapped meanings') [options[0].description, options[1].description] = [options[1].description, options[0].description];
expect((await f.ask(f.first)).behavior).toBe('deny');
} finally { f.close(); }
});
});
// Final responses of every Opus 5.5 investigate-owned-* gate record. Opus said "the
// verifier couldn't run"; the old /error|unavailable|cannot|could not|unable/ matched
// only the quoted FIXTURE_ENDING_ERROR output or "There were no cleanup errors".
describe('ending-error acknowledgment (captured final responses)', () => {
const finals = (id: string) => capturedFinals.finals.filter(entry => entry.case === id);
const old = /error|unavailable|cannot|could not|unable/i;
const own = (text: string) => text.replace(/`[^`]*`/g, '');
test('every captured ending-error response that reached the verifier acknowledges it in its own words', () => {
const reached = finals('investigate-owned-ending-error').filter(entry => entry.run !== '39195396438');
expect(reached).toHaveLength(6);
for (const entry of reached) expect(acknowledgesEndingError(own(entry.finalText)), entry.run).toBe(true);
expect(reached.filter(entry => !old.test(own(entry.finalText).replace(/\bno cleanup errors\b/gi, ''))).map(entry => entry.run).sort())
.toEqual(['39157502890', '39166935531', '39173752116', '39181697029', '39199692993']);
});
test('completion, abort and the run that never reached the verifier do not count', () => {
const absent = [...finals('investigate-owned-completion'), ...finals('investigate-owned-abort'),
...finals('investigate-owned-ending-error').filter(entry => entry.run === '39195396438')];
expect(absent).toHaveLength(15);
for (const entry of absent) expect(acknowledgesEndingError(entry.finalText), `${entry.case} ${entry.run}`).toBe(false);
for (const text of ['The fix is verified. Lock released with no errors.', 'The fix is complete and verification succeeded.',
'You chose to abort, so I made no edit and did not run the verifier. There were no cleanup errors.'])
expect(acknowledgesEndingError(text), text).toBe(false);
});
test('the registered actor credits the captured response and refuses a completion claim', async () => {
const captured = finals('investigate-owned-ending-error').find(entry => entry.run === '39166935531')!.finalText;
const accepted: EvalTestEntry[] = [];
await runBoundaryActor('investigate-owned-ending-error', entry => accepted.push(entry), protocolControl('investigate-owned-ending-error', { finalText: captured }).provider);
expect(accepted[0].passed).toBe(true);
const refused: EvalTestEntry[] = [];
const completion = finals('investigate-owned-completion').find(entry => entry.run === '39199692993')!.finalText;
await expect(runBoundaryActor('investigate-owned-ending-error', entry => refused.push(entry), protocolControl('investigate-owned-ending-error', { finalText: completion }).provider)).rejects.toThrow(/ending error not acknowledged/);
expect(refused[0].passed).toBe(false);
});
});