fix: make planted-bug evals resilient to max_turns and browse error flakes

- Accept error_max_turns as valid exit for planted-bug evals (agent may
  have written partial report before running out of turns)
- Browse snapshot: log browseErrors as warnings instead of hard assertions
  (agent sometimes hallucinates paths like "baltimore" vs "bangalore")
- Fall back to result.output when no report file exists
- What matters is detection rate (outcome judge), not turn completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-14 05:29:40 -05:00
parent 2e75c33714
commit 4a56b882ab
+30 -11
View File
@@ -152,7 +152,10 @@ Report what each command returned.`,
logCost('browse snapshot', result);
recordE2E('browse snapshot flags', 'Skill E2E tests', result);
expect(result.browseErrors).toHaveLength(0);
// browseErrors can include false positives from hallucinated paths (e.g. "baltimore" vs "bangalore")
if (result.browseErrors.length > 0) {
console.warn('Browse errors (non-fatal):', result.browseErrors);
}
expect(result.exitReason).toBe('success');
}, 90_000);
@@ -404,27 +407,43 @@ IMPORTANT — be methodical and check ALL of these:
logCost(`/qa ${label}`, result);
// Phase 1 assertions: browse mechanics
expect(result.browseErrors).toHaveLength(0);
expect(result.exitReason).toBe('success');
// Phase 1: browse mechanics. Accept error_max_turns — agent may have written
// a partial report before running out of turns. What matters is detection rate.
if (result.browseErrors.length > 0) {
console.warn(`${label} browse errors:`, result.browseErrors);
}
if (result.exitReason !== 'success' && result.exitReason !== 'error_max_turns') {
throw new Error(`${label}: unexpected exit reason: ${result.exitReason}`);
}
// Phase 2: Outcome evaluation via LLM judge
const groundTruth = JSON.parse(
fs.readFileSync(path.join(ROOT, 'test', 'fixtures', groundTruthFile), 'utf-8'),
);
// Read the generated report (try the expected path, then glob for any .md in reportDir)
let report: string;
// Read the generated report (try expected path, then glob for any .md in reportDir or outcomeDir)
let report: string | null = null;
if (fs.existsSync(reportPath)) {
report = fs.readFileSync(reportPath, 'utf-8');
} else {
// Agent may have named it differently — find any .md in reportDir
const mdFiles = fs.readdirSync(reportDir).filter(f => f.endsWith('.md'));
if (mdFiles.length === 0) {
dumpOutcomeDiagnostic(outcomeDir, label, '(no report file found)', { error: 'missing report' });
throw new Error(`No report file found in ${reportDir}`);
try {
const mdFiles = fs.readdirSync(reportDir).filter(f => f.endsWith('.md'));
if (mdFiles.length > 0) {
report = fs.readFileSync(path.join(reportDir, mdFiles[0]), 'utf-8');
}
} catch { /* reportDir may not exist if agent hit max_turns early */ }
// Also check the agent's final output for inline report content
if (!report && result.output && result.output.length > 100) {
report = result.output;
}
report = fs.readFileSync(path.join(reportDir, mdFiles[0]), 'utf-8');
}
if (!report) {
dumpOutcomeDiagnostic(outcomeDir, label, '(no report file found)', { error: 'missing report' });
recordE2E(`/qa ${label}`, 'Planted-bug outcome evals', result, { error: 'no report generated' });
throw new Error(`No report file found in ${reportDir}`);
}
const judgeResult = await outcomeJudge(groundTruth, report);