@post-print/agent-test 0.3.3 → 0.3.5
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 +126 -157
- package/dist/check.d.ts +38 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +169 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +12 -14
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +342 -366
- package/dist/cli.js.map +1 -1
- package/dist/compare-scenario.d.ts +33 -0
- package/dist/compare-scenario.d.ts.map +1 -0
- package/dist/compare-scenario.js +128 -0
- package/dist/compare-scenario.js.map +1 -0
- package/dist/debug-bundle.d.ts +2 -2
- package/dist/debug-bundle.d.ts.map +1 -1
- package/dist/debug-bundle.js +12 -12
- package/dist/debug-bundle.js.map +1 -1
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +136 -14
- package/dist/doctor.js.map +1 -1
- package/dist/expect.d.ts +6 -5
- package/dist/expect.d.ts.map +1 -1
- package/dist/expect.js +67 -57
- package/dist/expect.js.map +1 -1
- package/dist/host-log.d.ts +17 -0
- package/dist/host-log.d.ts.map +1 -0
- package/dist/host-log.js +154 -0
- package/dist/host-log.js.map +1 -0
- package/dist/hosts.d.ts +22 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +84 -0
- package/dist/hosts.js.map +1 -0
- package/dist/html-report.d.ts +0 -13
- package/dist/html-report.d.ts.map +1 -1
- package/dist/html-report.js +381 -318
- package/dist/html-report.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/live-isolation.d.ts +11 -5
- package/dist/live-isolation.d.ts.map +1 -1
- package/dist/live-isolation.js +32 -14
- package/dist/live-isolation.js.map +1 -1
- package/dist/load-adapters.d.ts +13 -0
- package/dist/load-adapters.d.ts.map +1 -0
- package/dist/load-adapters.js +76 -0
- package/dist/load-adapters.js.map +1 -0
- package/dist/load-suite.d.ts.map +1 -1
- package/dist/load-suite.js +16 -0
- package/dist/load-suite.js.map +1 -1
- package/dist/mcp-config.d.ts +8 -0
- package/dist/mcp-config.d.ts.map +1 -1
- package/dist/mcp-config.js +46 -0
- package/dist/mcp-config.js.map +1 -1
- package/dist/preflight.d.ts +2 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +4 -4
- package/dist/preflight.js.map +1 -1
- package/dist/progress.d.ts +7 -0
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +39 -9
- package/dist/progress.js.map +1 -1
- package/dist/record-trace.d.ts +11 -20
- package/dist/record-trace.d.ts.map +1 -1
- package/dist/record-trace.js +10 -57
- package/dist/record-trace.js.map +1 -1
- package/dist/report-preview-server.d.ts +2 -0
- package/dist/report-preview-server.d.ts.map +1 -0
- package/dist/report-preview-server.js +24 -0
- package/dist/report-preview-server.js.map +1 -0
- package/dist/report-preview.d.ts +11 -0
- package/dist/report-preview.d.ts.map +1 -0
- package/dist/report-preview.js +161 -0
- package/dist/report-preview.js.map +1 -0
- package/dist/run-suite.d.ts +62 -11
- package/dist/run-suite.d.ts.map +1 -1
- package/dist/run-suite.js +697 -206
- package/dist/run-suite.js.map +1 -1
- package/dist/scenario-story.d.ts +41 -0
- package/dist/scenario-story.d.ts.map +1 -0
- package/dist/scenario-story.js +198 -0
- package/dist/scenario-story.js.map +1 -0
- package/dist/suite-summary.d.ts +2 -0
- package/dist/suite-summary.d.ts.map +1 -1
- package/dist/suite-summary.js +70 -22
- package/dist/suite-summary.js.map +1 -1
- package/dist/theme.d.ts +29 -5
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +204 -45
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +107 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-seeds.d.ts.map +1 -1
- package/dist/validate-seeds.js +21 -6
- package/dist/validate-seeds.js.map +1 -1
- package/dist/validate-suite.d.ts +3 -1
- package/dist/validate-suite.d.ts.map +1 -1
- package/dist/validate-suite.js +256 -22
- package/dist/validate-suite.js.map +1 -1
- package/package.json +5 -5
- package/dist/compare.d.ts +0 -79
- package/dist/compare.d.ts.map +0 -1
- package/dist/compare.js +0 -232
- package/dist/compare.js.map +0 -1
package/dist/run-suite.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { cancelActiveClaudeRun, cancelActiveCursorRun, captureWorkingTreeStatus,
|
|
3
|
+
import { cancelActiveClaudeRun, cancelActiveCursorRun, cancelActiveOpenaiRun, captureWorkingTreeStatus, createSealedWorkspace, defaultSealedOverlayPaths, enrichTrace, filterWorkingTreeLeaks, findWorkingTreeLeak, formatWorkingTreeLeak, judgeCompareTraces, judgeTrace, loadContext, loadUnifiedDiffPaths, mergeMcpServers, parseScenarioWorkspace, partitionSeedCollateralLeaks, porcelainPathsFromLines, resolveAllowUserSkills, resolveHarnessArtifactIgnoreRoots, restoreWorkingTreePaths, runAgent, skillInvokeJudgeCriteria, skillPathsFromSetting, sumUsageParts, toolPathsOutsideWorkspace, traceEditsOutsideWorktree, traceHasUserInputTool, } from "@post-print/agent-harness";
|
|
4
|
+
import { applyCompareArm, applySidecarCompareDurations, assertCompareMetrics, compareArmLabel, compareStoryFields, prefixCompareFailures, } from "./compare-scenario.js";
|
|
4
5
|
import { collectDebugEnvironment, getDebugBundleDir, writeDebugBundle } from "./debug-bundle.js";
|
|
5
6
|
import { discoverSuites } from "./discover-suites.js";
|
|
6
7
|
import { assertRubric } from "./expect.js";
|
|
7
8
|
import { assertionFailure } from "./failures.js";
|
|
9
|
+
import { resolveSuiteHosts, scenarioRunsOnHost, uniqueHosts } from "./hosts.js";
|
|
8
10
|
import { failuresForLiveSubprocessExit, killActiveLiveChildren, liveScenarioIsolationEnabled, parentScenarioCounters, spawnLiveScenario, } from "./live-isolation.js";
|
|
9
11
|
import { resolveLiveTimeoutMs } from "./live-timeout.js";
|
|
10
12
|
import { loadSuiteFile } from "./load-suite.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
+
import { bindMcpServersToCaller, mcpStdioScriptPaths } from "./mcp-config.js";
|
|
14
|
+
import { formatDuration, logLive, logPhase, logProgress, logVerdict, refreshHeartbeat, withHeartbeat, } from "./progress.js";
|
|
15
|
+
import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingRootOverride, getLiveStagingSessionRoot, getStagingAgentStartPath, getStagingResultPath, getStagingTracePath, loadStagingResult, loadStagingTrace, recordTrace, resolveRecordingPath, setLiveStagingRootOverride, writeAgentStartMarker, writeStagingResult, } from "./record-trace.js";
|
|
13
16
|
import { resolveScenarioRetryMaxAttempts, shouldRetryAnnounceStopFlake } from "./scenario-retry.js";
|
|
14
17
|
import { captureCallerHead, restoreCallerHeadIfSeedCommit, seedScenarioWorktree, } from "./scenario-seed.js";
|
|
18
|
+
import { buildScenarioStory, pathFromArgs, quoteExcerpt } from "./scenario-story.js";
|
|
15
19
|
import { buildScenarioResultUsage, totalTokensFromScenarioUsage } from "./scenario-usage.js";
|
|
16
20
|
import { summarizeReportResults } from "./suite-summary.js";
|
|
17
21
|
import { theme } from "./theme.js";
|
|
22
|
+
import { validateSuiteFile } from "./validate-suite.js";
|
|
18
23
|
const require = createRequire(import.meta.url);
|
|
19
24
|
const packageVersion = require("../package.json").version;
|
|
20
25
|
let activeWorktreeCleanup;
|
|
@@ -37,6 +42,14 @@ async function restoreActiveCallerHead() {
|
|
|
37
42
|
function isChildProcess() {
|
|
38
43
|
return process.env.AGENT_TEST_CHILD === "1";
|
|
39
44
|
}
|
|
45
|
+
function resolveMaxConversationTurns() {
|
|
46
|
+
const raw = process.env.AGENT_TEST_MAX_TURNS?.trim();
|
|
47
|
+
if (!raw) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const parsed = Number(raw);
|
|
51
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
52
|
+
}
|
|
40
53
|
/** Parent process prints suite headers and final verdicts; children only print phases. */
|
|
41
54
|
export function shouldPrintSuiteChrome() {
|
|
42
55
|
return !isChildProcess();
|
|
@@ -51,6 +64,7 @@ export function registerLiveRunHandlers() {
|
|
|
51
64
|
killActiveLiveChildren();
|
|
52
65
|
cancelActiveCursorRun();
|
|
53
66
|
cancelActiveClaudeRun();
|
|
67
|
+
cancelActiveOpenaiRun();
|
|
54
68
|
const cleanup = activeWorktreeCleanup;
|
|
55
69
|
const headRestore = activeCallerHeadRestore;
|
|
56
70
|
if (!cleanup && !headRestore) {
|
|
@@ -98,6 +112,82 @@ function normalizeJudgeCriteria(judge) {
|
|
|
98
112
|
return { id: item.id ?? `judge-${index}`, question: item.question };
|
|
99
113
|
});
|
|
100
114
|
}
|
|
115
|
+
/** Explicit judge questions plus skill-invoke questions for mustInvokeSkill. */
|
|
116
|
+
export function collectJudgeCriteria(rubric) {
|
|
117
|
+
return [
|
|
118
|
+
...normalizeJudgeCriteria(rubric.judge),
|
|
119
|
+
...skillInvokeJudgeCriteria(rubric.mustInvokeSkill ?? []),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
/** Pairwise judge questions only. Skill-follow checks stay per-arm and deterministic. */
|
|
123
|
+
export function collectCompareJudgeCriteria(rubric) {
|
|
124
|
+
return normalizeJudgeCriteria(rubric.judge);
|
|
125
|
+
}
|
|
126
|
+
/** Judge auth and the judge call apply only when a rubric has judge work. */
|
|
127
|
+
export function judgeAuthRequired(judge, rubrics) {
|
|
128
|
+
return judge !== false && rubrics.some((rubric) => collectJudgeCriteria(rubric).length > 0);
|
|
129
|
+
}
|
|
130
|
+
/** Pairwise judge on compare. Skill-follow judge questions stay on non-compare scenarios. */
|
|
131
|
+
export function scenarioNeedsJudge(judge, scenario) {
|
|
132
|
+
if (judge === false) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (scenario.compare) {
|
|
136
|
+
return collectCompareJudgeCriteria(scenario.rubric).length > 0;
|
|
137
|
+
}
|
|
138
|
+
return collectJudgeCriteria(scenario.rubric).length > 0;
|
|
139
|
+
}
|
|
140
|
+
/** True when the selected live run will call the judge. */
|
|
141
|
+
export async function selectedRunNeedsJudge(options) {
|
|
142
|
+
if (options.judge === false) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
146
|
+
const filtered = options.filter
|
|
147
|
+
? suitePaths.filter((suitePath) => {
|
|
148
|
+
const suiteName = suiteNameFromPath(suitePath);
|
|
149
|
+
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
150
|
+
})
|
|
151
|
+
: suitePaths;
|
|
152
|
+
for (const suitePath of filtered) {
|
|
153
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
154
|
+
for (const scenario of suite.scenarios) {
|
|
155
|
+
if (scenario.skip) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (options.scenarioFilter && scenario.name !== options.scenarioFilter) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (scenarioNeedsJudge(true, scenario)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
export async function loadSelectedRubrics(options) {
|
|
169
|
+
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
170
|
+
const filtered = options.filter
|
|
171
|
+
? suitePaths.filter((suitePath) => {
|
|
172
|
+
const suiteName = suiteNameFromPath(suitePath);
|
|
173
|
+
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
174
|
+
})
|
|
175
|
+
: suitePaths;
|
|
176
|
+
const rubrics = [];
|
|
177
|
+
for (const suitePath of filtered) {
|
|
178
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
179
|
+
for (const scenario of suite.scenarios) {
|
|
180
|
+
if (scenario.skip) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (options.scenarioFilter && scenario.name !== options.scenarioFilter) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
rubrics.push(scenario.rubric);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return rubrics;
|
|
190
|
+
}
|
|
101
191
|
function questionForCriterion(criteria, id) {
|
|
102
192
|
return criteria.find((c) => c.id === id)?.question ?? id;
|
|
103
193
|
}
|
|
@@ -144,6 +234,7 @@ function emitScenarioVerdict(options) {
|
|
|
144
234
|
judgeVerdicts: options.judgeVerdicts,
|
|
145
235
|
rubricFailures: rubricFailuresOnly(options.failures),
|
|
146
236
|
failureCategory: options.failures[0]?.category,
|
|
237
|
+
story: options.story,
|
|
147
238
|
debug: options.debug,
|
|
148
239
|
debugBundleDir: options.debugBundleDir,
|
|
149
240
|
}));
|
|
@@ -155,7 +246,7 @@ async function maybeWriteDebugBundle(options) {
|
|
|
155
246
|
if (!options.stagingSessionId) {
|
|
156
247
|
return undefined;
|
|
157
248
|
}
|
|
158
|
-
const dir = getDebugBundleDir(options.stagingSessionId, options.suiteName, options.scenario.name, getLiveStagingSessionRoot);
|
|
249
|
+
const dir = getDebugBundleDir(options.stagingSessionId, options.suiteName, options.scenario.name, getLiveStagingSessionRoot, options.host);
|
|
159
250
|
const cliPath = process.argv[1] ?? resolve(options.cwd, "node_modules/@post-print/agent-test/dist/cli.js");
|
|
160
251
|
try {
|
|
161
252
|
await writeDebugBundle({
|
|
@@ -170,7 +261,7 @@ async function maybeWriteDebugBundle(options) {
|
|
|
170
261
|
host: options.host,
|
|
171
262
|
timeoutMs: options.timeoutMs,
|
|
172
263
|
worktree: options.worktree,
|
|
173
|
-
isolateLive:
|
|
264
|
+
isolateLive: liveScenarioIsolationEnabled(),
|
|
174
265
|
}),
|
|
175
266
|
rerun: {
|
|
176
267
|
cliPath,
|
|
@@ -179,7 +270,6 @@ async function maybeWriteDebugBundle(options) {
|
|
|
179
270
|
rubricsDir: options.rubricsDir,
|
|
180
271
|
suite: options.suiteName,
|
|
181
272
|
scenario: options.scenario.name,
|
|
182
|
-
live: options.live,
|
|
183
273
|
host: options.host,
|
|
184
274
|
judge: options.judge,
|
|
185
275
|
worktree: options.worktree,
|
|
@@ -210,10 +300,15 @@ export async function runSuite(options) {
|
|
|
210
300
|
if (options.debugDir !== undefined) {
|
|
211
301
|
setLiveStagingRootOverride(options.debugDir);
|
|
212
302
|
}
|
|
303
|
+
const ownsStagingSession = options.stagingSessionId === undefined;
|
|
304
|
+
const stagingSessionId = options.stagingSessionId ?? createLiveStagingSessionId();
|
|
213
305
|
try {
|
|
214
|
-
return await runSuiteBody(options);
|
|
306
|
+
return await runSuiteBody({ ...options, stagingSessionId });
|
|
215
307
|
}
|
|
216
308
|
finally {
|
|
309
|
+
if (ownsStagingSession && !options.keepRecordings && !options.debug) {
|
|
310
|
+
await cleanupStagingSession(getLiveStagingSessionRoot(stagingSessionId)).catch(() => undefined);
|
|
311
|
+
}
|
|
217
312
|
if (options.debugDir !== undefined) {
|
|
218
313
|
setLiveStagingRootOverride(previousStagingRoot);
|
|
219
314
|
}
|
|
@@ -221,24 +316,38 @@ export async function runSuite(options) {
|
|
|
221
316
|
}
|
|
222
317
|
async function runSuiteBody(options) {
|
|
223
318
|
const suite = await loadSuiteFile(options.suitePath, { rubricsDir: options.rubricsDir });
|
|
224
|
-
const
|
|
319
|
+
const validationIssues = validateSuiteFile(options.suitePath, suite);
|
|
320
|
+
if (validationIssues.length > 0) {
|
|
321
|
+
const details = validationIssues
|
|
322
|
+
.map((issue) => `${issue.scenario ? `${issue.scenario} · ` : ""}${issue.field}: ${issue.message}`)
|
|
323
|
+
.join("\n");
|
|
324
|
+
throw new Error(`Invalid suite file ${options.suitePath}:\n${details}`);
|
|
325
|
+
}
|
|
326
|
+
const defaultHost = options.host ?? suite.defaults?.host ?? "cursor";
|
|
225
327
|
const results = [];
|
|
226
|
-
const
|
|
328
|
+
const selected = options.scenarioFilter
|
|
227
329
|
? suite.scenarios.filter((scenario) => scenario.name === options.scenarioFilter)
|
|
228
330
|
: suite.scenarios;
|
|
229
|
-
if (options.scenarioFilter &&
|
|
331
|
+
if (options.scenarioFilter && selected.length === 0) {
|
|
230
332
|
throw new Error(`Scenario not found: ${options.scenarioFilter}`);
|
|
231
333
|
}
|
|
334
|
+
const hostLocked = options.hostLocked === true;
|
|
335
|
+
const scenarios = hostLocked
|
|
336
|
+
? selected.filter((scenario) => scenarioRunsOnHost(scenario, defaultHost, true))
|
|
337
|
+
: selected;
|
|
338
|
+
if (hostLocked && options.scenarioFilter && selected.length > 0 && scenarios.length === 0) {
|
|
339
|
+
const pinned = selected[0]?.host;
|
|
340
|
+
throw new Error(`Scenario ${options.scenarioFilter} is pinned to ${pinned} (this run is ${defaultHost})`);
|
|
341
|
+
}
|
|
232
342
|
const filteredTotal = scenarios.length;
|
|
233
343
|
const parentCounters = parentScenarioCounters();
|
|
234
344
|
const displayTotal = parentCounters?.total ?? filteredTotal;
|
|
235
|
-
const
|
|
236
|
-
const isolateLive = isLiveSuite && liveScenarioIsolationEnabled() && !options.scenarioFilter && filteredTotal > 1;
|
|
345
|
+
const isolateLive = liveScenarioIsolationEnabled() && !options.scenarioFilter && filteredTotal > 1;
|
|
237
346
|
let previousIsolatedExitCode;
|
|
238
347
|
if (shouldPrintSuiteChrome()) {
|
|
239
348
|
logProgress(`\n${theme.suiteHeader(suite.name, defaultHost, displayTotal)}`);
|
|
240
349
|
if (isolateLive) {
|
|
241
|
-
logProgress(`
|
|
350
|
+
logProgress(` ${theme.isolationNote()}`);
|
|
242
351
|
}
|
|
243
352
|
}
|
|
244
353
|
for (let index = 0; index < scenarios.length; index++) {
|
|
@@ -255,25 +364,33 @@ async function runSuiteBody(options) {
|
|
|
255
364
|
results.push({
|
|
256
365
|
suite: suite.name,
|
|
257
366
|
scenario: scenario.name,
|
|
367
|
+
prompt: scenario.prompt,
|
|
258
368
|
passed: true,
|
|
259
369
|
failures: [],
|
|
260
370
|
skipped: true,
|
|
261
371
|
durationMs: 0,
|
|
372
|
+
story: buildScenarioStory({
|
|
373
|
+
rubric: scenario.rubric,
|
|
374
|
+
passed: true,
|
|
375
|
+
skipped: true,
|
|
376
|
+
failures: [],
|
|
377
|
+
}),
|
|
262
378
|
});
|
|
263
379
|
continue;
|
|
264
380
|
}
|
|
265
381
|
const started = performance.now();
|
|
266
382
|
const debug = isDebugEnabled(options);
|
|
267
|
-
const maxAttempts = !isChildProcess()
|
|
383
|
+
const maxAttempts = !isChildProcess()
|
|
268
384
|
? resolveScenarioRetryMaxAttempts(options.scenarioRetries)
|
|
269
385
|
: 1;
|
|
270
386
|
let attempts = 0;
|
|
271
387
|
let failures = [];
|
|
272
388
|
let scenarioTrace;
|
|
273
389
|
let previousAttemptExitCode = previousIsolatedExitCode;
|
|
390
|
+
let childSidecar;
|
|
274
391
|
while (true) {
|
|
275
392
|
attempts++;
|
|
276
|
-
const
|
|
393
|
+
const spawned = await spawnLiveScenario({
|
|
277
394
|
cwd: options.cwd,
|
|
278
395
|
suiteName: suite.name,
|
|
279
396
|
scenarioName: scenario.name,
|
|
@@ -282,10 +399,10 @@ async function runSuiteBody(options) {
|
|
|
282
399
|
suiteFilter: options.suiteFilter ?? suite.name,
|
|
283
400
|
stagingSessionId: options.stagingSessionId,
|
|
284
401
|
keepRecordings: options.keepRecordings,
|
|
285
|
-
recordFixtures: options.recordFixtures,
|
|
286
402
|
worktree: options.worktree,
|
|
287
403
|
judge: options.judge,
|
|
288
404
|
host: defaultHost,
|
|
405
|
+
adapterModules: options.adapterModules,
|
|
289
406
|
scenarioIndex: index + 1,
|
|
290
407
|
scenarioTotal: filteredTotal,
|
|
291
408
|
timeoutMs: resolveLiveTimeoutMs(options.timeoutMs),
|
|
@@ -295,23 +412,34 @@ async function runSuiteBody(options) {
|
|
|
295
412
|
debugDir: options.debugDir,
|
|
296
413
|
previousExitCode: previousAttemptExitCode,
|
|
297
414
|
});
|
|
298
|
-
previousAttemptExitCode = exitCode;
|
|
299
|
-
previousIsolatedExitCode = exitCode;
|
|
415
|
+
previousAttemptExitCode = spawned.exitCode;
|
|
416
|
+
previousIsolatedExitCode = spawned.exitCode;
|
|
300
417
|
failures = [];
|
|
301
418
|
scenarioTrace = undefined;
|
|
302
|
-
|
|
303
|
-
|
|
419
|
+
childSidecar =
|
|
420
|
+
options.stagingSessionId !== undefined
|
|
304
421
|
? await loadStagingResult(getStagingResultPath(options.stagingSessionId, suite.name, scenario.name))
|
|
305
422
|
: undefined;
|
|
306
|
-
|
|
423
|
+
if (spawned.exitCode !== 0) {
|
|
424
|
+
failures.push(...failuresForLiveSubprocessExit(spawned.exitCode, childSidecar, spawned.stderr));
|
|
307
425
|
}
|
|
308
426
|
if (options.stagingSessionId) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
427
|
+
if (scenario.compare) {
|
|
428
|
+
try {
|
|
429
|
+
scenarioTrace = await loadStagingTrace(getStagingTracePath(options.stagingSessionId, suite.name, scenario.name, "a"));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Trace may be missing when the child crashed before recording.
|
|
433
|
+
}
|
|
312
434
|
}
|
|
313
|
-
|
|
314
|
-
|
|
435
|
+
else {
|
|
436
|
+
const tracePath = getStagingTracePath(options.stagingSessionId, suite.name, scenario.name);
|
|
437
|
+
try {
|
|
438
|
+
scenarioTrace = await loadStagingTrace(tracePath);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Trace may be missing when the child crashed before recording.
|
|
442
|
+
}
|
|
315
443
|
}
|
|
316
444
|
}
|
|
317
445
|
const canRetry = failures.length > 0 &&
|
|
@@ -324,38 +452,79 @@ async function runSuiteBody(options) {
|
|
|
324
452
|
break;
|
|
325
453
|
}
|
|
326
454
|
let judgeVerdicts;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
455
|
+
let compareResult;
|
|
456
|
+
const judgeHost = options.host ?? scenario.host ?? suite.defaults?.host ?? "cursor";
|
|
457
|
+
if (scenario.compare && options.stagingSessionId) {
|
|
458
|
+
compareResult = applySidecarCompareDurations(await loadCompareResultFromStaging(options.stagingSessionId, suite.name, scenario), childSidecar);
|
|
459
|
+
scenarioTrace = compareResult.a.trace ?? scenarioTrace;
|
|
460
|
+
}
|
|
461
|
+
if (failures.length === 0 && options.judge !== false) {
|
|
462
|
+
if (scenario.compare && compareResult?.a.trace && compareResult.b.trace) {
|
|
463
|
+
const criteria = collectCompareJudgeCriteria(scenario.rubric);
|
|
464
|
+
if (criteria.length > 0) {
|
|
465
|
+
releaseLiveMemory();
|
|
466
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
467
|
+
try {
|
|
468
|
+
const judged = await runCompareJudgeRubric(compareResult, scenario.rubric, options.cwd, judgeHost);
|
|
469
|
+
failures.push(...judged.failures);
|
|
470
|
+
judgeVerdicts = toJudgeVerdictResults({ ...compareResult.a.trace, judgeVerdicts: judged.verdicts }, criteria, judged.verdicts);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to judge compare traces", "judge_infra"));
|
|
474
|
+
}
|
|
337
475
|
}
|
|
338
|
-
|
|
339
|
-
|
|
476
|
+
}
|
|
477
|
+
else if (scenarioTrace && !scenario.compare) {
|
|
478
|
+
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
479
|
+
if (criteria.length > 0) {
|
|
480
|
+
releaseLiveMemory();
|
|
481
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
482
|
+
try {
|
|
483
|
+
const judged = await runJudgeRubric(scenarioTrace, scenario.rubric, options.cwd, judgeHost);
|
|
484
|
+
failures.push(...judged.failures);
|
|
485
|
+
scenarioTrace = judged.trace;
|
|
486
|
+
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to load staging trace for judge", "judge_infra"));
|
|
490
|
+
}
|
|
340
491
|
}
|
|
341
492
|
}
|
|
342
493
|
}
|
|
343
494
|
const durationMs = Math.round(performance.now() - started);
|
|
344
495
|
const passed = failures.length === 0;
|
|
345
496
|
const usageFields = buildScenarioResultUsage({
|
|
346
|
-
agentUsage:
|
|
497
|
+
agentUsage: compareResult
|
|
498
|
+
? sumUsageParts([compareResult.a.trace?.usage, compareResult.b.trace?.usage])
|
|
499
|
+
: scenarioTrace?.usage,
|
|
347
500
|
judgeVerdicts,
|
|
348
501
|
});
|
|
502
|
+
const story = buildScenarioStory({
|
|
503
|
+
rubric: scenario.rubric,
|
|
504
|
+
trace: scenarioTrace,
|
|
505
|
+
passed,
|
|
506
|
+
failures,
|
|
507
|
+
judgeVerdicts,
|
|
508
|
+
compare: compareResult
|
|
509
|
+
? {
|
|
510
|
+
...compareStoryFields(scenario),
|
|
511
|
+
aTrace: compareResult.a.trace,
|
|
512
|
+
bTrace: compareResult.b.trace,
|
|
513
|
+
}
|
|
514
|
+
: undefined,
|
|
515
|
+
});
|
|
349
516
|
const scenarioResult = {
|
|
350
517
|
suite: suite.name,
|
|
351
518
|
scenario: scenario.name,
|
|
352
|
-
|
|
519
|
+
prompt: scenario.prompt,
|
|
353
520
|
passed,
|
|
354
521
|
failures,
|
|
355
522
|
durationMs,
|
|
356
523
|
attempts,
|
|
357
524
|
judgeVerdicts,
|
|
358
525
|
trace: scenarioTrace,
|
|
526
|
+
compare: compareResult,
|
|
527
|
+
story,
|
|
359
528
|
...usageFields,
|
|
360
529
|
};
|
|
361
530
|
const debugBundleDir = await maybeWriteDebugBundle({
|
|
@@ -373,7 +542,6 @@ async function runSuiteBody(options) {
|
|
|
373
542
|
timeoutMs: options.timeoutMs,
|
|
374
543
|
worktree: options.worktree,
|
|
375
544
|
judge: options.judge,
|
|
376
|
-
live: true,
|
|
377
545
|
allowUserInput: options.allowUserInput,
|
|
378
546
|
keepRecordings: options.keepRecordings,
|
|
379
547
|
});
|
|
@@ -387,6 +555,7 @@ async function runSuiteBody(options) {
|
|
|
387
555
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, scenarioTrace?.usage),
|
|
388
556
|
judgeVerdicts,
|
|
389
557
|
failures,
|
|
558
|
+
story,
|
|
390
559
|
debug,
|
|
391
560
|
debugBundleDir,
|
|
392
561
|
});
|
|
@@ -394,67 +563,27 @@ async function runSuiteBody(options) {
|
|
|
394
563
|
releaseLiveMemory();
|
|
395
564
|
continue;
|
|
396
565
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const debugBundleDir = await maybeWriteDebugBundle({
|
|
419
|
-
debug,
|
|
420
|
-
cwd: options.cwd,
|
|
421
|
-
suitesDir: options.suitesDir ?? "agent-suites",
|
|
422
|
-
rubricsDir: options.rubricsDir,
|
|
423
|
-
stagingSessionId: options.stagingSessionId,
|
|
424
|
-
debugDir: options.debugDir,
|
|
425
|
-
suiteName: suite.name,
|
|
426
|
-
scenario,
|
|
427
|
-
host: scenario.host ?? defaultHost,
|
|
428
|
-
result: scenarioResult,
|
|
429
|
-
trace: scenarioResult.trace,
|
|
430
|
-
timeoutMs: options.timeoutMs,
|
|
431
|
-
worktree: options.worktree,
|
|
432
|
-
judge: options.judge,
|
|
433
|
-
live: true,
|
|
434
|
-
allowUserInput: options.allowUserInput,
|
|
435
|
-
keepRecordings: options.keepRecordings,
|
|
436
|
-
});
|
|
437
|
-
scenarioResult.debugBundleDir = debugBundleDir;
|
|
438
|
-
emitScenarioVerdict({
|
|
439
|
-
passed: scenarioResult.passed,
|
|
440
|
-
index: scenarioIndex,
|
|
441
|
-
total: scenarioTotal,
|
|
442
|
-
name: scenario.name,
|
|
443
|
-
durationMs: scenarioResult.durationMs,
|
|
444
|
-
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, scenarioResult.trace?.usage),
|
|
445
|
-
judgeVerdicts: scenarioResult.judgeVerdicts,
|
|
446
|
-
failures: scenarioResult.failures,
|
|
447
|
-
debug,
|
|
448
|
-
debugBundleDir,
|
|
449
|
-
});
|
|
450
|
-
results.push(scenarioResult);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
results.push(await runScenario(options.cwd, suite.name, scenario, defaultHost, suite.defaults?.profile, suite.defaults?.skills, suite.defaults?.contextSources, suite.defaults?.mcpServers, options.record, options.recordFixtures, options.judge, options.worktree, options.stagingSessionId, scenarioIndex, scenarioTotal, options.timeoutMs, options.allowUserInput, options.debug, options.debugDir, options.suitesDir ?? "agent-suites", options.keepRecordings, options.rubricsDir));
|
|
454
|
-
}
|
|
455
|
-
if (isLiveSuite) {
|
|
456
|
-
releaseLiveMemory();
|
|
457
|
-
}
|
|
566
|
+
results.push(await runAgentTest({
|
|
567
|
+
cwd: options.cwd,
|
|
568
|
+
suiteName: suite.name,
|
|
569
|
+
scenario,
|
|
570
|
+
defaults: suite.defaults,
|
|
571
|
+
host: options.host,
|
|
572
|
+
judge: options.judge,
|
|
573
|
+
worktree: options.worktree,
|
|
574
|
+
stagingSessionId: options.stagingSessionId,
|
|
575
|
+
scenarioIndex,
|
|
576
|
+
scenarioTotal,
|
|
577
|
+
timeoutMs: options.timeoutMs,
|
|
578
|
+
allowUserInput: options.allowUserInput,
|
|
579
|
+
debug: options.debug,
|
|
580
|
+
debugDir: options.debugDir,
|
|
581
|
+
suitesDir: options.suitesDir,
|
|
582
|
+
keepRecordings: options.keepRecordings,
|
|
583
|
+
rubricsDir: options.rubricsDir,
|
|
584
|
+
scenarioRetries: isChildProcess() ? 0 : options.scenarioRetries,
|
|
585
|
+
}));
|
|
586
|
+
releaseLiveMemory();
|
|
458
587
|
}
|
|
459
588
|
return {
|
|
460
589
|
suite: suite.name,
|
|
@@ -466,11 +595,139 @@ async function runSuiteBody(options) {
|
|
|
466
595
|
summary: summarizeReportResults(results),
|
|
467
596
|
};
|
|
468
597
|
}
|
|
598
|
+
/** Suite trees and MCP scripts from the caller, so uncommitted fixtures reach the host. */
|
|
599
|
+
function liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers) {
|
|
600
|
+
const extras = [...(contextSources ?? [])];
|
|
601
|
+
const suitesRel = overlayRelPath(cwd, suitesDir);
|
|
602
|
+
if (suitesRel && !extras.includes(suitesRel)) {
|
|
603
|
+
extras.push(suitesRel);
|
|
604
|
+
}
|
|
605
|
+
for (const script of mcpStdioScriptPaths(mcpServers)) {
|
|
606
|
+
if (!extras.includes(script)) {
|
|
607
|
+
extras.push(script);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return extras;
|
|
611
|
+
}
|
|
612
|
+
function overlayRelPath(cwd, path) {
|
|
613
|
+
const trimmed = path.replace(/^\.\//, "").trim();
|
|
614
|
+
if (trimmed.length === 0) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
if (!trimmed.startsWith("/")) {
|
|
618
|
+
return trimmed;
|
|
619
|
+
}
|
|
620
|
+
const root = resolve(cwd);
|
|
621
|
+
if (trimmed === root) {
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
if (trimmed.startsWith(`${root}/`)) {
|
|
625
|
+
return trimmed.slice(root.length + 1);
|
|
626
|
+
}
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
function resolveRunWorkspace(defaultWorkspace, scenarioWorkspace) {
|
|
630
|
+
const raw = scenarioWorkspace !== undefined ? scenarioWorkspace : defaultWorkspace;
|
|
631
|
+
const parsed = parseScenarioWorkspace(raw);
|
|
632
|
+
if (!parsed.ok) {
|
|
633
|
+
throw new Error(parsed.message);
|
|
634
|
+
}
|
|
635
|
+
return parsed.rel;
|
|
636
|
+
}
|
|
469
637
|
function mergeContextSources(defaults, scenarioSources) {
|
|
470
638
|
const merged = [...(defaults ?? []), ...(scenarioSources ?? [])].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
471
639
|
return merged.length > 0 ? merged : undefined;
|
|
472
640
|
}
|
|
473
|
-
|
|
641
|
+
function defaultProfileForHost(host) {
|
|
642
|
+
if (host === "cursor") {
|
|
643
|
+
return "cursor";
|
|
644
|
+
}
|
|
645
|
+
if (host === "claude") {
|
|
646
|
+
return "claude";
|
|
647
|
+
}
|
|
648
|
+
return "shared";
|
|
649
|
+
}
|
|
650
|
+
/** Run one real agent scenario. JSON suites delegate to this same execution boundary. */
|
|
651
|
+
export async function runAgentTest(options) {
|
|
652
|
+
const previousStagingRoot = getLiveStagingRootOverride();
|
|
653
|
+
if (options.debugDir !== undefined) {
|
|
654
|
+
setLiveStagingRootOverride(options.debugDir);
|
|
655
|
+
}
|
|
656
|
+
const stagingSessionId = options.stagingSessionId ??
|
|
657
|
+
(options.debug || options.keepRecordings ? createLiveStagingSessionId() : undefined);
|
|
658
|
+
try {
|
|
659
|
+
return await runAgentTestBody({ ...options, stagingSessionId });
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
if (options.debugDir !== undefined) {
|
|
663
|
+
setLiveStagingRootOverride(previousStagingRoot);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function runAgentTestBody(options) {
|
|
668
|
+
const legacyScenario = options.scenario;
|
|
669
|
+
const legacyDefaults = options.defaults;
|
|
670
|
+
if (legacyScenario.host === "replay" ||
|
|
671
|
+
legacyDefaults?.host === "replay" ||
|
|
672
|
+
"replayTrace" in legacyScenario) {
|
|
673
|
+
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor, Claude, or OpenAI.");
|
|
674
|
+
}
|
|
675
|
+
const suiteName = options.suiteName ?? "direct";
|
|
676
|
+
const defaultHost = options.host ?? options.defaults?.host ?? "cursor";
|
|
677
|
+
const maxAttempts = resolveScenarioRetryMaxAttempts(options.scenarioRetries);
|
|
678
|
+
let attempts = 0;
|
|
679
|
+
let result;
|
|
680
|
+
while (true) {
|
|
681
|
+
attempts++;
|
|
682
|
+
result = await runAgentTestOnce(options.cwd, suiteName, options.scenario, defaultHost, 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 });
|
|
683
|
+
const canRetry = !result.skipped &&
|
|
684
|
+
!result.passed &&
|
|
685
|
+
attempts < maxAttempts &&
|
|
686
|
+
shouldRetryAnnounceStopFlake(result.failures, result.trace);
|
|
687
|
+
if (!canRetry) {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
logPhase(theme.phase("retry", `${attempts}/${maxAttempts - 1}`));
|
|
691
|
+
}
|
|
692
|
+
result.attempts = attempts;
|
|
693
|
+
if (maxAttempts > 1) {
|
|
694
|
+
const debug = isDebugEnabled(options);
|
|
695
|
+
const debugBundleDir = await maybeWriteDebugBundle({
|
|
696
|
+
debug,
|
|
697
|
+
cwd: options.cwd,
|
|
698
|
+
suitesDir: options.suitesDir ?? "agent-suites",
|
|
699
|
+
rubricsDir: options.rubricsDir,
|
|
700
|
+
stagingSessionId: options.stagingSessionId,
|
|
701
|
+
debugDir: options.debugDir,
|
|
702
|
+
suiteName,
|
|
703
|
+
scenario: options.scenario,
|
|
704
|
+
host: options.scenario.host ?? defaultHost,
|
|
705
|
+
result,
|
|
706
|
+
trace: result.trace,
|
|
707
|
+
timeoutMs: options.timeoutMs,
|
|
708
|
+
worktree: options.worktree,
|
|
709
|
+
judge: options.judge ?? true,
|
|
710
|
+
allowUserInput: options.allowUserInput,
|
|
711
|
+
keepRecordings: options.keepRecordings,
|
|
712
|
+
});
|
|
713
|
+
result.debugBundleDir = debugBundleDir;
|
|
714
|
+
emitScenarioVerdict({
|
|
715
|
+
passed: result.passed,
|
|
716
|
+
index: options.scenarioIndex,
|
|
717
|
+
total: options.scenarioTotal,
|
|
718
|
+
name: options.scenario.name,
|
|
719
|
+
durationMs: result.durationMs,
|
|
720
|
+
totalTokens: totalTokensFromScenarioUsage(result.usage, result.trace?.usage),
|
|
721
|
+
judgeVerdicts: result.judgeVerdicts,
|
|
722
|
+
failures: result.failures,
|
|
723
|
+
story: result.story,
|
|
724
|
+
debug,
|
|
725
|
+
debugBundleDir,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir = "agent-suites", keepRecordings, rubricsDir, runOptions) {
|
|
474
731
|
const started = performance.now();
|
|
475
732
|
const debug = isDebugEnabled({ debug: debugFlag });
|
|
476
733
|
const suppressEmit = runOptions?.suppressEmit === true;
|
|
@@ -482,99 +739,113 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
482
739
|
return {
|
|
483
740
|
suite: suiteName,
|
|
484
741
|
scenario: scenario.name,
|
|
742
|
+
prompt: scenario.prompt,
|
|
485
743
|
passed: true,
|
|
486
744
|
failures: [],
|
|
487
745
|
skipped: true,
|
|
488
746
|
durationMs: 0,
|
|
747
|
+
story: buildScenarioStory({
|
|
748
|
+
rubric: scenario.rubric,
|
|
749
|
+
passed: true,
|
|
750
|
+
skipped: true,
|
|
751
|
+
failures: [],
|
|
752
|
+
}),
|
|
489
753
|
};
|
|
490
754
|
}
|
|
755
|
+
if (scenario.compare && runOptions?.compareArm === undefined) {
|
|
756
|
+
return runCompareAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir, keepRecordings, rubricsDir, { suppressEmit });
|
|
757
|
+
}
|
|
491
758
|
const host = scenario.host ?? defaultHost;
|
|
492
|
-
const profile = scenario.profile ?? defaultProfile ?? (host
|
|
759
|
+
const profile = scenario.profile ?? defaultProfile ?? defaultProfileForHost(host);
|
|
493
760
|
const skills = scenario.skills ?? defaultSkills;
|
|
494
761
|
const contextSources = mergeContextSources(defaultContextSources, scenario.contextSources);
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
const
|
|
762
|
+
const workspaceRel = resolveRunWorkspace(defaultWorkspace, scenario.workspace);
|
|
763
|
+
const fixtureWorkspace = workspaceRel !== undefined;
|
|
764
|
+
const allowUserSkills = resolveAllowUserSkills(scenario.allowUserSkills, defaultAllowUserSkills);
|
|
765
|
+
const mcpServers = fixtureWorkspace
|
|
766
|
+
? bindMcpServersToCaller(mergeMcpServers(defaultMcpServers, scenario.mcpServers), cwd)
|
|
767
|
+
: mergeMcpServers(defaultMcpServers, scenario.mcpServers);
|
|
768
|
+
const liveTimeoutMs = resolveLiveTimeoutMs(timeoutMs);
|
|
498
769
|
const failOnUserInput = !allowUserInput;
|
|
499
|
-
if (
|
|
770
|
+
if (runOptions?.compareArm) {
|
|
771
|
+
logPhase(theme.phase("arm", `${runOptions.compareArm.toUpperCase()} ${host}`));
|
|
772
|
+
}
|
|
773
|
+
else if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
500
774
|
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
501
775
|
}
|
|
502
776
|
else {
|
|
503
777
|
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
504
778
|
}
|
|
505
|
-
const useWorktree =
|
|
779
|
+
const useWorktree = worktree !== false && !process.env.AGENT_TEST_NO_WORKTREE;
|
|
506
780
|
let worktreeHandle;
|
|
507
781
|
let callerHeadBefore;
|
|
508
782
|
const callerTreeBefore = useWorktree ? await captureWorkingTreeStatus(cwd) : undefined;
|
|
509
783
|
if (useWorktree) {
|
|
510
|
-
if (
|
|
784
|
+
if (scenario.seedPatch) {
|
|
511
785
|
callerHeadBefore = await captureCallerHead(cwd);
|
|
512
786
|
setCallerHeadRestore(cwd, callerHeadBefore);
|
|
513
787
|
}
|
|
514
|
-
worktreeHandle = await
|
|
788
|
+
worktreeHandle = await createSealedWorkspace({
|
|
789
|
+
callerCwd: cwd,
|
|
790
|
+
workspace: workspaceRel,
|
|
791
|
+
overlayPaths: fixtureWorkspace
|
|
792
|
+
? undefined
|
|
793
|
+
: defaultSealedOverlayPaths(liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers), skillPathsFromSetting(skills)),
|
|
794
|
+
});
|
|
515
795
|
activeWorktreeCleanup = worktreeHandle.cleanup;
|
|
516
|
-
|
|
517
|
-
if (isLive && scenario.seedPatch) {
|
|
796
|
+
if (scenario.seedPatch) {
|
|
518
797
|
logPhase(theme.phase("seed", theme.basename(scenario.seedPatch)));
|
|
519
798
|
await seedScenarioWorktree(cwd, worktreeHandle.path, scenario.seedPatch, {
|
|
520
799
|
stageOnly: scenario.seedStageOnly === true,
|
|
521
800
|
});
|
|
522
801
|
}
|
|
523
802
|
}
|
|
524
|
-
else
|
|
803
|
+
else {
|
|
525
804
|
logPhase(theme.phase("worktree", theme.phaseDim("disabled (AGENT_TEST_ALLOW_IN_PLACE=1)")));
|
|
526
805
|
}
|
|
527
|
-
const runCwd = worktreeHandle?.path ?? cwd;
|
|
806
|
+
const runCwd = worktreeHandle?.path ?? (workspaceRel ? resolve(cwd, workspaceRel) : cwd);
|
|
528
807
|
try {
|
|
529
|
-
logPhase(theme.phase("context"));
|
|
530
|
-
// Live worktree runs code in an isolated checkout; load rules/AGENTS from caller cwd
|
|
531
|
-
// so uncommitted .cursor/rules and AGENTS.md edits apply during dogfood.
|
|
532
|
-
const contextRoot = isLive && useWorktree ? cwd : runCwd;
|
|
533
808
|
const context = await loadContext({
|
|
534
|
-
cwd:
|
|
809
|
+
cwd: runCwd,
|
|
535
810
|
profile,
|
|
536
811
|
skills,
|
|
537
812
|
contextSources,
|
|
538
813
|
});
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
logPhase(theme.phase("replay", theme.path(scenario.replayTrace ?? "trace")));
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
logPhase(theme.phase("agent"));
|
|
545
|
-
}
|
|
546
|
-
const outputContract = isLive ? outputContractForRubric(scenario.rubric) : undefined;
|
|
547
|
-
const agentStartMarkerPath = isChildProcess() && isLive && stagingSessionId
|
|
814
|
+
const outputContract = outputContractForRubric(scenario.rubric);
|
|
815
|
+
const agentStartMarkerPath = isChildProcess() && stagingSessionId
|
|
548
816
|
? getStagingAgentStartPath(stagingSessionId, suiteName, scenario.name)
|
|
549
817
|
: undefined;
|
|
818
|
+
logPhase(theme.phase("agent", theme.phaseDim("started")));
|
|
550
819
|
const agentStarted = performance.now();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
820
|
+
let livePreview;
|
|
821
|
+
const session = await withHeartbeat(runAgent({
|
|
822
|
+
host,
|
|
823
|
+
cwd: runCwd,
|
|
824
|
+
context,
|
|
825
|
+
profile,
|
|
826
|
+
prompt: scenario.prompt,
|
|
827
|
+
outputContract,
|
|
828
|
+
mcpServers,
|
|
829
|
+
allowUserSkills,
|
|
830
|
+
timeoutMs: liveTimeoutMs,
|
|
831
|
+
failOnUserInput,
|
|
832
|
+
maxConversationTurns: resolveMaxConversationTurns(),
|
|
833
|
+
onDeadlineStart: agentStartMarkerPath
|
|
834
|
+
? () => writeAgentStartMarker(agentStartMarkerPath)
|
|
835
|
+
: undefined,
|
|
836
|
+
onAgentEvent: (event) => {
|
|
837
|
+
if (event.type === "tool") {
|
|
838
|
+
logLive(theme.liveTool(event.name, pathFromArgs(event.args)));
|
|
839
|
+
livePreview = undefined;
|
|
840
|
+
refreshHeartbeat();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Clock tick paints the preview. Do not rewrite here — a long
|
|
844
|
+
// line wraps and `\r` cannot clear the leftover row.
|
|
845
|
+
livePreview = quoteExcerpt(event.text);
|
|
846
|
+
},
|
|
847
|
+
}), { started: agentStarted, preview: () => livePreview });
|
|
848
|
+
logPhase(theme.phase("agent", `${theme.statusCompleted(session.status)} ${theme.duration(formatDuration(performance.now() - agentStarted))}`));
|
|
578
849
|
let trace = enrichTrace(session.trace);
|
|
579
850
|
const failures = [];
|
|
580
851
|
if (session.status !== "completed") {
|
|
@@ -597,18 +868,23 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
597
868
|
.join("\n");
|
|
598
869
|
failures.push(assertionFailure("runAgent", session.error ?? `agent session ${session.status}`, "agent_runtime", runtimeEvidence));
|
|
599
870
|
}
|
|
600
|
-
else if (
|
|
871
|
+
else if (failOnUserInput && traceHasUserInputTool(trace.toolCalls)) {
|
|
601
872
|
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(", ")}`));
|
|
602
873
|
}
|
|
603
|
-
logPhase(theme.phase("rubric"));
|
|
604
874
|
failures.push(...assertRubric(trace, scenario.rubric, {
|
|
605
875
|
skillsMode: context.skillsMode,
|
|
606
876
|
}));
|
|
877
|
+
if (worktreeHandle) {
|
|
878
|
+
const escaped = toolPathsOutsideWorkspace(trace, worktreeHandle.path);
|
|
879
|
+
if (escaped.length > 0) {
|
|
880
|
+
failures.push(assertionFailure("workingTreeLeak", `agent used paths outside the sealed workspace: ${escaped.join(", ")}`, "worktree_leak", `escaped=${escaped.join(", ")}`));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
607
883
|
if (useWorktree && callerTreeBefore !== undefined) {
|
|
608
884
|
const callerTreeAfter = await captureWorkingTreeStatus(cwd);
|
|
609
885
|
const ignoreRoots = resolveHarnessArtifactIgnoreRoots(cwd, getLiveStagingRootOverride());
|
|
610
886
|
const leaked = filterWorkingTreeLeaks(findWorkingTreeLeak(callerTreeBefore, callerTreeAfter), ignoreRoots, cwd);
|
|
611
|
-
const seedPaths =
|
|
887
|
+
const seedPaths = scenario.seedPatch
|
|
612
888
|
? await loadUnifiedDiffPaths(resolve(cwd, scenario.seedPatch)).catch(() => [])
|
|
613
889
|
: [];
|
|
614
890
|
const outsideEdits = worktreeHandle !== undefined
|
|
@@ -634,33 +910,32 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
634
910
|
].join("\n")));
|
|
635
911
|
}
|
|
636
912
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
logPhase(theme.phase(recordLabel, theme.path(path)));
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
failures.push(assertionFailure("recordTrace", error instanceof Error ? error.message : "failed to record trace", "recording_error"));
|
|
913
|
+
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId, runOptions?.compareArm);
|
|
914
|
+
if (stagingTracePath) {
|
|
915
|
+
try {
|
|
916
|
+
const path = await recordTrace(stagingTracePath, trace);
|
|
917
|
+
if (keepRecordings || debug) {
|
|
918
|
+
logPhase(theme.phase("trace", theme.path(path)));
|
|
647
919
|
}
|
|
648
920
|
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
failures.push(assertionFailure("recordTrace", error instanceof Error ? error.message : "failed to record trace", "recording_error"));
|
|
923
|
+
}
|
|
649
924
|
}
|
|
650
925
|
const deferJudgeToParent = isChildProcess();
|
|
651
926
|
let judgeVerdicts;
|
|
652
|
-
if (judge &&
|
|
653
|
-
const criteria =
|
|
927
|
+
if (judge && !deferJudgeToParent) {
|
|
928
|
+
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
654
929
|
if (criteria.length > 0) {
|
|
655
930
|
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
656
931
|
}
|
|
657
|
-
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd);
|
|
932
|
+
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd, host);
|
|
658
933
|
failures.push(...judged.failures);
|
|
659
934
|
trace = judged.trace;
|
|
660
935
|
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
661
936
|
}
|
|
662
937
|
const durationMs = Math.round(performance.now() - started);
|
|
663
|
-
if (isChildProcess() &&
|
|
938
|
+
if (isChildProcess() && stagingSessionId && runOptions?.writeSidecar !== false) {
|
|
664
939
|
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
665
940
|
passed: failures.length === 0,
|
|
666
941
|
failures,
|
|
@@ -668,11 +943,6 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
668
943
|
});
|
|
669
944
|
}
|
|
670
945
|
if (worktreeHandle) {
|
|
671
|
-
const willJudge = Boolean(judge) &&
|
|
672
|
-
isLive &&
|
|
673
|
-
!isChildProcess() &&
|
|
674
|
-
normalizeJudgeCriteria(scenario.rubric.judge).length > 0;
|
|
675
|
-
logPhase(theme.phase("cleanup"), { last: !willJudge });
|
|
676
946
|
await worktreeHandle.cleanup();
|
|
677
947
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
678
948
|
activeWorktreeCleanup = undefined;
|
|
@@ -682,15 +952,24 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
682
952
|
}
|
|
683
953
|
worktreeHandle = undefined;
|
|
684
954
|
}
|
|
955
|
+
const passed = failures.length === 0;
|
|
956
|
+
const story = buildScenarioStory({
|
|
957
|
+
rubric: scenario.rubric,
|
|
958
|
+
trace,
|
|
959
|
+
passed,
|
|
960
|
+
failures,
|
|
961
|
+
judgeVerdicts,
|
|
962
|
+
});
|
|
685
963
|
const scenarioResult = {
|
|
686
964
|
suite: suiteName,
|
|
687
965
|
scenario: scenario.name,
|
|
688
|
-
|
|
689
|
-
passed
|
|
966
|
+
prompt: scenario.prompt,
|
|
967
|
+
passed,
|
|
690
968
|
failures,
|
|
691
969
|
durationMs,
|
|
692
970
|
judgeVerdicts,
|
|
693
971
|
trace,
|
|
972
|
+
story,
|
|
694
973
|
...buildScenarioResultUsage({
|
|
695
974
|
agentUsage: session.usage ?? trace?.usage,
|
|
696
975
|
judgeVerdicts,
|
|
@@ -712,13 +991,12 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
712
991
|
timeoutMs,
|
|
713
992
|
worktree,
|
|
714
993
|
judge,
|
|
715
|
-
live: isLive,
|
|
716
994
|
allowUserInput,
|
|
717
995
|
keepRecordings,
|
|
718
996
|
});
|
|
719
997
|
scenarioResult.debugBundleDir = debugBundleDir;
|
|
720
998
|
emitScenarioVerdict({
|
|
721
|
-
passed
|
|
999
|
+
passed,
|
|
722
1000
|
index: scenarioIndex,
|
|
723
1001
|
total: scenarioTotal,
|
|
724
1002
|
name: scenario.name,
|
|
@@ -726,6 +1004,7 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
726
1004
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, trace?.usage),
|
|
727
1005
|
judgeVerdicts,
|
|
728
1006
|
failures,
|
|
1007
|
+
story,
|
|
729
1008
|
debug,
|
|
730
1009
|
debugBundleDir,
|
|
731
1010
|
});
|
|
@@ -742,7 +1021,6 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
742
1021
|
}
|
|
743
1022
|
}
|
|
744
1023
|
if (worktreeHandle) {
|
|
745
|
-
logPhase(theme.phase("cleanup"), { last: true });
|
|
746
1024
|
await worktreeHandle.cleanup();
|
|
747
1025
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
748
1026
|
activeWorktreeCleanup = undefined;
|
|
@@ -753,12 +1031,215 @@ async function runScenario(cwd, suiteName, scenario, defaultHost, defaultProfile
|
|
|
753
1031
|
}
|
|
754
1032
|
}
|
|
755
1033
|
}
|
|
756
|
-
async function
|
|
757
|
-
const
|
|
1034
|
+
async function runCompareAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, defaultWorkspace, defaultAllowUserSkills, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir = "agent-suites", keepRecordings, rubricsDir, runOptions) {
|
|
1035
|
+
const started = performance.now();
|
|
1036
|
+
const debug = isDebugEnabled({ debug: debugFlag });
|
|
1037
|
+
const suppressEmit = runOptions?.suppressEmit === true;
|
|
1038
|
+
const host = scenario.host ?? defaultHost;
|
|
1039
|
+
if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
1040
|
+
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
1044
|
+
}
|
|
1045
|
+
const aScenario = applyCompareArm(scenario, "a");
|
|
1046
|
+
const bScenario = applyCompareArm(scenario, "b");
|
|
1047
|
+
const aLabel = compareArmLabel(scenario.compare?.a, "a");
|
|
1048
|
+
const bLabel = compareArmLabel(scenario.compare?.b, "b");
|
|
1049
|
+
const shared = {
|
|
1050
|
+
cwd,
|
|
1051
|
+
suiteName,
|
|
1052
|
+
defaultHost,
|
|
1053
|
+
defaultProfile,
|
|
1054
|
+
defaultSkills,
|
|
1055
|
+
defaultContextSources,
|
|
1056
|
+
defaultMcpServers,
|
|
1057
|
+
defaultWorkspace,
|
|
1058
|
+
defaultAllowUserSkills,
|
|
1059
|
+
worktree,
|
|
1060
|
+
stagingSessionId,
|
|
1061
|
+
timeoutMs,
|
|
1062
|
+
allowUserInput,
|
|
1063
|
+
debugFlag,
|
|
1064
|
+
debugDir,
|
|
1065
|
+
suitesDir,
|
|
1066
|
+
keepRecordings,
|
|
1067
|
+
rubricsDir,
|
|
1068
|
+
};
|
|
1069
|
+
const aResult = await runAgentTestOnce(shared.cwd, shared.suiteName, aScenario, shared.defaultHost, 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: "a", writeSidecar: false });
|
|
1070
|
+
const bResult = await runAgentTestOnce(shared.cwd, shared.suiteName, bScenario, shared.defaultHost, 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: "b", writeSidecar: false });
|
|
1071
|
+
const failures = [
|
|
1072
|
+
...prefixCompareFailures(aLabel, aResult.failures),
|
|
1073
|
+
...prefixCompareFailures(bLabel, bResult.failures),
|
|
1074
|
+
];
|
|
1075
|
+
const compareResult = {
|
|
1076
|
+
a: {
|
|
1077
|
+
id: "a",
|
|
1078
|
+
label: aLabel,
|
|
1079
|
+
prompt: aScenario.prompt,
|
|
1080
|
+
trace: aResult.trace,
|
|
1081
|
+
durationMs: aResult.durationMs,
|
|
1082
|
+
},
|
|
1083
|
+
b: {
|
|
1084
|
+
id: "b",
|
|
1085
|
+
label: bLabel,
|
|
1086
|
+
prompt: bScenario.prompt,
|
|
1087
|
+
trace: bResult.trace,
|
|
1088
|
+
durationMs: bResult.durationMs,
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
if (scenario.compare) {
|
|
1092
|
+
failures.push(...assertCompareMetrics(scenario.compare, compareResult));
|
|
1093
|
+
}
|
|
1094
|
+
const deferJudgeToParent = isChildProcess();
|
|
1095
|
+
let judgeVerdicts;
|
|
1096
|
+
if (judge &&
|
|
1097
|
+
!deferJudgeToParent &&
|
|
1098
|
+
failures.length === 0 &&
|
|
1099
|
+
compareResult.a.trace &&
|
|
1100
|
+
compareResult.b.trace) {
|
|
1101
|
+
const criteria = collectCompareJudgeCriteria(scenario.rubric);
|
|
1102
|
+
if (criteria.length > 0) {
|
|
1103
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
1104
|
+
}
|
|
1105
|
+
const judged = await runCompareJudgeRubric(compareResult, scenario.rubric, cwd, host);
|
|
1106
|
+
failures.push(...judged.failures);
|
|
1107
|
+
judgeVerdicts = toJudgeVerdictResults({ ...compareResult.a.trace, judgeVerdicts: judged.verdicts }, criteria, judged.verdicts);
|
|
1108
|
+
}
|
|
1109
|
+
const durationMs = Math.round(performance.now() - started);
|
|
1110
|
+
if (isChildProcess() && stagingSessionId) {
|
|
1111
|
+
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
1112
|
+
passed: failures.length === 0,
|
|
1113
|
+
failures,
|
|
1114
|
+
durationMs,
|
|
1115
|
+
compare: {
|
|
1116
|
+
a: { durationMs: aResult.durationMs },
|
|
1117
|
+
b: { durationMs: bResult.durationMs },
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
const passed = failures.length === 0;
|
|
1122
|
+
const story = buildScenarioStory({
|
|
1123
|
+
rubric: scenario.rubric,
|
|
1124
|
+
trace: compareResult.a.trace,
|
|
1125
|
+
passed,
|
|
1126
|
+
failures,
|
|
1127
|
+
judgeVerdicts,
|
|
1128
|
+
compare: {
|
|
1129
|
+
...compareStoryFields(scenario),
|
|
1130
|
+
aTrace: compareResult.a.trace,
|
|
1131
|
+
bTrace: compareResult.b.trace,
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
const scenarioResult = {
|
|
1135
|
+
suite: suiteName,
|
|
1136
|
+
scenario: scenario.name,
|
|
1137
|
+
prompt: scenario.prompt,
|
|
1138
|
+
passed,
|
|
1139
|
+
failures,
|
|
1140
|
+
durationMs,
|
|
1141
|
+
judgeVerdicts,
|
|
1142
|
+
trace: compareResult.a.trace,
|
|
1143
|
+
compare: compareResult,
|
|
1144
|
+
story,
|
|
1145
|
+
...buildScenarioResultUsage({
|
|
1146
|
+
agentUsage: sumUsageParts([aResult.agentUsage, bResult.agentUsage]),
|
|
1147
|
+
judgeVerdicts,
|
|
1148
|
+
}),
|
|
1149
|
+
};
|
|
1150
|
+
if (!suppressEmit) {
|
|
1151
|
+
const debugBundleDir = await maybeWriteDebugBundle({
|
|
1152
|
+
debug,
|
|
1153
|
+
cwd,
|
|
1154
|
+
suitesDir,
|
|
1155
|
+
rubricsDir,
|
|
1156
|
+
stagingSessionId,
|
|
1157
|
+
debugDir,
|
|
1158
|
+
suiteName,
|
|
1159
|
+
scenario,
|
|
1160
|
+
host,
|
|
1161
|
+
result: scenarioResult,
|
|
1162
|
+
trace: compareResult.a.trace,
|
|
1163
|
+
timeoutMs,
|
|
1164
|
+
worktree,
|
|
1165
|
+
judge,
|
|
1166
|
+
allowUserInput,
|
|
1167
|
+
keepRecordings,
|
|
1168
|
+
});
|
|
1169
|
+
scenarioResult.debugBundleDir = debugBundleDir;
|
|
1170
|
+
emitScenarioVerdict({
|
|
1171
|
+
passed,
|
|
1172
|
+
index: scenarioIndex,
|
|
1173
|
+
total: scenarioTotal,
|
|
1174
|
+
name: scenario.name,
|
|
1175
|
+
durationMs,
|
|
1176
|
+
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, compareResult.a.trace?.usage),
|
|
1177
|
+
judgeVerdicts,
|
|
1178
|
+
failures,
|
|
1179
|
+
story,
|
|
1180
|
+
debug,
|
|
1181
|
+
debugBundleDir,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return scenarioResult;
|
|
1185
|
+
}
|
|
1186
|
+
async function loadCompareResultFromStaging(stagingSessionId, suiteName, scenario) {
|
|
1187
|
+
const aLabel = compareArmLabel(scenario.compare?.a, "a");
|
|
1188
|
+
const bLabel = compareArmLabel(scenario.compare?.b, "b");
|
|
1189
|
+
const aScenario = applyCompareArm(scenario, "a");
|
|
1190
|
+
const bScenario = applyCompareArm(scenario, "b");
|
|
1191
|
+
const loadArm = async (side) => {
|
|
1192
|
+
try {
|
|
1193
|
+
return await loadStagingTrace(getStagingTracePath(stagingSessionId, suiteName, scenario.name, side));
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return undefined;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
return {
|
|
1200
|
+
a: { id: "a", label: aLabel, prompt: aScenario.prompt, trace: await loadArm("a") },
|
|
1201
|
+
b: { id: "b", label: bLabel, prompt: bScenario.prompt, trace: await loadArm("b") },
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
async function runCompareJudgeRubric(compare, rubric, runCwd, host) {
|
|
1205
|
+
const criteria = collectCompareJudgeCriteria(rubric);
|
|
1206
|
+
if (criteria.length === 0 || !compare.a.trace || !compare.b.trace) {
|
|
1207
|
+
return { failures: [], verdicts: [] };
|
|
1208
|
+
}
|
|
1209
|
+
const result = await judgeCompareTraces({
|
|
1210
|
+
aLabel: compare.a.label,
|
|
1211
|
+
a: compare.a.trace,
|
|
1212
|
+
bLabel: compare.b.label,
|
|
1213
|
+
b: compare.b.trace,
|
|
1214
|
+
}, criteria, { cwd: runCwd, host });
|
|
1215
|
+
if (result.skipped) {
|
|
1216
|
+
return {
|
|
1217
|
+
failures: [assertionFailure("judge", result.error ?? "judge skipped", "judge_infra")],
|
|
1218
|
+
verdicts: [],
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const failures = [];
|
|
1222
|
+
for (const verdict of result.verdicts) {
|
|
1223
|
+
if (!verdict.pass) {
|
|
1224
|
+
const category = verdict.infraError
|
|
1225
|
+
? "judge_infra"
|
|
1226
|
+
: verdict.parseError
|
|
1227
|
+
? "judge_parse"
|
|
1228
|
+
: "rubric_miss";
|
|
1229
|
+
failures.push(assertionFailure(`judge:${verdict.id}`, verdict.rationale, category));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (result.error && !result.verdicts.some((verdict) => !verdict.pass)) {
|
|
1233
|
+
failures.push(assertionFailure("judge", result.error, "judge_infra"));
|
|
1234
|
+
}
|
|
1235
|
+
return { failures, verdicts: result.verdicts };
|
|
1236
|
+
}
|
|
1237
|
+
async function runJudgeRubric(trace, rubric, runCwd, host) {
|
|
1238
|
+
const criteria = collectJudgeCriteria(rubric);
|
|
758
1239
|
if (criteria.length === 0) {
|
|
759
1240
|
return { trace, failures: [], verdicts: [] };
|
|
760
1241
|
}
|
|
761
|
-
const result = await judgeTrace(trace, criteria, { cwd: runCwd });
|
|
1242
|
+
const result = await judgeTrace(trace, criteria, { cwd: runCwd, host });
|
|
762
1243
|
if (result.skipped) {
|
|
763
1244
|
return {
|
|
764
1245
|
trace,
|
|
@@ -796,28 +1277,38 @@ export async function runAllSuites(options) {
|
|
|
796
1277
|
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
797
1278
|
})
|
|
798
1279
|
: suitePaths;
|
|
1280
|
+
const cliHosts = uniqueHosts(options.hosts ?? (options.host ? [options.host] : undefined));
|
|
799
1281
|
const reports = [];
|
|
800
1282
|
for (const suitePath of filtered) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1283
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
1284
|
+
const hosts = resolveSuiteHosts({
|
|
1285
|
+
cliHosts,
|
|
1286
|
+
suiteHosts: suite.hosts,
|
|
1287
|
+
defaultHost: suite.defaults?.host,
|
|
1288
|
+
});
|
|
1289
|
+
const hostLocked = hosts.length > 1;
|
|
1290
|
+
for (const host of hosts) {
|
|
1291
|
+
reports.push(await runSuite({
|
|
1292
|
+
cwd: options.cwd,
|
|
1293
|
+
suitePath,
|
|
1294
|
+
host,
|
|
1295
|
+
hostLocked,
|
|
1296
|
+
scenarioFilter: options.scenarioFilter,
|
|
1297
|
+
judge: options.judge,
|
|
1298
|
+
worktree: options.worktree,
|
|
1299
|
+
stagingSessionId: options.stagingSessionId,
|
|
1300
|
+
keepRecordings: options.keepRecordings,
|
|
1301
|
+
suitesDir: options.suitesDir,
|
|
1302
|
+
suiteFilter: options.filter,
|
|
1303
|
+
timeoutMs: options.timeoutMs,
|
|
1304
|
+
allowUserInput: options.allowUserInput,
|
|
1305
|
+
debug: options.debug,
|
|
1306
|
+
debugDir: options.debugDir,
|
|
1307
|
+
scenarioRetries: options.scenarioRetries,
|
|
1308
|
+
rubricsDir: options.rubricsDir,
|
|
1309
|
+
adapterModules: options.adapterModules,
|
|
1310
|
+
}));
|
|
1311
|
+
}
|
|
821
1312
|
}
|
|
822
1313
|
return reports;
|
|
823
1314
|
}
|