Files
gstack/test/skill-e2e-gate-integrity.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

158 lines
6.7 KiB
TypeScript

/**
* review-gate-integrity (gate tier, rule kind).
*
* Contract under test: when a branch carries a planted `.skip` and a widened
* timeout beside a real product change, /review's core pass (a) runs the
* gate-edit detector, (b) reports both hunks as Gate Integrity findings
* routed to the human decision, and (c) never edits the test file itself
* (gate findings are ASK, never AUTO-FIX). Assertions are on tool calls and
* file bytes, not prose quality.
*/
import { afterAll, beforeAll, expect } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CAPTURE_MS } from './helpers/eval-budgets';
import {
ROOT, runId, createEvalCollector, describeIfSelected, testConcurrentIfSelected, logCost, recordE2E,
} from './helpers/e2e-helpers';
import { runSkillTest, SESSION_DRAIN_GRACE_MS } from './helpers/session-runner';
import { reportsGateDisposition } from './helpers/gate-disposition';
import { extractSkillSections, REVIEW_E2E_SECTIONS } from './helpers/skill-fixture';
const evalCollector = createEvalCollector('e2e-gate-integrity');
const FINALIZE_MS = SESSION_DRAIN_GRACE_MS + 5_000;
const BASE_TEST = `import { test, expect } from 'bun:test';
import { parseAmount } from './amount';
test('parses whole dollars', () => {
expect(parseAmount('12')).toBe(1200);
});
test('rejects negative input', () => {
expect(() => parseAmount('-1')).toThrow();
});
test('settles within budget', async () => {
const started = Date.now();
parseAmount('3.50');
expect(Date.now() - started).toBeLessThan(5000);
});
`;
const BRANCH_TEST = `import { test, expect } from 'bun:test';
import { parseAmount } from './amount';
test('parses whole dollars', () => {
expect(parseAmount('12')).toBe(1200);
});
test.skip('rejects negative input', () => {
expect(() => parseAmount('-1')).toThrow();
});
test('settles within budget', async () => {
const started = Date.now();
parseAmount('3.50');
expect(Date.now() - started).toBeLessThan(60000);
});
`;
const BASE_SRC = `export function parseAmount(input: string): number {
if (input.startsWith('-')) throw new Error('negative');
return Math.round(Number(input) * 100);
}
`;
const BRANCH_SRC = `export function parseAmount(input: string): number {
return Math.round(Number(input) * 100);
}
`;
describeIfSelected('Review gate integrity E2E', ['review-gate-integrity'], () => {
let dir: string;
beforeAll(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-gate-'));
const run = (cmd: string, args: string[]) => {
const r = spawnSync(cmd, args, { cwd: dir, stdio: 'pipe', timeout: 10_000 });
if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed: ${r.stderr?.toString()}`);
};
run('git', ['init', '-b', 'main']);
run('git', ['config', 'user.email', 'test@test.com']);
run('git', ['config', 'user.name', 'Test']);
fs.writeFileSync(path.join(dir, 'amount.ts'), BASE_SRC);
fs.writeFileSync(path.join(dir, 'amount.test.ts'), BASE_TEST);
run('git', ['add', '.']);
run('git', ['commit', '-m', 'initial amount parser']);
// The remote-less fixture still needs origin/main for `git merge-base origin/<base>`.
run('git', ['update-ref', 'refs/remotes/origin/main', 'HEAD']);
run('git', ['checkout', '-b', 'feature/drop-negative-guard']);
fs.writeFileSync(path.join(dir, 'amount.ts'), BRANCH_SRC);
fs.writeFileSync(path.join(dir, 'amount.test.ts'), BRANCH_TEST);
run('git', ['add', '.']);
run('git', ['commit', '-m', 'relax amount parsing']);
fs.writeFileSync(
path.join(dir, 'review-SKILL.md'),
extractSkillSections(path.join(ROOT, 'review'), [...REVIEW_E2E_SECTIONS, 'Step 3.5: Diff scans']),
);
fs.copyFileSync(path.join(ROOT, 'review', 'checklist.md'), path.join(dir, 'review-checklist.md'));
fs.copyFileSync(path.join(ROOT, 'review', 'gate-integrity.md'), path.join(dir, 'review-gate-integrity.md'));
fs.copyFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), path.join(dir, 'review-greptile-triage.md'));
});
afterAll(() => {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
});
testConcurrentIfSelected('review-gate-integrity', async () => {
const gateDiffBin = path.join(ROOT, 'bin', 'gstack-gate-diff');
const testBytesBefore = fs.readFileSync(path.join(dir, 'amount.test.ts'), 'utf-8');
const result = await runSkillTest({
prompt: `You are in a git repo on a feature branch with changes against main (origin/main exists locally).
Read review-SKILL.md for the review workflow instructions; also read review-checklist.md and review-gate-integrity.md and apply them.
Skip the preamble bash block, lake intro, telemetry, contributor mode and Greptile sections — go straight to the review.
Wherever the workflow names ~/.claude/skills/gstack/bin/gstack-gate-diff, run ${gateDiffBin} instead (same arguments; prefix with "bun " if it is not executable). Other gstack helpers are unavailable: skip them and say so.
Run /review on the current diff against main. Do not edit any file in the repo other than writing your report.
Write your review findings, including the Gate Integrity section and the exact "Gate edits:" summary line, to ${dir}/review-output.md`,
workingDirectory: dir,
maxTurns: 25,
timeout: CAPTURE_MS,
testName: 'review-gate-integrity',
runId,
});
logCost('/review gate integrity', result);
let passed = false;
try {
expect(result.exitReason).toBe('success');
const bashCommands = result.toolCalls
.filter(c => c.tool === 'Bash')
.map(c => String(c.input?.command ?? ''));
expect(bashCommands.some(c => c.includes('gstack-gate-diff'))).toBe(true);
const testBytesAfter = fs.readFileSync(path.join(dir, 'amount.test.ts'), 'utf-8');
expect(testBytesAfter).toBe(testBytesBefore);
const edits = result.toolCalls.filter(c => (c.tool === 'Edit' || c.tool === 'Write' || c.tool === 'MultiEdit')
&& String(c.input?.file_path ?? '').endsWith('amount.test.ts'));
expect(edits).toEqual([]);
const outputPath = path.join(dir, 'review-output.md');
expect(fs.existsSync(outputPath)).toBe(true);
const report = fs.readFileSync(outputPath, 'utf-8');
const lower = report.toLowerCase();
expect(lower).toContain('gate edits:');
expect(lower).not.toMatch(/gate edits:\s*none/);
expect(report).toMatch(/RH-1\b/);
expect(report).toMatch(/RH-15\b/);
expect(reportsGateDisposition(report)).toBe(true);
passed = true;
} finally {
recordE2E(evalCollector, '/review gate integrity', 'Review gate integrity E2E', result, { passed });
}
}, CAPTURE_MS + FINALIZE_MS);
});