@post-print/agent-test 1.1.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -66
- package/dist/cli.d.ts +2 -64
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -691
- package/dist/cli.js.map +1 -1
- package/dist/compare-scenario.d.ts +1 -1
- package/dist/compare-scenario.d.ts.map +1 -1
- package/dist/compare-scenario.js +72 -75
- package/dist/compare-scenario.js.map +1 -1
- package/dist/failures.d.ts +5 -1
- package/dist/failures.d.ts.map +1 -1
- package/dist/failures.js +1 -1
- package/dist/failures.js.map +1 -1
- package/dist/host-login.d.ts +1 -26
- package/dist/host-login.d.ts.map +1 -1
- package/dist/host-login.js +1 -92
- package/dist/host-login.js.map +1 -1
- package/dist/index.d.ts +6 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -26
- package/dist/index.js.map +1 -1
- package/dist/scenario-story.d.ts.map +1 -1
- package/dist/scenario-story.js +57 -39
- package/dist/scenario-story.js.map +1 -1
- package/dist/sdk/cli.d.ts +3 -0
- package/dist/sdk/cli.d.ts.map +1 -0
- package/dist/sdk/cli.js +20 -0
- package/dist/sdk/cli.js.map +1 -0
- package/dist/sdk/definitions.d.ts +22 -0
- package/dist/sdk/definitions.d.ts.map +1 -0
- package/dist/sdk/definitions.js +58 -0
- package/dist/sdk/definitions.js.map +1 -0
- package/dist/sdk/expect.d.ts +31 -0
- package/dist/sdk/expect.d.ts.map +1 -0
- package/dist/sdk/expect.js +68 -0
- package/dist/sdk/expect.js.map +1 -0
- package/dist/sdk/judge.d.ts +16 -0
- package/dist/sdk/judge.d.ts.map +1 -0
- package/dist/sdk/judge.js +82 -0
- package/dist/sdk/judge.js.map +1 -0
- package/dist/sdk/metrics.d.ts +3 -0
- package/dist/sdk/metrics.d.ts.map +1 -0
- package/dist/sdk/metrics.js +24 -0
- package/dist/sdk/metrics.js.map +1 -0
- package/dist/sdk/reporter.d.ts +12 -0
- package/dist/sdk/reporter.d.ts.map +1 -0
- package/dist/sdk/reporter.js +58 -0
- package/dist/sdk/reporter.js.map +1 -0
- package/dist/sdk/runtime.d.ts +33 -0
- package/dist/sdk/runtime.d.ts.map +1 -0
- package/dist/sdk/runtime.js +188 -0
- package/dist/sdk/runtime.js.map +1 -0
- package/dist/sdk/test.d.ts +27 -0
- package/dist/sdk/test.d.ts.map +1 -0
- package/dist/sdk/test.js +92 -0
- package/dist/sdk/test.js.map +1 -0
- package/dist/sdk/types.d.ts +75 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/types.js +2 -0
- package/dist/sdk/types.js.map +1 -0
- package/dist/sdk/usage.d.ts +10 -0
- package/dist/sdk/usage.d.ts.map +1 -0
- package/dist/sdk/usage.js +13 -0
- package/dist/sdk/usage.js.map +1 -0
- package/dist/sdk/viewer-results.d.ts +43 -0
- package/dist/sdk/viewer-results.d.ts.map +1 -0
- package/dist/sdk/viewer-results.js +126 -0
- package/dist/sdk/viewer-results.js.map +1 -0
- package/dist/sdk/viewer.d.ts +11 -0
- package/dist/sdk/viewer.d.ts.map +1 -0
- package/dist/sdk/viewer.js +160 -0
- package/dist/sdk/viewer.js.map +1 -0
- package/dist/sdk/workspace.d.ts +33 -0
- package/dist/sdk/workspace.d.ts.map +1 -0
- package/dist/sdk/workspace.js +100 -0
- package/dist/sdk/workspace.js.map +1 -0
- package/dist/types.d.ts +4 -40
- package/dist/types.d.ts.map +1 -1
- package/dist/viewer/catalog.d.ts +2 -8
- package/dist/viewer/catalog.d.ts.map +1 -1
- package/dist/viewer/catalog.js +23 -128
- package/dist/viewer/catalog.js.map +1 -1
- package/dist/viewer/client.d.ts.map +1 -1
- package/dist/viewer/client.js +12 -12
- package/dist/viewer/client.js.map +1 -1
- package/dist/viewer/run-controller.d.ts +4 -2
- package/dist/viewer/run-controller.d.ts.map +1 -1
- package/dist/viewer/run-controller.js +297 -248
- package/dist/viewer/run-controller.js.map +1 -1
- package/dist/viewer/server.d.ts +4 -3
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +149 -112
- package/dist/viewer/server.js.map +1 -1
- package/dist/viewer/styles.d.ts +2 -0
- package/dist/viewer/styles.d.ts.map +1 -0
- package/dist/viewer/styles.js +444 -0
- package/dist/viewer/styles.js.map +1 -0
- package/dist/worker-pool.d.ts +5 -1
- package/dist/worker-pool.d.ts.map +1 -1
- package/dist/worker-pool.js +1 -1
- package/dist/worker-pool.js.map +1 -1
- package/package.json +7 -7
- package/dist/check.d.ts +0 -38
- package/dist/check.d.ts.map +0 -1
- package/dist/check.js +0 -169
- package/dist/check.js.map +0 -1
- package/dist/command-allowlist.d.ts +0 -9
- package/dist/command-allowlist.d.ts.map +0 -1
- package/dist/command-allowlist.js +0 -29
- package/dist/command-allowlist.js.map +0 -1
- package/dist/debug-bundle.d.ts +0 -81
- package/dist/debug-bundle.d.ts.map +0 -1
- package/dist/debug-bundle.js +0 -392
- package/dist/debug-bundle.js.map +0 -1
- package/dist/discover-suites.d.ts +0 -2
- package/dist/discover-suites.d.ts.map +0 -1
- package/dist/discover-suites.js +0 -19
- package/dist/discover-suites.js.map +0 -1
- package/dist/doctor.d.ts +0 -24
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js +0 -272
- package/dist/doctor.js.map +0 -1
- package/dist/expect.d.ts +0 -59
- package/dist/expect.d.ts.map +0 -1
- package/dist/expect.js +0 -518
- package/dist/expect.js.map +0 -1
- package/dist/host-log.d.ts +0 -17
- package/dist/host-log.d.ts.map +0 -1
- package/dist/host-log.js +0 -154
- package/dist/host-log.js.map +0 -1
- package/dist/hosts.d.ts +0 -22
- package/dist/hosts.d.ts.map +0 -1
- package/dist/hosts.js +0 -84
- package/dist/hosts.js.map +0 -1
- package/dist/html-report.d.ts +0 -18
- package/dist/html-report.d.ts.map +0 -1
- package/dist/html-report.js +0 -1276
- package/dist/html-report.js.map +0 -1
- package/dist/live-isolation.d.ts +0 -82
- package/dist/live-isolation.d.ts.map +0 -1
- package/dist/live-isolation.js +0 -399
- package/dist/live-isolation.js.map +0 -1
- package/dist/live-timeout.d.ts +0 -11
- package/dist/live-timeout.d.ts.map +0 -1
- package/dist/live-timeout.js +0 -29
- package/dist/live-timeout.js.map +0 -1
- package/dist/load-adapters.d.ts +0 -13
- package/dist/load-adapters.d.ts.map +0 -1
- package/dist/load-adapters.js +0 -76
- package/dist/load-adapters.js.map +0 -1
- package/dist/load-suite.d.ts +0 -22
- package/dist/load-suite.d.ts.map +0 -1
- package/dist/load-suite.js +0 -154
- package/dist/load-suite.js.map +0 -1
- package/dist/mcp-config.d.ts +0 -13
- package/dist/mcp-config.d.ts.map +0 -1
- package/dist/mcp-config.js +0 -121
- package/dist/mcp-config.js.map +0 -1
- package/dist/preflight.d.ts +0 -3
- package/dist/preflight.d.ts.map +0 -1
- package/dist/preflight.js +0 -17
- package/dist/preflight.js.map +0 -1
- package/dist/progress.d.ts +0 -24
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js +0 -111
- package/dist/progress.js.map +0 -1
- package/dist/record-trace.d.ts +0 -64
- package/dist/record-trace.d.ts.map +0 -1
- package/dist/record-trace.js +0 -124
- package/dist/record-trace.js.map +0 -1
- package/dist/report-preview-server.d.ts +0 -2
- package/dist/report-preview-server.d.ts.map +0 -1
- package/dist/report-preview-server.js +0 -24
- package/dist/report-preview-server.js.map +0 -1
- package/dist/report-preview.d.ts +0 -35
- package/dist/report-preview.d.ts.map +0 -1
- package/dist/report-preview.js +0 -205
- package/dist/report-preview.js.map +0 -1
- package/dist/run-suite.d.ts +0 -139
- package/dist/run-suite.d.ts.map +0 -1
- package/dist/run-suite.js +0 -1669
- package/dist/run-suite.js.map +0 -1
- package/dist/scenario-finalizer.d.ts +0 -27
- package/dist/scenario-finalizer.d.ts.map +0 -1
- package/dist/scenario-finalizer.js +0 -39
- package/dist/scenario-finalizer.js.map +0 -1
- package/dist/scenario-retry.d.ts +0 -15
- package/dist/scenario-retry.d.ts.map +0 -1
- package/dist/scenario-retry.js +0 -52
- package/dist/scenario-retry.js.map +0 -1
- package/dist/scenario-seed.d.ts +0 -13
- package/dist/scenario-seed.d.ts.map +0 -1
- package/dist/scenario-seed.js +0 -72
- package/dist/scenario-seed.js.map +0 -1
- package/dist/suite-summary.d.ts +0 -16
- package/dist/suite-summary.d.ts.map +0 -1
- package/dist/suite-summary.js +0 -218
- package/dist/suite-summary.js.map +0 -1
- package/dist/theme.d.ts +0 -96
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -446
- package/dist/theme.js.map +0 -1
- package/dist/validate-seeds.d.ts +0 -20
- package/dist/validate-seeds.d.ts.map +0 -1
- package/dist/validate-seeds.js +0 -72
- package/dist/validate-seeds.js.map +0 -1
- package/dist/validate-suite.d.ts +0 -24
- package/dist/validate-suite.d.ts.map +0 -1
- package/dist/validate-suite.js +0 -542
- package/dist/validate-suite.js.map +0 -1
- package/dist/viewer/cli.d.ts +0 -17
- package/dist/viewer/cli.d.ts.map +0 -1
- package/dist/viewer/cli.js +0 -38
- package/dist/viewer/cli.js.map +0 -1
- package/dist/viewer/compare-metrics.d.ts +0 -30
- package/dist/viewer/compare-metrics.d.ts.map +0 -1
- package/dist/viewer/compare-metrics.js +0 -101
- package/dist/viewer/compare-metrics.js.map +0 -1
- package/dist/viewer/context-files.d.ts +0 -8
- package/dist/viewer/context-files.d.ts.map +0 -1
- package/dist/viewer/context-files.js +0 -57
- package/dist/viewer/context-files.js.map +0 -1
- package/dist/viewer/emit.d.ts +0 -7
- package/dist/viewer/emit.d.ts.map +0 -1
- package/dist/viewer/emit.js +0 -25
- package/dist/viewer/emit.js.map +0 -1
- package/dist/viewer/live-runner.d.ts +0 -27
- package/dist/viewer/live-runner.d.ts.map +0 -1
- package/dist/viewer/live-runner.js +0 -166
- package/dist/viewer/live-runner.js.map +0 -1
- package/dist/viewer/session-server.d.ts +0 -2
- package/dist/viewer/session-server.d.ts.map +0 -1
- package/dist/viewer/session-server.js +0 -71
- package/dist/viewer/session-server.js.map +0 -1
- package/dist/warnings.d.ts +0 -3
- package/dist/warnings.d.ts.map +0 -1
- package/dist/warnings.js +0 -13
- package/dist/warnings.js.map +0 -1
package/dist/run-suite.js
DELETED
|
@@ -1,1669 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { buildHostPrompt, cancelActiveClaudeRun, cancelActiveCursorRun, cancelActiveOpenaiRun, captureWorkingTreeStatus, createReadOnlyWorkspaceSnapshot, createSealedWorkspace, defaultSealedOverlayPaths, enrichTrace, filterWorkingTreeLeaks, findWorkingTreeLeak, formatWorkingTreeLeak, getProcessAuthMode, isBuiltinAgentHost, judgeCompareTraces, judgeTrace, loadContext, loadUnifiedDiffPaths, mergeMcpServers, parseScenarioWorkspace, partitionSeedCollateralLeaks, porcelainPathsFromLines, resolveAllowUserSkills, resolveHarnessArtifactIgnoreRoots, restoreWorkingTreePaths, runAgent, setProcessAuthMode, skillInvokeJudgeCriteria, skillPathsFromSetting, sumUsageParts, toolPathsOutsideWorkspace, traceEditsOutsideWorktree, traceHasUserInputTool, } from "@post-print/agent-harness";
|
|
5
|
-
import { applyCompareArm, applySidecarCompareDurations, assertCompareGates, attachCompareStoryResults, buildCompareResult, compareArmDescription, compareArmLabel, compareArmTokens, compareArmTurns, compareResultArms, compareStoryFields, evaluateCompareGates, finalizeCompareGates, finalizeCompareOutcome, plainDescription, prefixCompareFailures, requireCompareArm, resolveCompareArms, } from "./compare-scenario.js";
|
|
6
|
-
import { collectDebugEnvironment, getDebugBundleDir, writeDebugBundle } from "./debug-bundle.js";
|
|
7
|
-
import { discoverSuites } from "./discover-suites.js";
|
|
8
|
-
import { assertRubric } from "./expect.js";
|
|
9
|
-
import { assertionFailure } from "./failures.js";
|
|
10
|
-
import { resolveSuiteHosts, scenarioRunsOnHost, uniqueHosts } from "./hosts.js";
|
|
11
|
-
import { failuresForLiveSubprocessExit, killActiveLiveChildren, liveScenarioIsolationEnabled, parentScenarioCounters, spawnLiveScenario, } from "./live-isolation.js";
|
|
12
|
-
import { resolveLiveTimeoutMs } from "./live-timeout.js";
|
|
13
|
-
import { loadSuiteFile } from "./load-suite.js";
|
|
14
|
-
import { bindMcpServersToCaller, mcpStdioScriptPaths } from "./mcp-config.js";
|
|
15
|
-
import { formatDuration, logLive, logPhase, logProgress, logVerdict, refreshHeartbeat, withHeartbeat, } from "./progress.js";
|
|
16
|
-
import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingRootOverride, getLiveStagingSessionRoot, getStagingAgentStartPath, getStagingResultPath, getStagingTracePath, loadStagingResult, loadStagingTrace, recordTrace, resolveRecordingPath, setLiveStagingRootOverride, writeAgentStartMarker, writeStagingResult, } from "./record-trace.js";
|
|
17
|
-
import { finalizeScenarioResult } from "./scenario-finalizer.js";
|
|
18
|
-
import { resolveScenarioRetryMaxAttempts, shouldRetryAnnounceStopFlake } from "./scenario-retry.js";
|
|
19
|
-
import { captureCallerHead, restoreCallerHeadIfSeedCommit, seedScenarioWorktree, } from "./scenario-seed.js";
|
|
20
|
-
import { buildScenarioStory, pathFromArgs, quoteExcerpt } from "./scenario-story.js";
|
|
21
|
-
import { totalTokensFromScenarioUsage } from "./scenario-usage.js";
|
|
22
|
-
import { summarizeReportResults } from "./suite-summary.js";
|
|
23
|
-
import { theme } from "./theme.js";
|
|
24
|
-
import { validateSuiteFile } from "./validate-suite.js";
|
|
25
|
-
import { viewerContextFiles } from "./viewer/context-files.js";
|
|
26
|
-
import { emitViewerEvent } from "./viewer/emit.js";
|
|
27
|
-
import { DEFAULT_CLI_WORKERS, runWorkerPool } from "./worker-pool.js";
|
|
28
|
-
const require = createRequire(import.meta.url);
|
|
29
|
-
const packageVersion = require("../package.json").version;
|
|
30
|
-
let activeWorktreeCleanup;
|
|
31
|
-
let activeCallerHeadRestore;
|
|
32
|
-
let liveSignalHandlersRegistered = false;
|
|
33
|
-
function setCallerHeadRestore(cwd, snapshot) {
|
|
34
|
-
activeCallerHeadRestore = { cwd, snapshot };
|
|
35
|
-
}
|
|
36
|
-
function clearCallerHeadRestore() {
|
|
37
|
-
activeCallerHeadRestore = undefined;
|
|
38
|
-
}
|
|
39
|
-
async function restoreActiveCallerHead() {
|
|
40
|
-
if (!activeCallerHeadRestore) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const { cwd, snapshot } = activeCallerHeadRestore;
|
|
44
|
-
await restoreCallerHeadIfSeedCommit(cwd, snapshot);
|
|
45
|
-
clearCallerHeadRestore();
|
|
46
|
-
}
|
|
47
|
-
function isChildProcess() {
|
|
48
|
-
return process.env.AGENT_TEST_CHILD === "1";
|
|
49
|
-
}
|
|
50
|
-
function logScenarioDescription(description) {
|
|
51
|
-
const text = plainDescription(description);
|
|
52
|
-
if (!text) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
for (const line of theme.scenarioDescription(text)) {
|
|
56
|
-
logProgress(line);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function resolveMaxConversationTurns() {
|
|
60
|
-
const raw = process.env.AGENT_TEST_MAX_TURNS?.trim();
|
|
61
|
-
if (!raw) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
const parsed = Number(raw);
|
|
65
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
66
|
-
}
|
|
67
|
-
/** Parent process prints suite headers and final verdicts; children only print phases. */
|
|
68
|
-
export function shouldPrintSuiteChrome() {
|
|
69
|
-
return !isChildProcess();
|
|
70
|
-
}
|
|
71
|
-
/** Best-effort cancel + worktree cleanup when live runs are interrupted (Ctrl+C / SIGINT/SIGTERM). */
|
|
72
|
-
export function registerLiveRunHandlers() {
|
|
73
|
-
if (liveSignalHandlersRegistered) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
liveSignalHandlersRegistered = true;
|
|
77
|
-
const interrupt = (code) => {
|
|
78
|
-
killActiveLiveChildren();
|
|
79
|
-
cancelActiveCursorRun();
|
|
80
|
-
cancelActiveClaudeRun();
|
|
81
|
-
cancelActiveOpenaiRun();
|
|
82
|
-
const cleanup = activeWorktreeCleanup;
|
|
83
|
-
const headRestore = activeCallerHeadRestore;
|
|
84
|
-
if (!cleanup && !headRestore) {
|
|
85
|
-
process.exit(code);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
void Promise.resolve()
|
|
89
|
-
.then(async () => {
|
|
90
|
-
if (cleanup) {
|
|
91
|
-
await cleanup().catch(() => undefined);
|
|
92
|
-
}
|
|
93
|
-
if (headRestore) {
|
|
94
|
-
await restoreCallerHeadIfSeedCommit(headRestore.cwd, headRestore.snapshot).catch(() => undefined);
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.finally(() => process.exit(code));
|
|
98
|
-
};
|
|
99
|
-
process.on("SIGINT", () => interrupt(130));
|
|
100
|
-
process.on("SIGTERM", () => interrupt(143));
|
|
101
|
-
}
|
|
102
|
-
function releaseLiveMemory() {
|
|
103
|
-
const bunGc = globalThis.Bun?.gc;
|
|
104
|
-
if (typeof bunGc === "function") {
|
|
105
|
-
bunGc(true);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** Live-only mode hint from rubric — not part of the user scenario prompt. */
|
|
109
|
-
export function outputContractForRubric(rubric) {
|
|
110
|
-
if (rubric.routingBlock) {
|
|
111
|
-
return "hands-off";
|
|
112
|
-
}
|
|
113
|
-
if (rubric.handsOnRouting) {
|
|
114
|
-
return "hands-on";
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
function normalizeJudgeCriteria(judge) {
|
|
119
|
-
if (!judge?.length) {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
return judge.map((item, index) => {
|
|
123
|
-
if (typeof item === "string") {
|
|
124
|
-
return { id: `judge-${index}`, question: item };
|
|
125
|
-
}
|
|
126
|
-
return { id: item.id ?? `judge-${index}`, question: item.question };
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
/** Explicit judge questions plus skill-invoke questions for mustInvokeSkill. */
|
|
130
|
-
export function collectJudgeCriteria(rubric) {
|
|
131
|
-
return [
|
|
132
|
-
...normalizeJudgeCriteria(rubric.judge),
|
|
133
|
-
...skillInvokeJudgeCriteria(rubric.mustInvokeSkill ?? []),
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
/** Shared judge questions only. Skill-follow checks stay per-arm and deterministic. */
|
|
137
|
-
export function collectCompareJudgeCriteria(rubric) {
|
|
138
|
-
return normalizeJudgeCriteria(rubric.judge);
|
|
139
|
-
}
|
|
140
|
-
/** Judge auth and the judge call apply only when a rubric has judge work. */
|
|
141
|
-
export function judgeAuthRequired(judge, rubrics) {
|
|
142
|
-
return judge !== false && rubrics.some((rubric) => collectJudgeCriteria(rubric).length > 0);
|
|
143
|
-
}
|
|
144
|
-
/** Shared judge on compare. Skill-follow judge questions stay on non-compare scenarios. */
|
|
145
|
-
export function scenarioNeedsJudge(judge, scenario) {
|
|
146
|
-
if (judge === false) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
if (scenario.compare) {
|
|
150
|
-
return (collectCompareJudgeCriteria(scenario.rubric).length > 0 ||
|
|
151
|
-
(scenario.compare.judgeMetrics?.length ?? 0) > 0);
|
|
152
|
-
}
|
|
153
|
-
return collectJudgeCriteria(scenario.rubric).length > 0;
|
|
154
|
-
}
|
|
155
|
-
/** True when the selected live run will call the judge. */
|
|
156
|
-
export async function selectedRunNeedsJudge(options) {
|
|
157
|
-
if (options.judge === false) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
161
|
-
const filtered = options.filter
|
|
162
|
-
? suitePaths.filter((suitePath) => {
|
|
163
|
-
const suiteName = suiteNameFromPath(suitePath);
|
|
164
|
-
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
165
|
-
})
|
|
166
|
-
: suitePaths;
|
|
167
|
-
for (const suitePath of filtered) {
|
|
168
|
-
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
169
|
-
for (const scenario of suite.scenarios) {
|
|
170
|
-
if (scenario.skip) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (options.scenarioFilter && scenario.name !== options.scenarioFilter) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (scenarioNeedsJudge(true, scenario)) {
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
function questionForCriterion(criteria, id) {
|
|
184
|
-
return criteria.find((c) => c.id === id)?.question ?? id;
|
|
185
|
-
}
|
|
186
|
-
function toJudgeVerdictResults(trace, criteria, verdictsFromJudge) {
|
|
187
|
-
const source = verdictsFromJudge ?? trace.judgeVerdicts ?? [];
|
|
188
|
-
return source.map((verdict) => {
|
|
189
|
-
const extended = verdict;
|
|
190
|
-
return {
|
|
191
|
-
id: extended.id,
|
|
192
|
-
question: questionForCriterion(criteria, extended.id),
|
|
193
|
-
pass: extended.pass,
|
|
194
|
-
rationale: extended.rationale,
|
|
195
|
-
evidence: extended.evidence,
|
|
196
|
-
prompt: extended.prompt,
|
|
197
|
-
response: extended.response,
|
|
198
|
-
infraError: extended.infraError,
|
|
199
|
-
parseError: extended.parseError,
|
|
200
|
-
rawSdkStatus: extended.rawSdkStatus,
|
|
201
|
-
sdkError: extended.sdkError,
|
|
202
|
-
attempt: extended.attempt,
|
|
203
|
-
durationMs: extended.durationMs,
|
|
204
|
-
transcriptChars: extended.transcriptChars,
|
|
205
|
-
promptChars: extended.promptChars,
|
|
206
|
-
workspaceEvidence: extended.workspaceEvidence,
|
|
207
|
-
usage: extended.usage,
|
|
208
|
-
};
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
function rubricFailuresOnly(failures) {
|
|
212
|
-
return failures.filter((f) => !f.matcher.startsWith("judge"));
|
|
213
|
-
}
|
|
214
|
-
function isDebugEnabled(options) {
|
|
215
|
-
return (options?.debug === true ||
|
|
216
|
-
process.env.AGENT_TEST_DEBUG === "1" ||
|
|
217
|
-
process.env.AGENT_TEST_DEBUG === "true");
|
|
218
|
-
}
|
|
219
|
-
function emitScenarioVerdict(options) {
|
|
220
|
-
if (!shouldPrintSuiteChrome()) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
logVerdict(theme.scenarioVerdict({
|
|
224
|
-
passed: options.passed,
|
|
225
|
-
index: options.index,
|
|
226
|
-
total: options.total,
|
|
227
|
-
name: options.name,
|
|
228
|
-
durationMs: options.durationMs,
|
|
229
|
-
totalTokens: options.totalTokens,
|
|
230
|
-
judgeVerdicts: options.judgeVerdicts,
|
|
231
|
-
rubricFailures: rubricFailuresOnly(options.failures),
|
|
232
|
-
failureCategory: options.failures[0]?.category,
|
|
233
|
-
story: options.story,
|
|
234
|
-
debug: options.debug,
|
|
235
|
-
debugBundleDir: options.debugBundleDir,
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
async function maybeWriteDebugBundle(options) {
|
|
239
|
-
if (!options.debug || options.result.skipped) {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
if (!options.stagingSessionId) {
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
const dir = getDebugBundleDir(options.stagingSessionId, options.suiteName, options.scenario.name, getLiveStagingSessionRoot, options.host);
|
|
246
|
-
const cliPath = process.argv[1] ?? resolve(options.cwd, "node_modules/@post-print/agent-test/dist/cli.js");
|
|
247
|
-
try {
|
|
248
|
-
await writeDebugBundle({
|
|
249
|
-
dir,
|
|
250
|
-
result: options.result,
|
|
251
|
-
trace: options.trace ?? options.result.trace,
|
|
252
|
-
scenario: options.scenario,
|
|
253
|
-
environment: collectDebugEnvironment({
|
|
254
|
-
suite: options.suiteName,
|
|
255
|
-
scenario: options.scenario.name,
|
|
256
|
-
packageVersion,
|
|
257
|
-
host: options.host,
|
|
258
|
-
timeoutMs: options.timeoutMs,
|
|
259
|
-
worktree: options.worktree,
|
|
260
|
-
isolateLive: liveScenarioIsolationEnabled(),
|
|
261
|
-
}),
|
|
262
|
-
rerun: {
|
|
263
|
-
cliPath,
|
|
264
|
-
cwd: options.cwd,
|
|
265
|
-
suitesDir: options.suitesDir,
|
|
266
|
-
rubricsDir: options.rubricsDir,
|
|
267
|
-
suite: options.suiteName,
|
|
268
|
-
scenario: options.scenario.name,
|
|
269
|
-
host: options.host,
|
|
270
|
-
judge: options.judge,
|
|
271
|
-
worktree: options.worktree,
|
|
272
|
-
timeoutMs: options.timeoutMs,
|
|
273
|
-
noTimeout: options.timeoutMs === 0,
|
|
274
|
-
allowUserInput: options.allowUserInput,
|
|
275
|
-
// Match child-spawn resolution (buildLiveScenarioCommand): fall back
|
|
276
|
-
// to the process-global staging override so a parent rewrite of the
|
|
277
|
-
// bundle never drops --debug-dir for library callers that set the
|
|
278
|
-
// override without an explicit debugDir.
|
|
279
|
-
debugDir: options.debugDir ?? getLiveStagingRootOverride(),
|
|
280
|
-
keepRecordings: options.keepRecordings ?? true,
|
|
281
|
-
authMode: options.authMode,
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
-
console.warn(`agent-test: debug bundle write failed (${dir}): ${message}`);
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
if (shouldPrintSuiteChrome()) {
|
|
291
|
-
logPhase(theme.debugBundlePointer(join(dir, "transcript.md")), { last: true });
|
|
292
|
-
}
|
|
293
|
-
return dir;
|
|
294
|
-
}
|
|
295
|
-
export async function runSuite(options) {
|
|
296
|
-
const previousStagingRoot = getLiveStagingRootOverride();
|
|
297
|
-
if (options.debugDir !== undefined) {
|
|
298
|
-
setLiveStagingRootOverride(options.debugDir);
|
|
299
|
-
}
|
|
300
|
-
const ownsStagingSession = options.stagingSessionId === undefined;
|
|
301
|
-
const stagingSessionId = options.stagingSessionId ?? createLiveStagingSessionId();
|
|
302
|
-
try {
|
|
303
|
-
return await runSuiteBody({ ...options, stagingSessionId });
|
|
304
|
-
}
|
|
305
|
-
finally {
|
|
306
|
-
if (ownsStagingSession && !options.keepRecordings && !options.debug) {
|
|
307
|
-
await cleanupStagingSession(getLiveStagingSessionRoot(stagingSessionId)).catch(() => undefined);
|
|
308
|
-
}
|
|
309
|
-
if (options.debugDir !== undefined) {
|
|
310
|
-
setLiveStagingRootOverride(previousStagingRoot);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async function runSuiteBody(options) {
|
|
315
|
-
const suite = await loadSuiteFile(options.suitePath, { rubricsDir: options.rubricsDir });
|
|
316
|
-
const validationIssues = validateSuiteFile(options.suitePath, suite);
|
|
317
|
-
if (validationIssues.length > 0) {
|
|
318
|
-
const details = validationIssues
|
|
319
|
-
.map((issue) => `${issue.scenario ? `${issue.scenario} · ` : ""}${issue.field}: ${issue.message}`)
|
|
320
|
-
.join("\n");
|
|
321
|
-
throw new Error(`Invalid suite file ${options.suitePath}:\n${details}`);
|
|
322
|
-
}
|
|
323
|
-
const defaultHost = options.host ?? suite.defaults?.host ?? "cursor";
|
|
324
|
-
const results = [];
|
|
325
|
-
const selected = options.scenarioFilter
|
|
326
|
-
? suite.scenarios.filter((scenario) => scenario.name === options.scenarioFilter)
|
|
327
|
-
: suite.scenarios;
|
|
328
|
-
if (options.scenarioFilter && selected.length === 0) {
|
|
329
|
-
throw new Error(`Scenario not found: ${options.scenarioFilter}`);
|
|
330
|
-
}
|
|
331
|
-
const hostLocked = options.hostLocked === true;
|
|
332
|
-
const scenarios = hostLocked
|
|
333
|
-
? selected.filter((scenario) => scenarioRunsOnHost(scenario, defaultHost, true))
|
|
334
|
-
: selected;
|
|
335
|
-
if (hostLocked && options.scenarioFilter && selected.length > 0 && scenarios.length === 0) {
|
|
336
|
-
const pinned = selected[0]?.host;
|
|
337
|
-
throw new Error(`Scenario ${options.scenarioFilter} is pinned to ${pinned} (this run is ${defaultHost})`);
|
|
338
|
-
}
|
|
339
|
-
const filteredTotal = scenarios.length;
|
|
340
|
-
const parentCounters = parentScenarioCounters();
|
|
341
|
-
const displayTotal = parentCounters?.total ?? filteredTotal;
|
|
342
|
-
const isolateLive = liveScenarioIsolationEnabled() && !options.scenarioFilter && filteredTotal > 1;
|
|
343
|
-
const workers = Math.max(1, options.workers ?? DEFAULT_CLI_WORKERS);
|
|
344
|
-
if (shouldPrintSuiteChrome()) {
|
|
345
|
-
logProgress(`\n${theme.suiteHeader(suite.name, defaultHost, displayTotal)}`);
|
|
346
|
-
if (isolateLive) {
|
|
347
|
-
logProgress(` ${theme.isolationNote()}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
if (isolateLive) {
|
|
351
|
-
const isolateSlots = new Array(scenarios.length);
|
|
352
|
-
await runWorkerPool(scenarios.map((scenario, index) => ({ scenario, index })), workers, async ({ scenario, index }) => {
|
|
353
|
-
if (!scenario) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const scenarioIndex = parentCounters?.index ?? index + 1;
|
|
357
|
-
const scenarioTotal = displayTotal;
|
|
358
|
-
if (scenario.skip) {
|
|
359
|
-
const skipLabel = `[${scenarioIndex}/${scenarioTotal}] ${scenario.name}`;
|
|
360
|
-
logProgress(theme.skipped(skipLabel));
|
|
361
|
-
logScenarioDescription(scenario.description);
|
|
362
|
-
isolateSlots[index] = {
|
|
363
|
-
suite: suite.name,
|
|
364
|
-
scenario: scenario.name,
|
|
365
|
-
description: plainDescription(scenario.description),
|
|
366
|
-
prompt: scenario.prompt,
|
|
367
|
-
contextMode: scenario.contextMode ?? suite.defaults?.contextMode ?? "harness-preamble",
|
|
368
|
-
passed: true,
|
|
369
|
-
failures: [],
|
|
370
|
-
skipped: true,
|
|
371
|
-
durationMs: 0,
|
|
372
|
-
story: buildScenarioStory({
|
|
373
|
-
rubric: scenario.rubric,
|
|
374
|
-
passed: true,
|
|
375
|
-
skipped: true,
|
|
376
|
-
failures: [],
|
|
377
|
-
}),
|
|
378
|
-
};
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const started = performance.now();
|
|
382
|
-
const debug = isDebugEnabled(options);
|
|
383
|
-
const maxAttempts = !isChildProcess()
|
|
384
|
-
? resolveScenarioRetryMaxAttempts(options.scenarioRetries)
|
|
385
|
-
: 1;
|
|
386
|
-
let attempts = 0;
|
|
387
|
-
let failures = [];
|
|
388
|
-
let scenarioTrace;
|
|
389
|
-
let previousAttemptExitCode;
|
|
390
|
-
let childSidecar;
|
|
391
|
-
while (true) {
|
|
392
|
-
attempts++;
|
|
393
|
-
const spawned = await spawnLiveScenario({
|
|
394
|
-
cwd: options.cwd,
|
|
395
|
-
suiteName: suite.name,
|
|
396
|
-
scenarioName: scenario.name,
|
|
397
|
-
suitesDir: options.suitesDir ?? "agent-suites",
|
|
398
|
-
rubricsDir: options.rubricsDir,
|
|
399
|
-
suiteFilter: options.suiteFilter ?? suite.name,
|
|
400
|
-
stagingSessionId: options.stagingSessionId,
|
|
401
|
-
keepRecordings: options.keepRecordings,
|
|
402
|
-
worktree: options.worktree,
|
|
403
|
-
judge: options.judge,
|
|
404
|
-
host: defaultHost,
|
|
405
|
-
adapterModules: options.adapterModules,
|
|
406
|
-
scenarioIndex: index + 1,
|
|
407
|
-
scenarioTotal: filteredTotal,
|
|
408
|
-
timeoutMs: resolveLiveTimeoutMs(options.timeoutMs),
|
|
409
|
-
noTimeout: options.timeoutMs === 0,
|
|
410
|
-
allowUserInput: options.allowUserInput,
|
|
411
|
-
debug: options.debug,
|
|
412
|
-
debugDir: options.debugDir,
|
|
413
|
-
previousExitCode: previousAttemptExitCode,
|
|
414
|
-
authMode: options.authMode ?? getProcessAuthMode(),
|
|
415
|
-
});
|
|
416
|
-
previousAttemptExitCode = spawned.exitCode;
|
|
417
|
-
failures = [];
|
|
418
|
-
scenarioTrace = undefined;
|
|
419
|
-
childSidecar =
|
|
420
|
-
options.stagingSessionId !== undefined
|
|
421
|
-
? await loadStagingResult(getStagingResultPath(options.stagingSessionId, suite.name, scenario.name))
|
|
422
|
-
: undefined;
|
|
423
|
-
if (spawned.exitCode !== 0) {
|
|
424
|
-
failures.push(...failuresForLiveSubprocessExit(spawned.exitCode, childSidecar, spawned.stderr));
|
|
425
|
-
}
|
|
426
|
-
if (options.stagingSessionId) {
|
|
427
|
-
if (scenario.compare) {
|
|
428
|
-
const firstArm = resolveCompareArms(scenario.compare)[0];
|
|
429
|
-
if (firstArm) {
|
|
430
|
-
try {
|
|
431
|
-
scenarioTrace = await loadStagingTrace(getStagingTracePath(options.stagingSessionId, suite.name, scenario.name, firstArm.id));
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// Trace may be missing when the child crashed before recording.
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
const tracePath = getStagingTracePath(options.stagingSessionId, suite.name, scenario.name);
|
|
440
|
-
try {
|
|
441
|
-
scenarioTrace = await loadStagingTrace(tracePath);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
// Trace may be missing when the child crashed before recording.
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const canRetry = failures.length > 0 &&
|
|
449
|
-
attempts < maxAttempts &&
|
|
450
|
-
shouldRetryAnnounceStopFlake(failures, scenarioTrace);
|
|
451
|
-
if (canRetry) {
|
|
452
|
-
logPhase(theme.phase("retry", `${attempts}/${maxAttempts - 1}`));
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
let judgeVerdicts;
|
|
458
|
-
let compareResult;
|
|
459
|
-
const judgeHost = options.host ?? scenario.host ?? suite.defaults?.host ?? "cursor";
|
|
460
|
-
if (scenario.compare && options.stagingSessionId) {
|
|
461
|
-
compareResult = applySidecarCompareDurations(await loadCompareResultFromStaging(options.stagingSessionId, suite.name, scenario, childSidecar), childSidecar);
|
|
462
|
-
scenarioTrace = compareResultArms(compareResult)[0]?.trace ?? scenarioTrace;
|
|
463
|
-
}
|
|
464
|
-
if (compareResult && options.judge !== false && scenario.compare?.judgeMetrics?.length) {
|
|
465
|
-
for (const arm of compareResult.arms) {
|
|
466
|
-
if (!arm.trace)
|
|
467
|
-
continue;
|
|
468
|
-
try {
|
|
469
|
-
const judged = await runJudgeRubric(arm.trace, { judge: scenario.compare.judgeMetrics }, options.cwd, judgeHost);
|
|
470
|
-
arm.trace = judged.trace;
|
|
471
|
-
arm.judgeVerdicts = toJudgeVerdictResults(judged.trace, scenario.compare.judgeMetrics, judged.verdicts);
|
|
472
|
-
arm.failures = [...(arm.failures ?? []), ...judged.failures];
|
|
473
|
-
failures.push(...prefixCompareFailures(arm.label, judged.failures.filter((failure) => failure.category !== "rubric_miss")));
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to judge compare arm", "judge_infra"));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (compareResult) {
|
|
481
|
-
compareResult.gateResults = evaluateCompareGates(compareResult.gates, compareResult);
|
|
482
|
-
if (failures.every((failure) => failure.category === "rubric_miss")) {
|
|
483
|
-
failures.push(...assertCompareGates(compareResult.gates, compareResult));
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if ((options.judge !== false && failures.length === 0) ||
|
|
487
|
-
(options.judge !== false &&
|
|
488
|
-
scenario.compare &&
|
|
489
|
-
failures.every((failure) => failure.category === "rubric_miss"))) {
|
|
490
|
-
const compareTraces = compareResult ? compareResultArms(compareResult) : [];
|
|
491
|
-
if (scenario.compare &&
|
|
492
|
-
compareResult &&
|
|
493
|
-
compareTraces.length > 0 &&
|
|
494
|
-
compareTraces.every((arm) => arm.trace)) {
|
|
495
|
-
const criteria = collectCompareJudgeCriteria(scenario.rubric);
|
|
496
|
-
if (criteria.length > 0) {
|
|
497
|
-
releaseLiveMemory();
|
|
498
|
-
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
499
|
-
try {
|
|
500
|
-
const judged = await runCompareJudgeRubric(compareResult, scenario.rubric, options.cwd, judgeHost);
|
|
501
|
-
failures.push(...judged.failures);
|
|
502
|
-
const firstTrace = compareTraces[0]?.trace;
|
|
503
|
-
judgeVerdicts = toJudgeVerdictResults({
|
|
504
|
-
...(firstTrace ?? {
|
|
505
|
-
messages: [],
|
|
506
|
-
toolCalls: [],
|
|
507
|
-
shellCommands: [],
|
|
508
|
-
artifacts: {},
|
|
509
|
-
}),
|
|
510
|
-
judgeVerdicts: judged.verdicts,
|
|
511
|
-
}, criteria, judged.verdicts);
|
|
512
|
-
}
|
|
513
|
-
catch (error) {
|
|
514
|
-
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to judge compare traces", "judge_infra"));
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
else if (scenarioTrace && !scenario.compare) {
|
|
519
|
-
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
520
|
-
if (criteria.length > 0) {
|
|
521
|
-
releaseLiveMemory();
|
|
522
|
-
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
523
|
-
try {
|
|
524
|
-
const judged = await runJudgeRubric(scenarioTrace, scenario.rubric, options.cwd, judgeHost, undefined, childSidecar?.judgeWorkspace
|
|
525
|
-
? [
|
|
526
|
-
{
|
|
527
|
-
name: childSidecar.judgeWorkspace.name,
|
|
528
|
-
path: childSidecar.judgeWorkspace.path,
|
|
529
|
-
},
|
|
530
|
-
]
|
|
531
|
-
: undefined);
|
|
532
|
-
failures.push(...judged.failures);
|
|
533
|
-
scenarioTrace = judged.trace;
|
|
534
|
-
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
535
|
-
}
|
|
536
|
-
catch (error) {
|
|
537
|
-
failures.push(assertionFailure("judge", error instanceof Error
|
|
538
|
-
? error.message
|
|
539
|
-
: "failed to load staging trace for judge", "judge_infra"));
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (childSidecar?.judgeWorkspace) {
|
|
545
|
-
await rm(dirname(childSidecar.judgeWorkspace.path), { recursive: true, force: true });
|
|
546
|
-
}
|
|
547
|
-
const durationMs = Math.round(performance.now() - started);
|
|
548
|
-
const scenarioResult = finalizeScenarioResult({
|
|
549
|
-
suite: suite.name,
|
|
550
|
-
scenario,
|
|
551
|
-
contextMode: childSidecar?.contextMode ??
|
|
552
|
-
scenario.contextMode ??
|
|
553
|
-
suite.defaults?.contextMode ??
|
|
554
|
-
"harness-preamble",
|
|
555
|
-
contextFiles: childSidecar?.contextFiles,
|
|
556
|
-
hostInput: childSidecar?.hostInput,
|
|
557
|
-
failures,
|
|
558
|
-
durationMs,
|
|
559
|
-
attempts,
|
|
560
|
-
judgeVerdicts,
|
|
561
|
-
trace: scenarioTrace,
|
|
562
|
-
compare: compareResult,
|
|
563
|
-
storyCompare: compareResult
|
|
564
|
-
? attachCompareStoryResults(compareStoryFields(scenario), compareResult)
|
|
565
|
-
: undefined,
|
|
566
|
-
agentUsage: compareResult
|
|
567
|
-
? sumUsageParts(compareResultArms(compareResult).map((arm) => arm.trace?.usage))
|
|
568
|
-
: scenarioTrace?.usage,
|
|
569
|
-
});
|
|
570
|
-
const { passed, story } = scenarioResult;
|
|
571
|
-
const debugBundleDir = await maybeWriteDebugBundle({
|
|
572
|
-
debug,
|
|
573
|
-
cwd: options.cwd,
|
|
574
|
-
suitesDir: options.suitesDir ?? "agent-suites",
|
|
575
|
-
rubricsDir: options.rubricsDir,
|
|
576
|
-
stagingSessionId: options.stagingSessionId,
|
|
577
|
-
debugDir: options.debugDir,
|
|
578
|
-
suiteName: suite.name,
|
|
579
|
-
scenario,
|
|
580
|
-
host: defaultHost,
|
|
581
|
-
result: scenarioResult,
|
|
582
|
-
trace: scenarioTrace,
|
|
583
|
-
timeoutMs: options.timeoutMs,
|
|
584
|
-
worktree: options.worktree,
|
|
585
|
-
judge: options.judge,
|
|
586
|
-
allowUserInput: options.allowUserInput,
|
|
587
|
-
keepRecordings: options.keepRecordings,
|
|
588
|
-
authMode: options.authMode ?? getProcessAuthMode(),
|
|
589
|
-
});
|
|
590
|
-
scenarioResult.debugBundleDir = debugBundleDir;
|
|
591
|
-
emitScenarioVerdict({
|
|
592
|
-
passed,
|
|
593
|
-
index: index + 1,
|
|
594
|
-
total: filteredTotal,
|
|
595
|
-
name: scenario.name,
|
|
596
|
-
durationMs,
|
|
597
|
-
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, scenarioTrace?.usage),
|
|
598
|
-
judgeVerdicts,
|
|
599
|
-
failures,
|
|
600
|
-
story,
|
|
601
|
-
debug,
|
|
602
|
-
debugBundleDir,
|
|
603
|
-
});
|
|
604
|
-
isolateSlots[index] = scenarioResult;
|
|
605
|
-
releaseLiveMemory();
|
|
606
|
-
return;
|
|
607
|
-
});
|
|
608
|
-
for (const result of isolateSlots) {
|
|
609
|
-
if (result) {
|
|
610
|
-
results.push(result);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
else {
|
|
615
|
-
for (let index = 0; index < scenarios.length; index++) {
|
|
616
|
-
const scenario = scenarios[index];
|
|
617
|
-
if (!scenario) {
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
const scenarioIndex = parentCounters?.index ?? index + 1;
|
|
621
|
-
const scenarioTotal = displayTotal;
|
|
622
|
-
results.push(await runAgentTest({
|
|
623
|
-
cwd: options.cwd,
|
|
624
|
-
suiteName: suite.name,
|
|
625
|
-
scenario,
|
|
626
|
-
defaults: suite.defaults,
|
|
627
|
-
host: options.host,
|
|
628
|
-
judge: options.judge,
|
|
629
|
-
worktree: options.worktree,
|
|
630
|
-
stagingSessionId: options.stagingSessionId,
|
|
631
|
-
scenarioIndex,
|
|
632
|
-
scenarioTotal,
|
|
633
|
-
timeoutMs: options.timeoutMs,
|
|
634
|
-
allowUserInput: options.allowUserInput,
|
|
635
|
-
debug: options.debug,
|
|
636
|
-
debugDir: options.debugDir,
|
|
637
|
-
suitesDir: options.suitesDir,
|
|
638
|
-
keepRecordings: options.keepRecordings,
|
|
639
|
-
rubricsDir: options.rubricsDir,
|
|
640
|
-
scenarioRetries: isChildProcess() ? 0 : options.scenarioRetries,
|
|
641
|
-
compareArm: options.compareArm,
|
|
642
|
-
}));
|
|
643
|
-
releaseLiveMemory();
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
suite: suite.name,
|
|
648
|
-
host: defaultHost,
|
|
649
|
-
passed: results.filter((r) => r.passed).length,
|
|
650
|
-
skipped: results.filter((r) => r.skipped).length,
|
|
651
|
-
failed: results.filter((r) => !r.passed && !r.skipped).length,
|
|
652
|
-
results,
|
|
653
|
-
summary: summarizeReportResults(results),
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
/** Suite trees and MCP scripts from the caller, so uncommitted fixtures reach the host. */
|
|
657
|
-
function liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers) {
|
|
658
|
-
const extras = [...(contextSources ?? [])];
|
|
659
|
-
const suitesRel = overlayRelPath(cwd, suitesDir);
|
|
660
|
-
if (suitesRel && !extras.includes(suitesRel)) {
|
|
661
|
-
extras.push(suitesRel);
|
|
662
|
-
}
|
|
663
|
-
for (const script of mcpStdioScriptPaths(mcpServers)) {
|
|
664
|
-
if (!extras.includes(script)) {
|
|
665
|
-
extras.push(script);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return extras;
|
|
669
|
-
}
|
|
670
|
-
function overlayRelPath(cwd, path) {
|
|
671
|
-
const trimmed = path.replace(/^\.\//, "").trim();
|
|
672
|
-
if (trimmed.length === 0) {
|
|
673
|
-
return undefined;
|
|
674
|
-
}
|
|
675
|
-
if (!trimmed.startsWith("/")) {
|
|
676
|
-
return trimmed;
|
|
677
|
-
}
|
|
678
|
-
const root = resolve(cwd);
|
|
679
|
-
if (trimmed === root) {
|
|
680
|
-
return undefined;
|
|
681
|
-
}
|
|
682
|
-
if (trimmed.startsWith(`${root}/`)) {
|
|
683
|
-
return trimmed.slice(root.length + 1);
|
|
684
|
-
}
|
|
685
|
-
return undefined;
|
|
686
|
-
}
|
|
687
|
-
function resolveRunWorkspace(defaultWorkspace, scenarioWorkspace) {
|
|
688
|
-
const raw = scenarioWorkspace !== undefined ? scenarioWorkspace : defaultWorkspace;
|
|
689
|
-
const parsed = parseScenarioWorkspace(raw);
|
|
690
|
-
if (!parsed.ok) {
|
|
691
|
-
throw new Error(parsed.message);
|
|
692
|
-
}
|
|
693
|
-
return parsed.rel;
|
|
694
|
-
}
|
|
695
|
-
function mergeContextSources(defaults, scenarioSources) {
|
|
696
|
-
const merged = [...(defaults ?? []), ...(scenarioSources ?? [])].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
697
|
-
return merged.length > 0 ? merged : undefined;
|
|
698
|
-
}
|
|
699
|
-
function defaultProfileForHost(host) {
|
|
700
|
-
if (host === "cursor") {
|
|
701
|
-
return "cursor";
|
|
702
|
-
}
|
|
703
|
-
if (host === "claude") {
|
|
704
|
-
return "claude";
|
|
705
|
-
}
|
|
706
|
-
return "shared";
|
|
707
|
-
}
|
|
708
|
-
/** Run one real agent scenario. JSON suites delegate to this same execution boundary. */
|
|
709
|
-
export async function runAgentTest(options) {
|
|
710
|
-
const previousStagingRoot = getLiveStagingRootOverride();
|
|
711
|
-
if (options.debugDir !== undefined) {
|
|
712
|
-
setLiveStagingRootOverride(options.debugDir);
|
|
713
|
-
}
|
|
714
|
-
const stagingSessionId = options.stagingSessionId ??
|
|
715
|
-
(options.debug || options.keepRecordings ? createLiveStagingSessionId() : undefined);
|
|
716
|
-
const previousAuthMode = getProcessAuthMode();
|
|
717
|
-
if (options.authMode !== undefined) {
|
|
718
|
-
setProcessAuthMode(options.authMode);
|
|
719
|
-
}
|
|
720
|
-
try {
|
|
721
|
-
return await runAgentTestBody({ ...options, stagingSessionId });
|
|
722
|
-
}
|
|
723
|
-
finally {
|
|
724
|
-
if (options.authMode !== undefined) {
|
|
725
|
-
setProcessAuthMode(previousAuthMode);
|
|
726
|
-
}
|
|
727
|
-
if (options.debugDir !== undefined) {
|
|
728
|
-
setLiveStagingRootOverride(previousStagingRoot);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
async function runAgentTestBody(options) {
|
|
733
|
-
const legacyScenario = options.scenario;
|
|
734
|
-
const legacyDefaults = options.defaults;
|
|
735
|
-
if (legacyScenario.host === "replay" ||
|
|
736
|
-
legacyDefaults?.host === "replay" ||
|
|
737
|
-
"replayTrace" in legacyScenario) {
|
|
738
|
-
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor, Claude, or OpenAI.");
|
|
739
|
-
}
|
|
740
|
-
const suiteName = options.suiteName ?? "direct";
|
|
741
|
-
const defaultHost = options.host ?? options.defaults?.host ?? "cursor";
|
|
742
|
-
const maxAttempts = resolveScenarioRetryMaxAttempts(options.scenarioRetries);
|
|
743
|
-
let attempts = 0;
|
|
744
|
-
let result;
|
|
745
|
-
while (true) {
|
|
746
|
-
attempts++;
|
|
747
|
-
result = await runAgentTestOnce(options.cwd, suiteName, options.scenario, defaultHost, options.defaults?.contextMode, options.defaults?.profile, options.defaults?.skills, options.defaults?.contextSources, options.defaults?.mcpServers, options.defaults?.workspace, options.defaults?.allowUserSkills, options.judge ?? true, options.worktree ?? true, options.stagingSessionId, options.scenarioIndex, options.scenarioTotal, options.timeoutMs, options.allowUserInput, options.debug, options.debugDir, options.suitesDir ?? "agent-suites", options.keepRecordings, options.rubricsDir, { suppressEmit: maxAttempts > 1, compareArm: options.compareArm });
|
|
748
|
-
const canRetry = !result.skipped &&
|
|
749
|
-
!result.passed &&
|
|
750
|
-
attempts < maxAttempts &&
|
|
751
|
-
shouldRetryAnnounceStopFlake(result.failures, result.trace);
|
|
752
|
-
if (!canRetry) {
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
logPhase(theme.phase("retry", `${attempts}/${maxAttempts - 1}`));
|
|
756
|
-
}
|
|
757
|
-
result.attempts = attempts;
|
|
758
|
-
if (maxAttempts > 1) {
|
|
759
|
-
const debug = isDebugEnabled(options);
|
|
760
|
-
const debugBundleDir = await maybeWriteDebugBundle({
|
|
761
|
-
debug,
|
|
762
|
-
cwd: options.cwd,
|
|
763
|
-
suitesDir: options.suitesDir ?? "agent-suites",
|
|
764
|
-
rubricsDir: options.rubricsDir,
|
|
765
|
-
stagingSessionId: options.stagingSessionId,
|
|
766
|
-
debugDir: options.debugDir,
|
|
767
|
-
suiteName,
|
|
768
|
-
scenario: options.scenario,
|
|
769
|
-
host: options.scenario.host ?? defaultHost,
|
|
770
|
-
result,
|
|
771
|
-
trace: result.trace,
|
|
772
|
-
timeoutMs: options.timeoutMs,
|
|
773
|
-
worktree: options.worktree,
|
|
774
|
-
judge: options.judge ?? true,
|
|
775
|
-
allowUserInput: options.allowUserInput,
|
|
776
|
-
keepRecordings: options.keepRecordings,
|
|
777
|
-
authMode: options.authMode ?? getProcessAuthMode(),
|
|
778
|
-
});
|
|
779
|
-
result.debugBundleDir = debugBundleDir;
|
|
780
|
-
emitScenarioVerdict({
|
|
781
|
-
passed: result.passed,
|
|
782
|
-
index: options.scenarioIndex,
|
|
783
|
-
total: options.scenarioTotal,
|
|
784
|
-
name: options.scenario.name,
|
|
785
|
-
durationMs: result.durationMs,
|
|
786
|
-
totalTokens: totalTokensFromScenarioUsage(result.usage, result.trace?.usage),
|
|
787
|
-
judgeVerdicts: result.judgeVerdicts,
|
|
788
|
-
failures: result.failures,
|
|
789
|
-
story: result.story,
|
|
790
|
-
debug,
|
|
791
|
-
debugBundleDir,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
return result;
|
|
795
|
-
}
|
|
796
|
-
async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultContextMode, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir = "agent-suites", keepRecordings, rubricsDir, runOptions) {
|
|
797
|
-
const started = performance.now();
|
|
798
|
-
const debug = isDebugEnabled({ debug: debugFlag });
|
|
799
|
-
const suppressEmit = runOptions?.suppressEmit === true;
|
|
800
|
-
const requestedContextMode = scenario.contextMode ?? defaultContextMode ?? "harness-preamble";
|
|
801
|
-
if (scenario.skip) {
|
|
802
|
-
const skipLabel = scenarioIndex !== undefined && scenarioTotal !== undefined
|
|
803
|
-
? `[${scenarioIndex}/${scenarioTotal}] ${scenario.name}`
|
|
804
|
-
: scenario.name;
|
|
805
|
-
logProgress(theme.skipped(skipLabel));
|
|
806
|
-
logScenarioDescription(scenario.description);
|
|
807
|
-
return {
|
|
808
|
-
suite: suiteName,
|
|
809
|
-
scenario: scenario.name,
|
|
810
|
-
description: plainDescription(scenario.description),
|
|
811
|
-
prompt: scenario.prompt,
|
|
812
|
-
contextMode: requestedContextMode,
|
|
813
|
-
passed: true,
|
|
814
|
-
failures: [],
|
|
815
|
-
skipped: true,
|
|
816
|
-
durationMs: 0,
|
|
817
|
-
story: buildScenarioStory({
|
|
818
|
-
rubric: scenario.rubric,
|
|
819
|
-
passed: true,
|
|
820
|
-
skipped: true,
|
|
821
|
-
failures: [],
|
|
822
|
-
}),
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
if (scenario.compare && runOptions?.compareArm === undefined) {
|
|
826
|
-
return runCompareAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultContextMode, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir, keepRecordings, rubricsDir, { suppressEmit });
|
|
827
|
-
}
|
|
828
|
-
if (runOptions?.compareArm && scenario.compare) {
|
|
829
|
-
scenario = requireCompareArm(scenario, runOptions.compareArm);
|
|
830
|
-
}
|
|
831
|
-
const host = scenario.host ?? defaultHost;
|
|
832
|
-
const contextMode = scenario.contextMode ?? defaultContextMode ?? "harness-preamble";
|
|
833
|
-
const profile = scenario.profile ?? defaultProfile ?? defaultProfileForHost(host);
|
|
834
|
-
const skills = scenario.skills ?? defaultSkills;
|
|
835
|
-
const contextSources = mergeContextSources(defaultContextSources, scenario.contextSources);
|
|
836
|
-
const workspaceRel = resolveRunWorkspace(defaultWorkspace, scenario.workspace);
|
|
837
|
-
const fixtureWorkspace = workspaceRel !== undefined;
|
|
838
|
-
const allowUserSkills = resolveAllowUserSkills(scenario.allowUserSkills, defaultAllowUserSkills);
|
|
839
|
-
const mcpServers = fixtureWorkspace
|
|
840
|
-
? bindMcpServersToCaller(mergeMcpServers(defaultMcpServers, scenario.mcpServers), cwd)
|
|
841
|
-
: mergeMcpServers(defaultMcpServers, scenario.mcpServers);
|
|
842
|
-
const liveTimeoutMs = resolveLiveTimeoutMs(timeoutMs);
|
|
843
|
-
const failOnUserInput = !allowUserInput;
|
|
844
|
-
const viewerEnvelope = {
|
|
845
|
-
suite: suiteName,
|
|
846
|
-
scenario: scenario.name,
|
|
847
|
-
host,
|
|
848
|
-
...(runOptions?.compareArm ? { arm: runOptions.compareArm } : {}),
|
|
849
|
-
};
|
|
850
|
-
if (runOptions?.compareArm) {
|
|
851
|
-
logPhase(theme.phase("arm", `${runOptions.compareArm.toUpperCase()} ${host}`));
|
|
852
|
-
}
|
|
853
|
-
else if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
854
|
-
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
855
|
-
logScenarioDescription(scenario.description);
|
|
856
|
-
}
|
|
857
|
-
else {
|
|
858
|
-
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
859
|
-
logScenarioDescription(scenario.description);
|
|
860
|
-
}
|
|
861
|
-
const useWorktree = worktree !== false && !process.env.AGENT_TEST_NO_WORKTREE;
|
|
862
|
-
let worktreeHandle;
|
|
863
|
-
let callerHeadBefore;
|
|
864
|
-
const callerTreeBefore = useWorktree ? await captureWorkingTreeStatus(cwd) : undefined;
|
|
865
|
-
if (useWorktree) {
|
|
866
|
-
emitViewerEvent({ type: "status", text: "Creating sealed workspace.", ...viewerEnvelope });
|
|
867
|
-
if (scenario.seedPatch) {
|
|
868
|
-
callerHeadBefore = await captureCallerHead(cwd);
|
|
869
|
-
setCallerHeadRestore(cwd, callerHeadBefore);
|
|
870
|
-
}
|
|
871
|
-
worktreeHandle = await createSealedWorkspace({
|
|
872
|
-
callerCwd: cwd,
|
|
873
|
-
workspace: workspaceRel,
|
|
874
|
-
overlayPaths: fixtureWorkspace
|
|
875
|
-
? undefined
|
|
876
|
-
: defaultSealedOverlayPaths(liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers), skillPathsFromSetting(skills)),
|
|
877
|
-
});
|
|
878
|
-
activeWorktreeCleanup = worktreeHandle.cleanup;
|
|
879
|
-
if (scenario.seedPatch) {
|
|
880
|
-
logPhase(theme.phase("seed", theme.basename(scenario.seedPatch)));
|
|
881
|
-
emitViewerEvent({
|
|
882
|
-
type: "status",
|
|
883
|
-
text: `Applying seed ${basename(scenario.seedPatch)}.`,
|
|
884
|
-
...viewerEnvelope,
|
|
885
|
-
});
|
|
886
|
-
await seedScenarioWorktree(cwd, worktreeHandle.path, scenario.seedPatch, {
|
|
887
|
-
stageOnly: scenario.seedStageOnly === true,
|
|
888
|
-
});
|
|
889
|
-
emitViewerEvent({ type: "status", text: "Seed applied.", ...viewerEnvelope });
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
logPhase(theme.phase("worktree", theme.phaseDim("disabled (AGENT_TEST_ALLOW_IN_PLACE=1)")));
|
|
894
|
-
}
|
|
895
|
-
const runCwd = worktreeHandle?.path ?? (workspaceRel ? resolve(cwd, workspaceRel) : cwd);
|
|
896
|
-
let judgeWorkspace;
|
|
897
|
-
try {
|
|
898
|
-
const context = await loadContext({
|
|
899
|
-
cwd: runCwd,
|
|
900
|
-
mode: contextMode,
|
|
901
|
-
profile,
|
|
902
|
-
skills,
|
|
903
|
-
contextSources,
|
|
904
|
-
});
|
|
905
|
-
const outputContract = contextMode === "host-native" ? undefined : outputContractForRubric(scenario.rubric);
|
|
906
|
-
const hostInput = isBuiltinAgentHost(host)
|
|
907
|
-
? buildHostPrompt({ context, outputContract, prompt: scenario.prompt })
|
|
908
|
-
: undefined;
|
|
909
|
-
const agentStartMarkerPath = isChildProcess() && stagingSessionId
|
|
910
|
-
? getStagingAgentStartPath(stagingSessionId, suiteName, scenario.name, runOptions?.compareArm)
|
|
911
|
-
: undefined;
|
|
912
|
-
emitViewerEvent({ type: "cell_started", ...viewerEnvelope });
|
|
913
|
-
emitViewerEvent({ type: "status", text: "Starting host agent.", ...viewerEnvelope });
|
|
914
|
-
const contextFiles = viewerContextFiles(context.preamble, context.sources, contextSources ?? []);
|
|
915
|
-
if (contextFiles.length > 0) {
|
|
916
|
-
emitViewerEvent({
|
|
917
|
-
type: "status",
|
|
918
|
-
text: contextFiles.map((file) => file.why).join(" "),
|
|
919
|
-
...viewerEnvelope,
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
emitViewerEvent({
|
|
923
|
-
type: "context",
|
|
924
|
-
mode: contextMode,
|
|
925
|
-
files: contextFiles,
|
|
926
|
-
hostInput,
|
|
927
|
-
...viewerEnvelope,
|
|
928
|
-
});
|
|
929
|
-
emitViewerEvent({ type: "prompt", text: scenario.prompt, ...viewerEnvelope });
|
|
930
|
-
logPhase(theme.phase("agent", theme.phaseDim("started")));
|
|
931
|
-
const agentStarted = performance.now();
|
|
932
|
-
let livePreview;
|
|
933
|
-
const session = await withHeartbeat(runAgent({
|
|
934
|
-
host,
|
|
935
|
-
cwd: runCwd,
|
|
936
|
-
context,
|
|
937
|
-
profile,
|
|
938
|
-
prompt: scenario.prompt,
|
|
939
|
-
outputContract,
|
|
940
|
-
mcpServers,
|
|
941
|
-
allowUserSkills,
|
|
942
|
-
networkAccess: scenario.networkAccess === true,
|
|
943
|
-
authMode: getProcessAuthMode(),
|
|
944
|
-
timeoutMs: liveTimeoutMs,
|
|
945
|
-
failOnUserInput,
|
|
946
|
-
maxConversationTurns: resolveMaxConversationTurns(),
|
|
947
|
-
onDeadlineStart: agentStartMarkerPath
|
|
948
|
-
? () => writeAgentStartMarker(agentStartMarkerPath)
|
|
949
|
-
: undefined,
|
|
950
|
-
onAgentEvent: (event) => {
|
|
951
|
-
if (event.type === "tool") {
|
|
952
|
-
emitViewerEvent({
|
|
953
|
-
type: "tool",
|
|
954
|
-
name: event.name,
|
|
955
|
-
args: event.args,
|
|
956
|
-
...viewerEnvelope,
|
|
957
|
-
});
|
|
958
|
-
logLive(theme.liveTool(event.name, pathFromArgs(event.args)));
|
|
959
|
-
livePreview = undefined;
|
|
960
|
-
refreshHeartbeat();
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
emitViewerEvent({ type: "text", text: event.text, ...viewerEnvelope });
|
|
964
|
-
// Clock tick paints the preview. Do not rewrite here — a long
|
|
965
|
-
// line wraps and `\r` cannot clear the leftover row.
|
|
966
|
-
livePreview = quoteExcerpt(event.text);
|
|
967
|
-
},
|
|
968
|
-
}), { started: agentStarted, preview: () => livePreview });
|
|
969
|
-
logPhase(theme.phase("agent", `${theme.statusCompleted(session.status)} ${theme.duration(formatDuration(performance.now() - agentStarted))}`));
|
|
970
|
-
let trace = enrichTrace(session.trace);
|
|
971
|
-
const failures = [];
|
|
972
|
-
if (session.status !== "completed") {
|
|
973
|
-
const runtimeEvidence = [
|
|
974
|
-
`durationMs=${session.durationMs}`,
|
|
975
|
-
`messages=${trace.messages.length}`,
|
|
976
|
-
`toolCalls=${trace.toolCalls.length}`,
|
|
977
|
-
`skillsInvoked=${trace.skillsInvoked?.length ? trace.skillsInvoked.join(",") : "(none)"}`,
|
|
978
|
-
trace.artifacts?.cursorRawStatus
|
|
979
|
-
? `cursorRawStatus=${trace.artifacts.cursorRawStatus}`
|
|
980
|
-
: undefined,
|
|
981
|
-
trace.artifacts?.cursorSdkErrorCode
|
|
982
|
-
? `cursorSdkErrorCode=${trace.artifacts.cursorSdkErrorCode}`
|
|
983
|
-
: undefined,
|
|
984
|
-
trace.artifacts?.cursorSdkErrorMessage
|
|
985
|
-
? `cursorSdkErrorMessage=${trace.artifacts.cursorSdkErrorMessage}`
|
|
986
|
-
: undefined,
|
|
987
|
-
]
|
|
988
|
-
.filter(Boolean)
|
|
989
|
-
.join("\n");
|
|
990
|
-
failures.push(assertionFailure("runAgent", session.error ?? `agent session ${session.status}`, "agent_runtime", runtimeEvidence));
|
|
991
|
-
}
|
|
992
|
-
else if (failOnUserInput && traceHasUserInputTool(trace.toolCalls)) {
|
|
993
|
-
failures.push(assertionFailure("runAgent", "agent trace contains AskQuestion-style user-input tool in headless mode", "agent_runtime", `toolCalls=${trace.toolCalls.map((call) => call.name).join(", ")}`));
|
|
994
|
-
}
|
|
995
|
-
failures.push(...assertRubric(trace, scenario.rubric, {
|
|
996
|
-
skillsMode: context.skillsMode,
|
|
997
|
-
}));
|
|
998
|
-
if (worktreeHandle) {
|
|
999
|
-
const escaped = toolPathsOutsideWorkspace(trace, worktreeHandle.path);
|
|
1000
|
-
if (escaped.length > 0) {
|
|
1001
|
-
failures.push(assertionFailure("workingTreeLeak", `agent used paths outside the sealed workspace: ${escaped.join(", ")}`, "worktree_leak", `escaped=${escaped.join(", ")}`));
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
if (useWorktree && callerTreeBefore !== undefined) {
|
|
1005
|
-
const callerTreeAfter = await captureWorkingTreeStatus(cwd);
|
|
1006
|
-
const ignoreRoots = resolveHarnessArtifactIgnoreRoots(cwd, getLiveStagingRootOverride());
|
|
1007
|
-
const leaked = filterWorkingTreeLeaks(findWorkingTreeLeak(callerTreeBefore, callerTreeAfter), ignoreRoots, cwd);
|
|
1008
|
-
const seedPaths = scenario.seedPatch
|
|
1009
|
-
? await loadUnifiedDiffPaths(resolve(cwd, scenario.seedPatch)).catch(() => [])
|
|
1010
|
-
: [];
|
|
1011
|
-
const outsideEdits = worktreeHandle !== undefined
|
|
1012
|
-
? traceEditsOutsideWorktree(trace, worktreeHandle.path, cwd)
|
|
1013
|
-
: [];
|
|
1014
|
-
const { collateral, agentLeaks } = partitionSeedCollateralLeaks(leaked, seedPaths, outsideEdits);
|
|
1015
|
-
if (collateral.length > 0) {
|
|
1016
|
-
await restoreWorkingTreePaths(cwd, porcelainPathsFromLines(collateral)).catch(() => undefined);
|
|
1017
|
-
}
|
|
1018
|
-
if (outsideEdits.length > 0) {
|
|
1019
|
-
failures.push(assertionFailure("workingTreeLeak", `agent edited caller checkout outside worktree: ${outsideEdits.join(", ")}`, "worktree_leak", [
|
|
1020
|
-
`outsideEdits=${outsideEdits.join(", ")}`,
|
|
1021
|
-
`seedPaths=${seedPaths.join(", ") || "(none)"}`,
|
|
1022
|
-
`collateralRestored=${collateral.length}`,
|
|
1023
|
-
].join("\n")));
|
|
1024
|
-
}
|
|
1025
|
-
if (agentLeaks.length > 0) {
|
|
1026
|
-
failures.push(assertionFailure("workingTreeLeak", `live agent mutated caller working tree (use worktree isolation):\n${formatWorkingTreeLeak(agentLeaks)}`, "worktree_leak", [
|
|
1027
|
-
`agentLeaks=${agentLeaks.length}`,
|
|
1028
|
-
`seedPaths=${seedPaths.join(", ") || "(none)"}`,
|
|
1029
|
-
`collateralRestored=${collateral.length}`,
|
|
1030
|
-
`outsideEdits=${outsideEdits.join(", ") || "(none)"}`,
|
|
1031
|
-
].join("\n")));
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId, runOptions?.compareArm);
|
|
1035
|
-
if (stagingTracePath) {
|
|
1036
|
-
try {
|
|
1037
|
-
const path = await recordTrace(stagingTracePath, trace);
|
|
1038
|
-
if (keepRecordings || debug) {
|
|
1039
|
-
logPhase(theme.phase("trace", theme.path(path)));
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
catch (error) {
|
|
1043
|
-
failures.push(assertionFailure("recordTrace", error instanceof Error ? error.message : "failed to record trace", "recording_error"));
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
const deferJudgeToParent = isChildProcess();
|
|
1047
|
-
let judgeVerdicts;
|
|
1048
|
-
const judgeCriteria = collectJudgeCriteria(scenario.rubric);
|
|
1049
|
-
if (judgeCriteria.length > 0) {
|
|
1050
|
-
judgeWorkspace = await createReadOnlyWorkspaceSnapshot(runCwd, runOptions?.compareArm ?? "scenario");
|
|
1051
|
-
}
|
|
1052
|
-
if (judge && !deferJudgeToParent) {
|
|
1053
|
-
if (judgeCriteria.length > 0) {
|
|
1054
|
-
logPhase(theme.judgePhase(judgeCriteria.length), { last: true });
|
|
1055
|
-
}
|
|
1056
|
-
const snapshot = judgeWorkspace;
|
|
1057
|
-
if (!snapshot)
|
|
1058
|
-
throw new Error("judge workspace snapshot was not created");
|
|
1059
|
-
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd, host, undefined, [
|
|
1060
|
-
{ name: runOptions?.compareArm ?? "scenario", path: snapshot.path },
|
|
1061
|
-
]);
|
|
1062
|
-
failures.push(...judged.failures);
|
|
1063
|
-
trace = judged.trace;
|
|
1064
|
-
judgeVerdicts = toJudgeVerdictResults(judged.trace, judgeCriteria, judged.verdicts);
|
|
1065
|
-
}
|
|
1066
|
-
const durationMs = Math.round(performance.now() - started);
|
|
1067
|
-
if (isChildProcess() && stagingSessionId && runOptions?.writeSidecar !== false) {
|
|
1068
|
-
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name, runOptions?.compareArm), {
|
|
1069
|
-
passed: failures.length === 0,
|
|
1070
|
-
failures,
|
|
1071
|
-
durationMs,
|
|
1072
|
-
contextMode,
|
|
1073
|
-
contextFiles,
|
|
1074
|
-
hostInput,
|
|
1075
|
-
judgeWorkspace: judgeWorkspace
|
|
1076
|
-
? { name: runOptions?.compareArm ?? "scenario", path: judgeWorkspace.path }
|
|
1077
|
-
: undefined,
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
if (worktreeHandle) {
|
|
1081
|
-
await worktreeHandle.cleanup();
|
|
1082
|
-
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
1083
|
-
activeWorktreeCleanup = undefined;
|
|
1084
|
-
}
|
|
1085
|
-
if (callerHeadBefore) {
|
|
1086
|
-
await restoreActiveCallerHead();
|
|
1087
|
-
}
|
|
1088
|
-
worktreeHandle = undefined;
|
|
1089
|
-
}
|
|
1090
|
-
if (judgeWorkspace && judge && runOptions?.compareArm === undefined) {
|
|
1091
|
-
await judgeWorkspace.cleanup();
|
|
1092
|
-
judgeWorkspace = undefined;
|
|
1093
|
-
}
|
|
1094
|
-
const scenarioResult = finalizeScenarioResult({
|
|
1095
|
-
suite: suiteName,
|
|
1096
|
-
scenario,
|
|
1097
|
-
contextMode,
|
|
1098
|
-
contextFiles,
|
|
1099
|
-
hostInput,
|
|
1100
|
-
failures,
|
|
1101
|
-
durationMs,
|
|
1102
|
-
judgeVerdicts,
|
|
1103
|
-
trace,
|
|
1104
|
-
agentUsage: session.usage ?? trace?.usage,
|
|
1105
|
-
});
|
|
1106
|
-
if (judgeWorkspace) {
|
|
1107
|
-
Object.defineProperty(scenarioResult, "judgeWorkspace", {
|
|
1108
|
-
value: {
|
|
1109
|
-
name: runOptions?.compareArm ?? "scenario",
|
|
1110
|
-
path: judgeWorkspace.path,
|
|
1111
|
-
cleanup: judgeWorkspace.cleanup,
|
|
1112
|
-
},
|
|
1113
|
-
enumerable: false,
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
const { passed, story } = scenarioResult;
|
|
1117
|
-
const armMetrics = {
|
|
1118
|
-
id: runOptions?.compareArm ?? "cell",
|
|
1119
|
-
label: runOptions?.compareArm ?? "cell",
|
|
1120
|
-
prompt: scenario.prompt,
|
|
1121
|
-
trace,
|
|
1122
|
-
};
|
|
1123
|
-
const turns = compareArmTurns(armMetrics);
|
|
1124
|
-
const tokens = compareArmTokens(armMetrics);
|
|
1125
|
-
const tools = trace.toolCalls.length;
|
|
1126
|
-
emitViewerEvent({
|
|
1127
|
-
type: "cell_finished",
|
|
1128
|
-
...viewerEnvelope,
|
|
1129
|
-
passed,
|
|
1130
|
-
durationMs,
|
|
1131
|
-
failures: failures.map((failure) => ({
|
|
1132
|
-
matcher: failure.matcher,
|
|
1133
|
-
message: failure.message,
|
|
1134
|
-
})),
|
|
1135
|
-
metrics: {
|
|
1136
|
-
...(turns !== undefined ? { turns } : {}),
|
|
1137
|
-
...(tokens !== undefined ? { tokens } : {}),
|
|
1138
|
-
tools,
|
|
1139
|
-
},
|
|
1140
|
-
});
|
|
1141
|
-
if (judgeVerdicts && judgeVerdicts.length > 0) {
|
|
1142
|
-
emitViewerEvent({
|
|
1143
|
-
type: "judge",
|
|
1144
|
-
...viewerEnvelope,
|
|
1145
|
-
verdicts: judgeVerdicts.map((verdict) => ({
|
|
1146
|
-
id: verdict.id,
|
|
1147
|
-
question: verdict.question,
|
|
1148
|
-
pass: verdict.pass,
|
|
1149
|
-
rationale: verdict.rationale,
|
|
1150
|
-
})),
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
if (!suppressEmit) {
|
|
1154
|
-
const debugBundleDir = await maybeWriteDebugBundle({
|
|
1155
|
-
debug,
|
|
1156
|
-
cwd,
|
|
1157
|
-
suitesDir,
|
|
1158
|
-
rubricsDir,
|
|
1159
|
-
stagingSessionId,
|
|
1160
|
-
debugDir,
|
|
1161
|
-
suiteName,
|
|
1162
|
-
scenario,
|
|
1163
|
-
host,
|
|
1164
|
-
result: scenarioResult,
|
|
1165
|
-
trace,
|
|
1166
|
-
timeoutMs,
|
|
1167
|
-
worktree,
|
|
1168
|
-
judge,
|
|
1169
|
-
allowUserInput,
|
|
1170
|
-
keepRecordings,
|
|
1171
|
-
authMode: getProcessAuthMode(),
|
|
1172
|
-
});
|
|
1173
|
-
scenarioResult.debugBundleDir = debugBundleDir;
|
|
1174
|
-
emitViewerEvent({
|
|
1175
|
-
type: "scenario_result",
|
|
1176
|
-
...viewerEnvelope,
|
|
1177
|
-
result: scenarioResult,
|
|
1178
|
-
});
|
|
1179
|
-
emitScenarioVerdict({
|
|
1180
|
-
passed,
|
|
1181
|
-
index: scenarioIndex,
|
|
1182
|
-
total: scenarioTotal,
|
|
1183
|
-
name: scenario.name,
|
|
1184
|
-
durationMs,
|
|
1185
|
-
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, trace?.usage),
|
|
1186
|
-
judgeVerdicts,
|
|
1187
|
-
failures,
|
|
1188
|
-
story,
|
|
1189
|
-
debug,
|
|
1190
|
-
debugBundleDir,
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
return scenarioResult;
|
|
1194
|
-
}
|
|
1195
|
-
finally {
|
|
1196
|
-
if (judgeWorkspace && judge && runOptions?.compareArm === undefined) {
|
|
1197
|
-
await judgeWorkspace.cleanup().catch(() => undefined);
|
|
1198
|
-
judgeWorkspace = undefined;
|
|
1199
|
-
}
|
|
1200
|
-
if (useWorktree && callerTreeBefore !== undefined) {
|
|
1201
|
-
const callerTreeAfter = await captureWorkingTreeStatus(cwd).catch(() => "");
|
|
1202
|
-
const ignoreRoots = resolveHarnessArtifactIgnoreRoots(cwd, getLiveStagingRootOverride());
|
|
1203
|
-
const leaked = filterWorkingTreeLeaks(findWorkingTreeLeak(callerTreeBefore, callerTreeAfter), ignoreRoots, cwd);
|
|
1204
|
-
if (leaked.length > 0) {
|
|
1205
|
-
await restoreWorkingTreePaths(cwd, porcelainPathsFromLines(leaked)).catch(() => undefined);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
if (worktreeHandle) {
|
|
1209
|
-
await worktreeHandle.cleanup();
|
|
1210
|
-
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
1211
|
-
activeWorktreeCleanup = undefined;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
if (activeCallerHeadRestore) {
|
|
1215
|
-
await restoreActiveCallerHead().catch(() => undefined);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
async function runCompareAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultContextMode, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir = "agent-suites", keepRecordings, rubricsDir, runOptions) {
|
|
1220
|
-
const started = performance.now();
|
|
1221
|
-
const debug = isDebugEnabled({ debug: debugFlag });
|
|
1222
|
-
const suppressEmit = runOptions?.suppressEmit === true;
|
|
1223
|
-
const host = scenario.host ?? defaultHost;
|
|
1224
|
-
if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
1225
|
-
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
1226
|
-
}
|
|
1227
|
-
else {
|
|
1228
|
-
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
1229
|
-
}
|
|
1230
|
-
logScenarioDescription(scenario.description);
|
|
1231
|
-
const resolvedArms = resolveCompareArms(scenario.compare);
|
|
1232
|
-
const shared = {
|
|
1233
|
-
cwd,
|
|
1234
|
-
suiteName,
|
|
1235
|
-
defaultHost,
|
|
1236
|
-
defaultContextMode,
|
|
1237
|
-
defaultProfile,
|
|
1238
|
-
defaultSkills,
|
|
1239
|
-
defaultContextSources,
|
|
1240
|
-
defaultMcpServers,
|
|
1241
|
-
defaultWorkspace,
|
|
1242
|
-
defaultAllowUserSkills,
|
|
1243
|
-
worktree,
|
|
1244
|
-
stagingSessionId,
|
|
1245
|
-
timeoutMs,
|
|
1246
|
-
allowUserInput,
|
|
1247
|
-
debugFlag,
|
|
1248
|
-
debugDir,
|
|
1249
|
-
suitesDir,
|
|
1250
|
-
keepRecordings,
|
|
1251
|
-
rubricsDir,
|
|
1252
|
-
};
|
|
1253
|
-
const armRuns = [];
|
|
1254
|
-
for (const entry of resolvedArms) {
|
|
1255
|
-
const armScenario = applyCompareArm(scenario, entry.id);
|
|
1256
|
-
const label = compareArmLabel(entry.arm, entry.id);
|
|
1257
|
-
const result = await runAgentTestOnce(shared.cwd, shared.suiteName, armScenario, shared.defaultHost, shared.defaultContextMode, shared.defaultProfile, shared.defaultSkills, shared.defaultContextSources, shared.defaultMcpServers, shared.defaultWorkspace, shared.defaultAllowUserSkills, false, shared.worktree, shared.stagingSessionId, undefined, undefined, shared.timeoutMs, shared.allowUserInput, shared.debugFlag, shared.debugDir, shared.suitesDir, shared.keepRecordings, shared.rubricsDir, { suppressEmit: true, compareArm: entry.id, writeSidecar: false });
|
|
1258
|
-
armRuns.push({ id: entry.id, label, scenario: armScenario, result });
|
|
1259
|
-
}
|
|
1260
|
-
const durationMs = Math.round(performance.now() - started);
|
|
1261
|
-
const scenarioResult = await finalizeCompareArmRuns({
|
|
1262
|
-
cwd,
|
|
1263
|
-
suite: suiteName,
|
|
1264
|
-
scenario,
|
|
1265
|
-
host,
|
|
1266
|
-
armRuns,
|
|
1267
|
-
durationMs,
|
|
1268
|
-
judge: Boolean(judge && !isChildProcess()),
|
|
1269
|
-
evaluateGates: !isChildProcess(),
|
|
1270
|
-
});
|
|
1271
|
-
const { passed, story, failures, trace: firstTrace } = scenarioResult;
|
|
1272
|
-
if (isChildProcess() && stagingSessionId) {
|
|
1273
|
-
const sidecarArms = {};
|
|
1274
|
-
for (const arm of armRuns) {
|
|
1275
|
-
sidecarArms[arm.id] = {
|
|
1276
|
-
durationMs: arm.result.durationMs,
|
|
1277
|
-
contextMode: arm.result.contextMode,
|
|
1278
|
-
contextFiles: arm.result.contextFiles,
|
|
1279
|
-
hostInput: arm.result.hostInput,
|
|
1280
|
-
judgeWorkspace: arm.result.judgeWorkspace
|
|
1281
|
-
? { name: arm.result.judgeWorkspace.name, path: arm.result.judgeWorkspace.path }
|
|
1282
|
-
: undefined,
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
1286
|
-
passed: failures.length === 0,
|
|
1287
|
-
failures,
|
|
1288
|
-
durationMs,
|
|
1289
|
-
compare: {
|
|
1290
|
-
a: sidecarArms.a,
|
|
1291
|
-
b: sidecarArms.b,
|
|
1292
|
-
arms: sidecarArms,
|
|
1293
|
-
},
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
if (!suppressEmit) {
|
|
1297
|
-
const debugBundleDir = await maybeWriteDebugBundle({
|
|
1298
|
-
debug,
|
|
1299
|
-
cwd,
|
|
1300
|
-
suitesDir,
|
|
1301
|
-
rubricsDir,
|
|
1302
|
-
stagingSessionId,
|
|
1303
|
-
debugDir,
|
|
1304
|
-
suiteName,
|
|
1305
|
-
scenario,
|
|
1306
|
-
host,
|
|
1307
|
-
result: scenarioResult,
|
|
1308
|
-
trace: firstTrace,
|
|
1309
|
-
timeoutMs,
|
|
1310
|
-
worktree,
|
|
1311
|
-
judge,
|
|
1312
|
-
allowUserInput,
|
|
1313
|
-
keepRecordings,
|
|
1314
|
-
authMode: getProcessAuthMode(),
|
|
1315
|
-
});
|
|
1316
|
-
scenarioResult.debugBundleDir = debugBundleDir;
|
|
1317
|
-
emitViewerEvent({
|
|
1318
|
-
type: "scenario_result",
|
|
1319
|
-
suite: suiteName,
|
|
1320
|
-
scenario: scenario.name,
|
|
1321
|
-
host,
|
|
1322
|
-
result: scenarioResult,
|
|
1323
|
-
});
|
|
1324
|
-
emitScenarioVerdict({
|
|
1325
|
-
passed,
|
|
1326
|
-
index: scenarioIndex,
|
|
1327
|
-
total: scenarioTotal,
|
|
1328
|
-
name: scenario.name,
|
|
1329
|
-
durationMs,
|
|
1330
|
-
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, firstTrace?.usage),
|
|
1331
|
-
judgeVerdicts: scenarioResult.judgeVerdicts,
|
|
1332
|
-
failures,
|
|
1333
|
-
story,
|
|
1334
|
-
debug,
|
|
1335
|
-
debugBundleDir,
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
return scenarioResult;
|
|
1339
|
-
}
|
|
1340
|
-
async function finalizeCompareArmRuns(options) {
|
|
1341
|
-
const resolvedArms = resolveCompareArms(options.scenario.compare);
|
|
1342
|
-
const finalized = finalizeCompareOutcome(options.armRuns.map((arm) => ({
|
|
1343
|
-
id: arm.id,
|
|
1344
|
-
label: arm.label,
|
|
1345
|
-
description: compareArmDescription(resolvedArms.find((entry) => entry.id === arm.id)?.arm),
|
|
1346
|
-
prompt: arm.scenario.prompt,
|
|
1347
|
-
trace: arm.result.trace,
|
|
1348
|
-
contextMode: arm.result.contextMode,
|
|
1349
|
-
contextFiles: arm.result.contextFiles,
|
|
1350
|
-
hostInput: arm.result.hostInput,
|
|
1351
|
-
judgeWorkspace: arm.result.judgeWorkspace,
|
|
1352
|
-
durationMs: arm.result.durationMs,
|
|
1353
|
-
passed: !arm.result.failures.some((failure) => failure.category === "rubric_miss"),
|
|
1354
|
-
failures: arm.result.failures,
|
|
1355
|
-
})), options.scenario.compare?.gates, { evaluateGates: false });
|
|
1356
|
-
const compareResult = finalized.compare;
|
|
1357
|
-
const failures = finalized.failures;
|
|
1358
|
-
if (options.judge && options.scenario.compare?.judgeMetrics?.length) {
|
|
1359
|
-
for (const arm of compareResult.arms) {
|
|
1360
|
-
if (!arm.trace)
|
|
1361
|
-
continue;
|
|
1362
|
-
const judged = await runJudgeRubric(arm.trace, { judge: options.scenario.compare.judgeMetrics }, options.cwd, options.host, undefined, arm.judgeWorkspace
|
|
1363
|
-
? [{ name: arm.judgeWorkspace.name, path: arm.judgeWorkspace.path }]
|
|
1364
|
-
: undefined);
|
|
1365
|
-
arm.trace = judged.trace;
|
|
1366
|
-
arm.judgeVerdicts = toJudgeVerdictResults(judged.trace, options.scenario.compare.judgeMetrics, judged.verdicts);
|
|
1367
|
-
arm.failures = [...(arm.failures ?? []), ...judged.failures];
|
|
1368
|
-
failures.push(...prefixCompareFailures(arm.label, judged.failures.filter((failure) => failure.category !== "rubric_miss")));
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
if (options.evaluateGates)
|
|
1372
|
-
finalizeCompareGates(compareResult, failures);
|
|
1373
|
-
const compareArms = compareResultArms(compareResult);
|
|
1374
|
-
const firstTrace = compareArms[0]?.trace;
|
|
1375
|
-
let judgeVerdicts;
|
|
1376
|
-
if (options.judge &&
|
|
1377
|
-
failures.every((failure) => failure.category === "rubric_miss") &&
|
|
1378
|
-
compareArms.length > 0 &&
|
|
1379
|
-
compareArms.every((arm) => arm.trace)) {
|
|
1380
|
-
const criteria = collectCompareJudgeCriteria(options.scenario.rubric);
|
|
1381
|
-
if (criteria.length > 0)
|
|
1382
|
-
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
1383
|
-
const judged = await runCompareJudgeRubric(compareResult, options.scenario.rubric, options.cwd, options.host);
|
|
1384
|
-
failures.push(...judged.failures);
|
|
1385
|
-
if (firstTrace) {
|
|
1386
|
-
judgeVerdicts = toJudgeVerdictResults({ ...firstTrace, judgeVerdicts: judged.verdicts }, criteria, judged.verdicts);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
for (const arm of compareResult.arms) {
|
|
1390
|
-
if (arm.judgeWorkspace) {
|
|
1391
|
-
await arm.judgeWorkspace.cleanup().catch(() => undefined);
|
|
1392
|
-
delete arm.judgeWorkspace;
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return finalizeScenarioResult({
|
|
1396
|
-
suite: options.suite,
|
|
1397
|
-
scenario: options.scenario,
|
|
1398
|
-
failures,
|
|
1399
|
-
durationMs: options.durationMs,
|
|
1400
|
-
judgeVerdicts,
|
|
1401
|
-
trace: firstTrace,
|
|
1402
|
-
compare: compareResult,
|
|
1403
|
-
storyCompare: attachCompareStoryResults(compareStoryFields(options.scenario), compareResult),
|
|
1404
|
-
agentUsage: sumUsageParts(options.armRuns.map((arm) => arm.result.agentUsage)),
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
/** Finalize viewer-scheduled compare arms through the same domain and judge path as the CLI. */
|
|
1408
|
-
export async function finalizeScheduledCompareScenario(options) {
|
|
1409
|
-
const compareArms = options.scenario.compare ?? [];
|
|
1410
|
-
const scenario = {
|
|
1411
|
-
name: options.scenario.name,
|
|
1412
|
-
description: options.scenario.description,
|
|
1413
|
-
prompt: options.scenario.prompt,
|
|
1414
|
-
rubric: options.scenario.rubric,
|
|
1415
|
-
compare: {
|
|
1416
|
-
arms: compareArms.map((arm) => ({
|
|
1417
|
-
id: arm.id,
|
|
1418
|
-
label: arm.label,
|
|
1419
|
-
description: arm.description,
|
|
1420
|
-
prompt: arm.prompt,
|
|
1421
|
-
})),
|
|
1422
|
-
gates: options.scenario.gates,
|
|
1423
|
-
judgeMetrics: options.scenario.judgeMetrics,
|
|
1424
|
-
},
|
|
1425
|
-
};
|
|
1426
|
-
const armRuns = compareArms.flatMap((arm) => {
|
|
1427
|
-
const result = options.armResults.get(arm.id);
|
|
1428
|
-
return result
|
|
1429
|
-
? [
|
|
1430
|
-
{
|
|
1431
|
-
id: arm.id,
|
|
1432
|
-
label: arm.label,
|
|
1433
|
-
scenario: applyCompareArm(scenario, arm.id),
|
|
1434
|
-
result,
|
|
1435
|
-
},
|
|
1436
|
-
]
|
|
1437
|
-
: [];
|
|
1438
|
-
});
|
|
1439
|
-
return finalizeCompareArmRuns({
|
|
1440
|
-
cwd: options.cwd,
|
|
1441
|
-
suite: options.suite,
|
|
1442
|
-
scenario,
|
|
1443
|
-
host: options.host,
|
|
1444
|
-
armRuns,
|
|
1445
|
-
durationMs: Math.max(0, ...armRuns.map((arm) => arm.result.durationMs)),
|
|
1446
|
-
judge: options.judge !== false,
|
|
1447
|
-
evaluateGates: true,
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
/** Finalize one viewer-scheduled scenario through the parent-owned judge path. */
|
|
1451
|
-
export async function finalizeScheduledScenario(options) {
|
|
1452
|
-
const criteria = collectJudgeCriteria(options.scenario.rubric);
|
|
1453
|
-
if (options.judge === false ||
|
|
1454
|
-
criteria.length === 0 ||
|
|
1455
|
-
!options.result.trace ||
|
|
1456
|
-
options.result.failures.length > 0) {
|
|
1457
|
-
return options.result;
|
|
1458
|
-
}
|
|
1459
|
-
const started = performance.now();
|
|
1460
|
-
let trace = options.result.trace;
|
|
1461
|
-
const failures = [...options.result.failures];
|
|
1462
|
-
let judgeVerdicts;
|
|
1463
|
-
try {
|
|
1464
|
-
const judged = await runJudgeRubric(trace, options.scenario.rubric, options.cwd, options.host, options.onJudgeEvent, options.result.judgeWorkspace
|
|
1465
|
-
? [{ name: options.result.judgeWorkspace.name, path: options.result.judgeWorkspace.path }]
|
|
1466
|
-
: undefined);
|
|
1467
|
-
failures.push(...judged.failures);
|
|
1468
|
-
trace = judged.trace;
|
|
1469
|
-
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
1470
|
-
}
|
|
1471
|
-
catch (error) {
|
|
1472
|
-
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to judge viewer scenario", "judge_infra"));
|
|
1473
|
-
}
|
|
1474
|
-
return finalizeScenarioResult({
|
|
1475
|
-
suite: options.suite,
|
|
1476
|
-
scenario: options.scenario,
|
|
1477
|
-
contextMode: options.result.contextMode,
|
|
1478
|
-
contextFiles: options.result.contextFiles,
|
|
1479
|
-
hostInput: options.result.hostInput,
|
|
1480
|
-
failures,
|
|
1481
|
-
durationMs: options.result.durationMs + Math.round(performance.now() - started),
|
|
1482
|
-
attempts: options.result.attempts,
|
|
1483
|
-
judgeVerdicts,
|
|
1484
|
-
trace,
|
|
1485
|
-
agentUsage: options.result.agentUsage ?? options.result.trace.usage,
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
async function loadCompareResultFromStaging(stagingSessionId, suiteName, scenario, childSidecar) {
|
|
1489
|
-
const resolved = resolveCompareArms(scenario.compare);
|
|
1490
|
-
const arms = [];
|
|
1491
|
-
for (const entry of resolved) {
|
|
1492
|
-
const armScenario = applyCompareArm(scenario, entry.id);
|
|
1493
|
-
let trace;
|
|
1494
|
-
try {
|
|
1495
|
-
trace = await loadStagingTrace(getStagingTracePath(stagingSessionId, suiteName, scenario.name, entry.id));
|
|
1496
|
-
}
|
|
1497
|
-
catch {
|
|
1498
|
-
trace = undefined;
|
|
1499
|
-
}
|
|
1500
|
-
const sidecarWorkspace = childSidecar?.compare?.arms?.[entry.id]?.judgeWorkspace;
|
|
1501
|
-
arms.push({
|
|
1502
|
-
id: entry.id,
|
|
1503
|
-
label: compareArmLabel(entry.arm, entry.id),
|
|
1504
|
-
description: compareArmDescription(entry.arm),
|
|
1505
|
-
prompt: armScenario.prompt,
|
|
1506
|
-
trace,
|
|
1507
|
-
passed: trace ? assertRubric(trace, armScenario.rubric).length === 0 : undefined,
|
|
1508
|
-
failures: trace ? assertRubric(trace, armScenario.rubric) : undefined,
|
|
1509
|
-
judgeWorkspace: sidecarWorkspace
|
|
1510
|
-
? {
|
|
1511
|
-
name: sidecarWorkspace.name,
|
|
1512
|
-
path: sidecarWorkspace.path,
|
|
1513
|
-
cleanup: async () => {
|
|
1514
|
-
await rm(dirname(sidecarWorkspace.path), {
|
|
1515
|
-
recursive: true,
|
|
1516
|
-
force: true,
|
|
1517
|
-
});
|
|
1518
|
-
},
|
|
1519
|
-
}
|
|
1520
|
-
: undefined,
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
return buildCompareResult(arms, scenario.compare?.gates);
|
|
1524
|
-
}
|
|
1525
|
-
async function runCompareJudgeRubric(compare, rubric, runCwd, host) {
|
|
1526
|
-
const criteria = collectCompareJudgeCriteria(rubric);
|
|
1527
|
-
const arms = compareResultArms(compare).filter((arm) => arm.trace !== undefined);
|
|
1528
|
-
if (criteria.length === 0 || arms.length < 2) {
|
|
1529
|
-
return { failures: [], verdicts: [] };
|
|
1530
|
-
}
|
|
1531
|
-
const pair = arms.length === 2 && arms[0] && arms[1]
|
|
1532
|
-
? {
|
|
1533
|
-
aLabel: arms[0].label,
|
|
1534
|
-
a: arms[0].trace,
|
|
1535
|
-
bLabel: arms[1].label,
|
|
1536
|
-
b: arms[1].trace,
|
|
1537
|
-
}
|
|
1538
|
-
: undefined;
|
|
1539
|
-
const workspaces = arms
|
|
1540
|
-
.map((arm) => arm.judgeWorkspace)
|
|
1541
|
-
.filter((workspace) => workspace !== undefined)
|
|
1542
|
-
.map(({ name, path }) => ({ name, path }));
|
|
1543
|
-
const result = await judgeCompareTraces(pair ?? {
|
|
1544
|
-
arms: arms.map((arm) => ({ label: arm.label, trace: arm.trace })),
|
|
1545
|
-
}, criteria, { cwd: runCwd, host, workspaces });
|
|
1546
|
-
if (result.skipped) {
|
|
1547
|
-
return {
|
|
1548
|
-
failures: [assertionFailure("judge", result.error ?? "judge skipped", "judge_infra")],
|
|
1549
|
-
verdicts: [],
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
const failures = [];
|
|
1553
|
-
for (const verdict of result.verdicts) {
|
|
1554
|
-
if (!verdict.pass) {
|
|
1555
|
-
const category = verdict.infraError
|
|
1556
|
-
? "judge_infra"
|
|
1557
|
-
: verdict.parseError
|
|
1558
|
-
? "judge_parse"
|
|
1559
|
-
: "rubric_miss";
|
|
1560
|
-
failures.push(assertionFailure(`judge:${verdict.id}`, verdict.rationale, category));
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
if (result.error && !result.verdicts.some((verdict) => !verdict.pass)) {
|
|
1564
|
-
failures.push(assertionFailure("judge", result.error, "judge_infra"));
|
|
1565
|
-
}
|
|
1566
|
-
return { failures, verdicts: result.verdicts };
|
|
1567
|
-
}
|
|
1568
|
-
async function runJudgeRubric(trace, rubric, runCwd, host, onJudgeEvent, workspaces) {
|
|
1569
|
-
const criteria = collectJudgeCriteria(rubric);
|
|
1570
|
-
if (criteria.length === 0) {
|
|
1571
|
-
return { trace, failures: [], verdicts: [] };
|
|
1572
|
-
}
|
|
1573
|
-
const result = await judgeTrace(trace, criteria, {
|
|
1574
|
-
cwd: runCwd,
|
|
1575
|
-
host,
|
|
1576
|
-
workspaces,
|
|
1577
|
-
onEvent: onJudgeEvent,
|
|
1578
|
-
});
|
|
1579
|
-
if (result.skipped) {
|
|
1580
|
-
return {
|
|
1581
|
-
trace,
|
|
1582
|
-
failures: [assertionFailure("judge", result.error ?? "judge skipped", "judge_infra")],
|
|
1583
|
-
verdicts: [],
|
|
1584
|
-
};
|
|
1585
|
-
}
|
|
1586
|
-
const judgedTrace = { ...trace, judgeVerdicts: result.verdicts };
|
|
1587
|
-
const failures = [];
|
|
1588
|
-
for (const verdict of result.verdicts) {
|
|
1589
|
-
if (!verdict.pass) {
|
|
1590
|
-
const category = verdict.infraError
|
|
1591
|
-
? "judge_infra"
|
|
1592
|
-
: verdict.parseError
|
|
1593
|
-
? "judge_parse"
|
|
1594
|
-
: "rubric_miss";
|
|
1595
|
-
failures.push(assertionFailure(`judge:${verdict.id}`, verdict.rationale, category));
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
// Top-level only when no failing verdict already covers the error (avoids
|
|
1599
|
-
// double-filing parse/infra failures as both judge:<id> and judge).
|
|
1600
|
-
if (result.error && !result.verdicts.some((v) => !v.pass)) {
|
|
1601
|
-
failures.push(assertionFailure("judge", result.error, "judge_infra"));
|
|
1602
|
-
}
|
|
1603
|
-
return { trace: judgedTrace, failures, verdicts: result.verdicts };
|
|
1604
|
-
}
|
|
1605
|
-
function suiteNameFromPath(suitePath) {
|
|
1606
|
-
return basename(dirname(suitePath));
|
|
1607
|
-
}
|
|
1608
|
-
export async function runAllSuites(options) {
|
|
1609
|
-
const previousAuthMode = getProcessAuthMode();
|
|
1610
|
-
if (options.authMode !== undefined) {
|
|
1611
|
-
setProcessAuthMode(options.authMode);
|
|
1612
|
-
}
|
|
1613
|
-
try {
|
|
1614
|
-
return await runAllSuitesBody(options);
|
|
1615
|
-
}
|
|
1616
|
-
finally {
|
|
1617
|
-
if (options.authMode !== undefined) {
|
|
1618
|
-
setProcessAuthMode(previousAuthMode);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
async function runAllSuitesBody(options) {
|
|
1623
|
-
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
1624
|
-
const filtered = options.filter
|
|
1625
|
-
? suitePaths.filter((suitePath) => {
|
|
1626
|
-
const suiteName = suiteNameFromPath(suitePath);
|
|
1627
|
-
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
1628
|
-
})
|
|
1629
|
-
: suitePaths;
|
|
1630
|
-
const cliHosts = uniqueHosts(options.hosts ?? (options.host ? [options.host] : undefined));
|
|
1631
|
-
const reports = [];
|
|
1632
|
-
for (const suitePath of filtered) {
|
|
1633
|
-
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
1634
|
-
const hosts = resolveSuiteHosts({
|
|
1635
|
-
cliHosts,
|
|
1636
|
-
suiteHosts: suite.hosts,
|
|
1637
|
-
defaultHost: suite.defaults?.host,
|
|
1638
|
-
});
|
|
1639
|
-
const hostLocked = hosts.length > 1;
|
|
1640
|
-
for (const host of hosts) {
|
|
1641
|
-
reports.push(await runSuite({
|
|
1642
|
-
cwd: options.cwd,
|
|
1643
|
-
suitePath,
|
|
1644
|
-
host,
|
|
1645
|
-
hostLocked,
|
|
1646
|
-
scenarioFilter: options.scenarioFilter,
|
|
1647
|
-
judge: options.judge,
|
|
1648
|
-
worktree: options.worktree,
|
|
1649
|
-
stagingSessionId: options.stagingSessionId,
|
|
1650
|
-
keepRecordings: options.keepRecordings,
|
|
1651
|
-
suitesDir: options.suitesDir,
|
|
1652
|
-
suiteFilter: options.filter,
|
|
1653
|
-
timeoutMs: options.timeoutMs,
|
|
1654
|
-
allowUserInput: options.allowUserInput,
|
|
1655
|
-
debug: options.debug,
|
|
1656
|
-
debugDir: options.debugDir,
|
|
1657
|
-
scenarioRetries: options.scenarioRetries,
|
|
1658
|
-
rubricsDir: options.rubricsDir,
|
|
1659
|
-
adapterModules: options.adapterModules,
|
|
1660
|
-
authMode: options.authMode ?? getProcessAuthMode(),
|
|
1661
|
-
compareArm: options.compareArm,
|
|
1662
|
-
workers: options.workers,
|
|
1663
|
-
}));
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
return reports;
|
|
1667
|
-
}
|
|
1668
|
-
export { discoverSuites } from "./discover-suites.js";
|
|
1669
|
-
//# sourceMappingURL=run-suite.js.map
|