mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* fix(capy): the install trailer says behind=<n> and its fix, so a stale install shows in every relayed trailer (field list 2 item 13, #2378) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(capy): a lease records its holder's start identity, so a pid reused after a wake is reclaimed; a skipped session-start upgrade ends fail behind, never ok (GBRA-105, GBRA-106) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): stale needs proof (unreadable or malformed identities keep the lease), ps identity is timezone-independent, and reclaim never deletes a concurrent renewal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): enforced round cap and spend budget, deferred close, public export without machine paths, opt-in durable checkpoints (field list 2 W5-W7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): one directory lock for publish, prune and reclaim; reclaim rereads the record judged stale, so a renewal is never deleted or hidden Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): the directory lock names its holder; broken only when the holder is dead or its pid reused, unlock removes only its own lock, touch exits 3 under a live holder Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(lease): breakers serialize on .lock.break and reread the lock before removing it; gstack-skill-start reports a lease the held lock kept out Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(lease): name the double-crash limit of breaking a dead lock Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(capy): --for review-voice install profile, no-Drive bootstrap command, reviewer prompt trailer (field list 2 W11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit89bf556c11) * test: pin review-voice row, helpers row and the no-Drive fallback in existing install tests Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit7f6c70459a) * test: the DRIVER template names the reviewer trailer relay (pregate lane) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit2545d9a6d2) * test(context-budget): capy skill eager ceiling for the review-voice subagent paragraph (W11; ratchet protocol, capture value) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(eval-plan): prereg diagnostics dry run (W8) and .gstack/eval-models.json lint (W9), WIP Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commitb509096218) * feat(eval-plan): W8/W9 tests, docs/eval-models.md, troubleshooting anchors, /eval-plan steps Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit9335e45de2) * test(eval-plan): eval-plan/SKILL.md names only defined codes and accepted flags Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commita7a974d268) * test(review-voice): the launch line points at the verdict's bun-only directory (#3233's launch contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(third-party-actions): the Aside drive-offer detector matches the product name, not the English idiom (paid run 38988927989 tpa-absent-linux false positive) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): export refuses artifact paths outside the run before writing; a deferred close keeps earlier undecided findings; reopening CEO grants Eng its rerun (review of #3234) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan-budget): bind the reservation expectation to lib/autoplan-budget's default estimate (test-of-test ratchet) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): the docs observer accepts the spawned-prefixed gstack-skill-start --help read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): curate capy-review-voice out of the Windows subset (POSIX launcher contract) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for field list 2 wave A Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: stamp v1.91.115.0 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>
124 lines
7.4 KiB
TypeScript
124 lines
7.4 KiB
TypeScript
/**
|
|
* Field list 2, W8: `gstack-eval-plan prereg --check` runs a preregistration's
|
|
* declared summarizer on its declared synthetic fixture (only with
|
|
* --allow-repo-commands) and fails PREREG_DIAGNOSTIC_UNTESTED when a count
|
|
* differs from its preregistered definition, before the freeze commit. The D7
|
|
* shape (gbrain-evals #162): two diagnostic counts that did not match.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { checkPrereg, parsePreregDiagnostics, preregTemplate } from '../lib/eval-plan';
|
|
import { capyHost, type CapyHost } from './helpers/capy-host';
|
|
|
|
const BIN = path.resolve(import.meta.dir, '..', 'bin', 'gstack-eval-plan');
|
|
const FILLED = ['Bars', 'Arms', 'Decoys', 'Stop rules', 'Held-out exposure'].map(s => `## ${s}\n- filled in\n`).join('\n');
|
|
const SUMMARIZER = [
|
|
'touch ran.marker',
|
|
'r=$(grep -c \'"kind":"refusal"\' "$1")',
|
|
't=$(grep -c \'"kind":"truncated"\' "$1")',
|
|
'echo "summarizing $1"',
|
|
'printf \'{"refusals":%s,"truncated":%s}\\n\' "$r" "$t"',
|
|
].join('\n') + '\n';
|
|
const RECORDS = ['refusal', 'refusal', 'truncated', 'answer'].map(kind => JSON.stringify({ kind })).join('\n') + '\n';
|
|
|
|
function diagnostics(rows: Array<[string, string]>, o: { summarizer?: string; fixture?: string } = {}): string {
|
|
return `\n## Diagnostics\nsummarizer: ${o.summarizer ?? '`sh summarize.sh {fixture}`'}\nfixture: ${o.fixture ?? 'fixtures/synthetic records.jsonl'}\n\n| diagnostic | expected |\n|---|---|\n${rows.map(([n, c]) => `| ${n} | ${c} |`).join('\n')}\n`;
|
|
}
|
|
|
|
let host: CapyHost;
|
|
let repo: string;
|
|
beforeEach(() => {
|
|
host = capyHost();
|
|
repo = path.join(host.base, 'repo');
|
|
fs.mkdirSync(path.join(repo, 'fixtures'), { recursive: true });
|
|
const init = spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: repo, env: host.env, encoding: 'utf8', timeout: 30_000 });
|
|
if (init.status !== 0) throw new Error(init.stderr);
|
|
fs.writeFileSync(path.join(repo, 'summarize.sh'), SUMMARIZER);
|
|
fs.writeFileSync(path.join(repo, 'fixtures', 'synthetic records.jsonl'), RECORDS);
|
|
fs.mkdirSync(path.join(repo, 'evals'));
|
|
});
|
|
afterEach(() => host.cleanup());
|
|
|
|
function check(text: string, ...flags: string[]) {
|
|
const file = path.join(repo, 'evals', 'prereg.md');
|
|
fs.writeFileSync(file, text);
|
|
const r = spawnSync(BIN, ['prereg', '--check', file, ...flags], { cwd: host.base, env: host.env, encoding: 'utf8', timeout: 60_000 });
|
|
return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '', ran: fs.existsSync(path.join(repo, 'ran.marker')) };
|
|
}
|
|
|
|
describe('prereg --check: the Diagnostics section', () => {
|
|
test('the declared summarizer is listed, not run, without --allow-repo-commands (exit 3)', () => {
|
|
const r = check(FILLED + diagnostics([['refusals', '2'], ['truncated', '1']]));
|
|
expect(r.code).toBe(3);
|
|
expect(r.ran).toBe(false);
|
|
expect(r.out).toContain('PREREG: ok present=5/5 diagnostics=2 file=');
|
|
expect(r.out).toContain(`PREREG_COMMAND: summarizer="sh summarize.sh {fixture}" fixture=fixtures/synthetic records.jsonl cwd=${fs.realpathSync(repo)}`);
|
|
expect(r.err).toContain('(REPO_COMMANDS_NOT_ALLOWED)');
|
|
expect(r.out).not.toContain('PREREG_DIAGNOSTICS:');
|
|
});
|
|
|
|
test('with --allow-repo-commands, matching counts pass and bind the fixture by sha256; the path reaches the command quoted', () => {
|
|
const r = check(FILLED + diagnostics([['refusals', '2'], ['truncated', '1']]), '--allow-repo-commands');
|
|
expect(r.err).toBe('');
|
|
expect(r.code).toBe(0);
|
|
expect(r.ran).toBe(true);
|
|
const sha = createHash('sha256').update(RECORDS).digest('hex').slice(0, 12);
|
|
expect(r.out).toContain('PREREG_DIAGNOSTIC: refusals expected=2 got=2 ok\nPREREG_DIAGNOSTIC: truncated expected=1 got=1 ok\n');
|
|
expect(r.out).toContain(`PREREG_DIAGNOSTICS: ok n=2 fixture=fixtures/synthetic records.jsonl sha256=${sha}\n`);
|
|
});
|
|
|
|
test('the D7 shape: two counts off their definitions, a declared count the summarizer never reports and an undeclared one all fail PREREG_DIAGNOSTIC_UNTESTED', () => {
|
|
const r = check(FILLED + diagnostics([['refusals', '3'], ['truncated', '0'], ['empty_answers', '0']]), '--allow-repo-commands');
|
|
expect(r.code).toBe(1);
|
|
expect(r.out).toContain('PREREG_DIAGNOSTIC: refusals expected=3 got=2 mismatch');
|
|
expect(r.out).toContain('PREREG_DIAGNOSTIC: truncated expected=0 got=1 mismatch');
|
|
expect(r.out).toContain('PREREG_DIAGNOSTIC: empty_answers expected=0 got=none missing');
|
|
expect(r.out).toContain('PREREG_DIAGNOSTICS: untested n=3');
|
|
expect(r.err).toContain('refusals mismatch (expected 3, got 2); truncated mismatch (expected 0, got 1); empty_answers missing (expected 0, got none)');
|
|
expect(r.err.trim()).toEndWith('(PREREG_DIAGNOSTIC_UNTESTED)');
|
|
const undeclared = check(FILLED + diagnostics([['refusals', '2']]), '--allow-repo-commands');
|
|
expect(undeclared.code).toBe(1);
|
|
expect(undeclared.out).toContain('PREREG_DIAGNOSTIC: truncated expected=undeclared got=1 undeclared');
|
|
expect(undeclared.err).toContain('(PREREG_DIAGNOSTIC_UNTESTED)');
|
|
});
|
|
|
|
test('a failing summarizer, non-JSON output and a missing fixture are untested, never passed', () => {
|
|
fs.writeFileSync(path.join(repo, 'fail.sh'), 'echo "boom: no such column" >&2\nexit 2\n');
|
|
const failed = check(FILLED + diagnostics([['refusals', '2']], { summarizer: 'sh fail.sh {fixture}' }), '--allow-repo-commands');
|
|
expect(failed.code).toBe(1);
|
|
expect(failed.out).toContain('PREREG_DIAGNOSTICS: untested n=0');
|
|
expect(failed.err).toContain('summarizer exited 2: boom: no such column');
|
|
const prose = check(FILLED + diagnostics([['refusals', '2']], { summarizer: 'echo refusals=2 {fixture}' }), '--allow-repo-commands');
|
|
expect(prose.code).toBe(1);
|
|
expect(prose.err).toContain('not a JSON object of diagnostic counts');
|
|
const missing = check(FILLED + diagnostics([['refusals', '2']], { fixture: 'fixtures/nope.jsonl' }), '--allow-repo-commands');
|
|
expect(missing.code).toBe(1);
|
|
expect(missing.ran).toBe(false);
|
|
expect(missing.err).toContain('declared fixture fixtures/nope.jsonl is not a file');
|
|
});
|
|
|
|
test('an incomplete declaration is PREREG_INCOMPLETE and never runs, even with --allow-repo-commands', () => {
|
|
const r = check(FILLED + diagnostics([['refusals', 'two']], { summarizer: 'sh summarize.sh fixtures/x.jsonl' }), '--allow-repo-commands');
|
|
expect(r.code).toBe(1);
|
|
expect(r.ran).toBe(false);
|
|
expect(r.out).toContain('PREREG: incomplete present=5/5 diagnostics=invalid');
|
|
expect(r.err).toContain('diagnostics: row "| refusals | two |"');
|
|
expect(r.err).toContain('diagnostics: the summarizer command never names the fixture ({fixture})');
|
|
expect(r.err).toContain('diagnostics: no | diagnostic | expected | rows');
|
|
expect(r.err.trim()).toEndWith('(PREREG_INCOMPLETE)');
|
|
});
|
|
|
|
test('without a Diagnostics section the check is unchanged; the template mentions the section without adding a heading', () => {
|
|
const r = check(FILLED);
|
|
expect(r.code).toBe(0);
|
|
expect(r.out).toBe(`PREREG: ok present=5/5 file=${path.join(repo, 'evals', 'prereg.md')}\n`);
|
|
expect(parsePreregDiagnostics(FILLED)).toBeNull();
|
|
expect(parsePreregDiagnostics(FILLED + '\n## Diagnostics\n<!-- none -->\n')).toBeNull();
|
|
expect(preregTemplate()).toContain('## Diagnostics');
|
|
expect(checkPrereg(preregTemplate()).diagnostics).toBeNull();
|
|
});
|
|
});
|