mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-15 12:57:22 +01:00
fix: pre-landing review fixes (p1b applies finding fixes, api attempts reserve no host default, cross-file title pairing, paired severity counted)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
590a7db0df
commit
c300c2c3e5
12 files changed
+76
-26
No files matched your search
@@ -19,6 +19,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { ROOT, SMALL_PLAN, boundOf, recCarrying, resultOf } from './helpers/autoplan-run-fixture';
|
||||
import { capyRunner, phaseVia, readJsonl, readRunJson, writePlan } from './helpers/autoplan-capy';
|
||||
import { settledIds } from '../lib/autoplan-confirm';
|
||||
|
||||
const r = capyRunner();
|
||||
afterAll(() => r.cleanup());
|
||||
@@ -332,5 +333,30 @@ describe('open Critical and High findings reach the gate (GBRA-223 claim 3)', ()
|
||||
expect(decisions(g.out).find(d => d.label === item.id.toUpperCase())).toMatchObject({ chosen: 'b' });
|
||||
expect(readJsonl(path.join(g.out, 'tasks.jsonl')).some(t => /No kill switch/.test(t.title))).toBe(true);
|
||||
}
|
||||
const g = gated(KILL, NONE);
|
||||
const item = gate(g.out).items.find((i: any) => i.finding);
|
||||
answer(g.out, 1, 'p1a');
|
||||
expect(readJsonl(path.join(g.out, 'attempts.jsonl')).find(e => e.event === 'answered')).toMatchObject({ answer_class: 'overrides', overrides: [`${item.id}b`] });
|
||||
expect(settledIds({ out: g.out } as any).has(item.finding[0])).toBe(false);
|
||||
});
|
||||
|
||||
test('p1b applies the finding fixes its option text lists, and an applied finding is settled', () => {
|
||||
const g = gated(KILL, NONE);
|
||||
const item = gate(g.out).items.find((i: any) => i.finding);
|
||||
expect(gate(g.out).items.find((i: any) => i.id === 'p1').options[1].text).toContain(`${item.id}a`);
|
||||
expect(answer(g.out, 1, 'p1b').stdout).toMatch(/^APPLY: /m);
|
||||
expect(decisions(g.out).find(d => d.label === item.id.toUpperCase())).toMatchObject({ chosen: 'a' });
|
||||
expect(readJsonl(path.join(g.out, 'attempts.jsonl')).find(e => e.event === 'answered')).toMatchObject({ answer_class: 'plan-changes', changes: [`${item.id}a`] });
|
||||
expect(readRunJson(g.out).status).toBe('amend_pending');
|
||||
expect(settledIds({ out: g.out } as any).has(item.finding[0])).toBe(true);
|
||||
});
|
||||
|
||||
test('at the round cap, an outside Critical paired by file with an unrelated native Medium still counts as open', () => {
|
||||
const dir = path.join(r.host.base, `gj${++n}`);
|
||||
const out = path.join(dir, 'run');
|
||||
expect(r.ap(['next', '--out', out, '--plan', writePlan(dir), '--no-ui', '--max-rounds', '1']).status).toBe(0);
|
||||
phaseVia(r, out, 'ceo', { native: [{ id: 'F1', severity: 'Medium', title: 'rename the config key', file: 'src/resolver.ts' }], outside: [{ id: 'O1', severity: 'Critical', title: 'SQL injection in lookup query', file: 'src/resolver.ts' }] });
|
||||
const eng = phaseVia(r, out, 'eng', NONE).outside.stdout;
|
||||
expect(eng.split('\n').filter(l => l.startsWith('ESCALATE:'))).toEqual(['ESCALATE: evidence sufficient, decision needed (ceo: 1 High open)']);
|
||||
});
|
||||
});
|
||||
@@ -90,4 +90,10 @@ describe('plan reviews name a file without a line (GBRA-223 claim 1)', () => {
|
||||
expect(reconcile([lined], [row('outside', 1, { severity: 'High', title: 'retry loop has no bound', file: 'src/resolver.ts', line: 90 })]).rows[0]!.disposition).toBe('new');
|
||||
expect(reconcile([native], [row('outside', 1, { severity: 'High', title: 'CAP6:src/cli.ts:fix', file: 'src/cli.ts' })]).rows[0]!.disposition).toBe('new');
|
||||
});
|
||||
|
||||
test('control: the same CAP item and wording on two different files never pairs by title', () => {
|
||||
const a = row('native', 1, { severity: 'High', title: 'CAP6:bin/gstack-upgrade:recovery no retry bound on network failure', file: 'bin/gstack-upgrade' });
|
||||
const b = row('outside', 1, { severity: 'High', title: 'CAP6:lib/telemetry-sync.ts:recovery no retry bound on network failure', file: 'lib/telemetry-sync.ts' });
|
||||
expect(reconcile([a], [b]).rows[0]).toMatchObject({ disposition: 'new', native_id: null });
|
||||
});
|
||||
});
|
||||
@@ -31,13 +31,13 @@ beforeAll(() => {
|
||||
});
|
||||
afterAll(() => { server.stop(true); fs.rmSync(TMP, { recursive: true, force: true }); });
|
||||
|
||||
async function run(name: string) {
|
||||
async function run(name: string, extra: string[] = []) {
|
||||
const dir = path.join(TMP, name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'plan.md'), SMALL_PLAN);
|
||||
const out = path.join(dir, 'run');
|
||||
const base = `http://127.0.0.1:${server.port}`;
|
||||
const proc = Bun.spawn(['bun', AUTOPLAN, 'run', '--out', out, '--plan', path.join(dir, 'plan.md'), '--no-ui', '--no-dx', '--native-model', 'anthropic/claude-opus-5-5', '--outside-model', 'openai/gpt-6-astra', '--runner', 'api', '--native-runner', 'api'], {
|
||||
const proc = Bun.spawn(['bun', AUTOPLAN, 'run', '--out', out, '--plan', path.join(dir, 'plan.md'), '--no-ui', '--no-dx', '--native-model', 'anthropic/claude-opus-5-5', '--outside-model', 'openai/gpt-6-astra', '--runner', 'api', '--native-runner', 'api', ...extra], {
|
||||
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||
env: env({ ANTHROPIC_API_KEY: 'ak-test', OPENAI_API_KEY: 'sk-test', ANTHROPIC_BASE_URL: base, OPENAI_BASE_URL: `${base}/v1`, GSTACK_AUTOPLAN_CHECKPOINT_DIR: '', GSTACK_STATE_ROOT: path.join(TMP, 'state') }),
|
||||
});
|
||||
@@ -56,6 +56,15 @@ describe('gstack-autoplan run --native-runner api --runner api', () => {
|
||||
for (const phase of ['ceo', 'eng']) for (const voice of ['native', 'outside']) expect(r.state.phases[phase][voice]).toMatchObject({ status: 'completed', findings: 1 });
|
||||
}, 150_000);
|
||||
|
||||
test('a spend cap does not reserve the host-subagent default for api attempts', async () => {
|
||||
mode = 'ok';
|
||||
const r = await run('capped', ['--spend-cap', '8']);
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`GSTACK_RESULT: skill=autoplan status=gate_pending run=${r.out}`);
|
||||
expect(r.stdout).not.toMatch(/^ESTIMATE:/m);
|
||||
expect(r.stdout).toContain('GATE_SPEND: spent=0.00 worst_case=0.00 cap=8.00');
|
||||
}, 150_000);
|
||||
|
||||
test('a response stopped at the output limit is named truncated and interrupts the run', async () => {
|
||||
mode = 'cut';
|
||||
const r = await run('cut');
|
||||
|
||||
Reference in new issue
Block a user