mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
* feat(eval-plan): results --check refuses a results table without its no-gstack rows Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(model-panel): a <model>#no-gstack arm per model, identity-keyed cells, one campaign, results.md (GST-468) Each panel model also runs as <model>#no-gstack: the same fixture, ticket, tools, limits, route and reservation, with no skill folder and no routing line. The arm identity keys the ledger attempt, test name, row and summary; candidate folders are named after the fixture project and the seed commit has a neutral author. summarize reads one campaign (run id and PREREG.md sha256) and refuses a duplicate cell; suggestionLine reads the skill arms only, so the preregistered particle-1 line is unchanged. renderResults writes results.md, which passes gstack-eval-plan results --check against the new test/fixtures/model-panel/PREREG.md (supersedes GSTA-173 for later runs). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(arm-benchmark): no-gstack arm on a pinned model, 3 paired trials, both-order paired judge, spend ledger (GST-469) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(evals): no-gstack arm for the safety-rule and router evals (GST-470) GSTACK_SAFETY_ARM and GSTACK_ROUTER_ARM take a third value, none: the same fixture, tools and user request with no skill excerpt, rule text, step file or installed skill. Under none every behavioral check is recorded as an observation (none-arm-observations.jsonl) and never fails the case or writes contract-violations.jsonl; harness preconditions still assert. The four safety prompt builders (ship-evidence, codex-boundary, codex-consult, design-risk) and the router fixtures move into helpers; the rule, removed, current and before prompts stay byte-identical, proven against reference copies of main's code. pair-agent and ios-demo refuse none with the reason (NO_NONE_ARM). GSTACK_ROUTER_ARM now throws on an unknown value instead of silently running current. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: changelog fragment for the no-gstack arms in existing evals Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(arm-benchmark): arm-pair paired-judge calibration run (bar met 6/6, $7.22) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(paid-shards): keep loop-named panel and arm-benchmark cases selectable by --case The lanes moved each cell's testName into helpers, so the static detector no longer saw a computed registration and B4 failed for all eight cases. Same runtime test names; the paid runs are unaffected. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(tests): keep FREE_FIXTURES and the placeholder allowlist current for the no-gstack arms scripts/judge-calibration.ts is now an arm-benchmark paid touchfile, so it can no longer be a free consumer of test/fixtures/judge-calibration/** (CI free-suite 8). The eval-plan Step 5 fence names <prereg-path> and <results-path>; allowlist both as PATH placeholders (CI free-suite 19). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(eval-plan): cover a complete table after a partial one, --json output and the results usage error Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(eval-plan): keep the Step 7 coverage test as reviewed Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: name the changelog fragment after the branch slug Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: arm-benchmark project folder names and the arm-pair calibration corpus Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: keep the judge-calibration fixture README as reviewed Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(skills): describe eval-plan results --check 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>
88 lines
4.9 KiB
TypeScript
88 lines
4.9 KiB
TypeScript
/**
|
|
* Router failure routing (#3157, #3158). The gstack router checks a reported
|
|
* failure's cause before it routes: a user-supplied cause that one read-only
|
|
* check confirms is fixed directly; an unknown cause gets one bounded triage
|
|
* pass; anything still unverified goes to /investigate before any edit.
|
|
*
|
|
* Four fixtures, two per issue. The direct cases (verified cause, triage that
|
|
* resolves it) must fix the bug without /investigate and leave the suite
|
|
* green. The fallback cases (contradicted cause, unclear intermittent failure)
|
|
* are the negative controls: an edit before /investigate is a contract
|
|
* violation. Every case needs one to four read-only calls before it routes. The agent sees the rendered router section, sliced from
|
|
* SKILL.md; GSTACK_ROUTER_ARM=before slices the base branch's router instead.
|
|
* GSTACK_ROUTER_ARM=none is the descriptive no-gstack arm: the request alone,
|
|
* no installed skills, scored on plain metrics (suite green, edits before the
|
|
* first read, read-only calls before the first edit) recorded as observations.
|
|
* Fixtures and scoring live in test/helpers/failure-routing-fixture.ts.
|
|
*/
|
|
import { afterAll, expect } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { runSkillTest } from './helpers/session-runner';
|
|
import { expectContract } from './helpers/eval-store';
|
|
import {
|
|
runId, describeIfSelected, testConcurrentIfSelected, logCost, recordE2E,
|
|
createEvalCollector, finalizeEvalCollector,
|
|
} from './helpers/e2e-helpers';
|
|
import { CAPTURE_MS } from './helpers/eval-budgets';
|
|
import { bunIsolationArgs } from '../lib/bun-isolation';
|
|
import { FIXTURES, makeRepo, plainMetrics, routerArm, routerPrompt, routing, sourceSnapshot } from './helpers/failure-routing-fixture';
|
|
import { recordObservation } from './helpers/none-arm';
|
|
|
|
const collector = createEvalCollector('e2e-failure-routing');
|
|
const ARM = routerArm();
|
|
|
|
describeIfSelected('Router failure routing (#3157, #3158)', FIXTURES.map(f => f.name), () => {
|
|
const dirs: string[] = [];
|
|
afterAll(() => {
|
|
for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
finalizeEvalCollector(collector);
|
|
});
|
|
|
|
for (const fixture of FIXTURES) {
|
|
testConcurrentIfSelected(fixture.name, async () => {
|
|
const repo = makeRepo(fixture, ARM);
|
|
dirs.push(repo);
|
|
const sourceBefore = sourceSnapshot(repo, fixture);
|
|
const result = await runSkillTest({
|
|
prompt: routerPrompt(fixture, ARM),
|
|
workingDirectory: repo,
|
|
maxTurns: 6,
|
|
tools: ['Skill', 'Read', 'Grep', 'Glob', 'Edit', 'Bash'],
|
|
allowedTools: ['Skill', 'Read', 'Grep', 'Glob', 'Edit', 'Bash'],
|
|
timeout: CAPTURE_MS,
|
|
testName: fixture.name,
|
|
runId,
|
|
});
|
|
logCost(`router ${fixture.name} (${ARM})`, result);
|
|
const suite = spawnSync(process.execPath, [...bunIsolationArgs(), 'test'], { cwd: repo, encoding: 'utf8', timeout: 60_000 });
|
|
const green = suite.status === 0;
|
|
if (ARM === 'none') {
|
|
const p = plainMetrics(result.toolCalls);
|
|
const plain = `arm=none reads_before_edit=${p.readsBeforeEdit} edited_before_read=${p.editedBeforeRead} green=${green} tools: ${p.sequence}`;
|
|
recordE2E(collector, fixture.name, 'Router failure routing', result, { passed: true, output: plain });
|
|
recordObservation(fixture.name, green, `suite green after the run: ${plain}`);
|
|
recordObservation(fixture.name, !p.editedBeforeRead, `no edit before the first read-only call: ${plain}`);
|
|
recordObservation(fixture.name, p.readsBeforeEdit >= 1, `at least one read-only call before the first edit: ${plain}`);
|
|
return;
|
|
}
|
|
const r = routing(result);
|
|
const sourceChanged = sourceSnapshot(repo, fixture) !== sourceBefore;
|
|
const summary = `arm=${ARM} route=${r.investigated ? 'investigate' : 'direct'} reads=${r.readsBeforeRoute} green=${green} tools: ${r.sequence}`;
|
|
const bounded = r.readsBeforeRoute >= 1 && r.readsBeforeRoute <= 4;
|
|
const passed = bounded && (fixture.route === 'direct' ? !r.investigated && green : r.investigated && !r.editedFirst);
|
|
recordE2E(collector, fixture.name, 'Router failure routing', result, { passed, output: summary });
|
|
|
|
if (fixture.route === 'investigate') {
|
|
expectContract(!r.editedFirst && (r.investigated || !sourceChanged), `edited before /investigate on an unverified cause: ${summary}`, { collector, name: fixture.name });
|
|
expect(r.investigated, summary).toBe(true);
|
|
} else {
|
|
expect(r.investigated, summary).toBe(false);
|
|
expect(green, `suite still red after the direct fix: ${summary}\n${suite.stdout}${suite.stderr}`).toBe(true);
|
|
}
|
|
expect(r.readsBeforeRoute, `no read-only check before routing: ${summary}`).toBeGreaterThanOrEqual(1);
|
|
expect(r.readsBeforeRoute, `triage was not bounded: ${summary}`).toBeLessThanOrEqual(4);
|
|
}, CAPTURE_MS);
|
|
}
|
|
});
|