From 29d413c82501d673f45ffc32f22682452da670eb Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 09:47:50 +0000 Subject: [PATCH] fix: shadow reader dedupes by run folder, so two runs started in the same second both count (wave browse CI free-suite 16) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/autoplan-shadow-reader.ts | 5 +++-- test/autoplan-shadow-driver.test.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/autoplan-shadow-reader.ts b/lib/autoplan-shadow-reader.ts index a6a072766..93afffb98 100644 --- a/lib/autoplan-shadow-reader.ts +++ b/lib/autoplan-shadow-reader.ts @@ -33,7 +33,7 @@ export interface ShadowExport { rows: ShadowExportRow[]; counts: Record fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l) as Record); -/** Every answered revision of every run, deduped by run, gate_rev and item. */ +/** Every answered revision of every run, deduped by run folder, gate_rev and item (two runs can share a run id). */ export function exportShadow(dirs: string[]): ShadowExport { const rows = new Map(); const refused: ShadowExport['refused'] = []; @@ -41,6 +41,7 @@ export function exportShadow(dirs: string[]): ShadowExport { let state; try { state = readState(dir); } catch (e: any) { refused.push({ run: dir, code: e.code ?? 'RUN_NOT_INITIALIZED', detail: `${dir}: ${e.detail ?? e.message}` }); continue; } if (state.ceo_only) { refused.push({ run: state.run, code: 'CEO_ONLY_NO_GATE', detail: `${dir} is CEO-only run ${state.run}: no gate, nothing answered` }); continue; } + const key = fs.realpathSync(dir); const decisions = readDecisions(dir).filter(r => r.gate_rev >= 1); const journal = readJournal(dir); for (const rev of [...new Set(decisions.map(r => r.gate_rev))].sort((a, b) => a - b)) { @@ -65,7 +66,7 @@ export function exportShadow(dirs: string[]): ShadowExport { owner: { chosen: d.chosen ?? null, status: d.status, owner_mode: d.owner_mode ?? null, owner_named: d.owner_named ?? null, carried: !!(d.carried_from || d.carried_gate_from), carried_from: d.carried_from ?? d.carried_gate_from ?? null, answered_role: d.answered_role ?? null, answered_by: d.answered_by ?? null, rec_hash_at_answer: d.rec_hash ?? null, rec_hash_at_record: s?.rec_hash ?? null }, }; - rows.set(`${row.run}|${rev}|${item}`, row); + rows.set(`${key}|${rev}|${item}`, row); } } } diff --git a/test/autoplan-shadow-driver.test.ts b/test/autoplan-shadow-driver.test.ts index f4fb1b856..237f7fbad 100644 --- a/test/autoplan-shadow-driver.test.ts +++ b/test/autoplan-shadow-driver.test.ts @@ -391,6 +391,16 @@ describe('the switch through the driver, and the spend cap', async () => { }); describe('the reader over many runs', () => { + test('two runs that share a run id are both counted; the same folder listed twice is counted once', () => { + const copy = path.join(TMP, 'fail-copy', 'run'); + fs.cpSync(path.join(TMP, 'fail', 'run'), copy, { recursive: true }); + expect(readRun(copy).run).toBe(readRun(path.join(TMP, 'fail', 'run')).run); + const list = path.join(TMP, 'runs-same-id.txt'); + fs.writeFileSync(list, [path.join(TMP, 'fail', 'run'), copy, copy].join('\n') + '\n'); + const r = exported(['--runs', list], baseEnv({ GSTACK_CAPY: '' })); + expect(r.status).toBe(0); + expect(JSON.parse(r.stdout).counts.status).toEqual({ failed: 14 }); + }); test('missing, failed, malformed, retrospective and recorded rows; dedupe across a repeated entry; CEO_ONLY_NO_GATE', () => { const e = baseEnv({ GSTACK_CAPY: '' }); const { out: missing } = start('reader-missing', e);