@post-print/agent-test 0.3.4 → 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 +122 -43
- 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 +11 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +327 -318
- 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 -1
- package/dist/debug-bundle.d.ts.map +1 -1
- package/dist/debug-bundle.js +12 -5
- 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 +133 -11
- 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 +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -6
- package/dist/index.js.map +1 -1
- package/dist/live-isolation.d.ts +11 -4
- package/dist/live-isolation.d.ts.map +1 -1
- package/dist/live-isolation.js +31 -10
- 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 +6 -2
- 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/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 +9 -7
- package/dist/record-trace.d.ts.map +1 -1
- package/dist/record-trace.js +5 -16
- 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 +34 -1
- package/dist/run-suite.d.ts.map +1 -1
- package/dist/run-suite.js +551 -82
- 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 +97 -11
- 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 +258 -23
- package/dist/validate-suite.js.map +1 -1
- package/package.json +4 -4
- 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,17 +1,21 @@
|
|
|
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 {
|
|
13
|
+
import { bindMcpServersToCaller, mcpStdioScriptPaths } from "./mcp-config.js";
|
|
14
|
+
import { formatDuration, logLive, logPhase, logProgress, logVerdict, refreshHeartbeat, withHeartbeat, } from "./progress.js";
|
|
12
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";
|
|
@@ -38,6 +42,14 @@ async function restoreActiveCallerHead() {
|
|
|
38
42
|
function isChildProcess() {
|
|
39
43
|
return process.env.AGENT_TEST_CHILD === "1";
|
|
40
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
|
+
}
|
|
41
53
|
/** Parent process prints suite headers and final verdicts; children only print phases. */
|
|
42
54
|
export function shouldPrintSuiteChrome() {
|
|
43
55
|
return !isChildProcess();
|
|
@@ -52,6 +64,7 @@ export function registerLiveRunHandlers() {
|
|
|
52
64
|
killActiveLiveChildren();
|
|
53
65
|
cancelActiveCursorRun();
|
|
54
66
|
cancelActiveClaudeRun();
|
|
67
|
+
cancelActiveOpenaiRun();
|
|
55
68
|
const cleanup = activeWorktreeCleanup;
|
|
56
69
|
const headRestore = activeCallerHeadRestore;
|
|
57
70
|
if (!cleanup && !headRestore) {
|
|
@@ -99,6 +112,82 @@ function normalizeJudgeCriteria(judge) {
|
|
|
99
112
|
return { id: item.id ?? `judge-${index}`, question: item.question };
|
|
100
113
|
});
|
|
101
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
|
+
}
|
|
102
191
|
function questionForCriterion(criteria, id) {
|
|
103
192
|
return criteria.find((c) => c.id === id)?.question ?? id;
|
|
104
193
|
}
|
|
@@ -145,6 +234,7 @@ function emitScenarioVerdict(options) {
|
|
|
145
234
|
judgeVerdicts: options.judgeVerdicts,
|
|
146
235
|
rubricFailures: rubricFailuresOnly(options.failures),
|
|
147
236
|
failureCategory: options.failures[0]?.category,
|
|
237
|
+
story: options.story,
|
|
148
238
|
debug: options.debug,
|
|
149
239
|
debugBundleDir: options.debugBundleDir,
|
|
150
240
|
}));
|
|
@@ -156,7 +246,7 @@ async function maybeWriteDebugBundle(options) {
|
|
|
156
246
|
if (!options.stagingSessionId) {
|
|
157
247
|
return undefined;
|
|
158
248
|
}
|
|
159
|
-
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);
|
|
160
250
|
const cliPath = process.argv[1] ?? resolve(options.cwd, "node_modules/@post-print/agent-test/dist/cli.js");
|
|
161
251
|
try {
|
|
162
252
|
await writeDebugBundle({
|
|
@@ -235,12 +325,20 @@ async function runSuiteBody(options) {
|
|
|
235
325
|
}
|
|
236
326
|
const defaultHost = options.host ?? suite.defaults?.host ?? "cursor";
|
|
237
327
|
const results = [];
|
|
238
|
-
const
|
|
328
|
+
const selected = options.scenarioFilter
|
|
239
329
|
? suite.scenarios.filter((scenario) => scenario.name === options.scenarioFilter)
|
|
240
330
|
: suite.scenarios;
|
|
241
|
-
if (options.scenarioFilter &&
|
|
331
|
+
if (options.scenarioFilter && selected.length === 0) {
|
|
242
332
|
throw new Error(`Scenario not found: ${options.scenarioFilter}`);
|
|
243
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
|
+
}
|
|
244
342
|
const filteredTotal = scenarios.length;
|
|
245
343
|
const parentCounters = parentScenarioCounters();
|
|
246
344
|
const displayTotal = parentCounters?.total ?? filteredTotal;
|
|
@@ -249,7 +347,7 @@ async function runSuiteBody(options) {
|
|
|
249
347
|
if (shouldPrintSuiteChrome()) {
|
|
250
348
|
logProgress(`\n${theme.suiteHeader(suite.name, defaultHost, displayTotal)}`);
|
|
251
349
|
if (isolateLive) {
|
|
252
|
-
logProgress(`
|
|
350
|
+
logProgress(` ${theme.isolationNote()}`);
|
|
253
351
|
}
|
|
254
352
|
}
|
|
255
353
|
for (let index = 0; index < scenarios.length; index++) {
|
|
@@ -266,10 +364,17 @@ async function runSuiteBody(options) {
|
|
|
266
364
|
results.push({
|
|
267
365
|
suite: suite.name,
|
|
268
366
|
scenario: scenario.name,
|
|
367
|
+
prompt: scenario.prompt,
|
|
269
368
|
passed: true,
|
|
270
369
|
failures: [],
|
|
271
370
|
skipped: true,
|
|
272
371
|
durationMs: 0,
|
|
372
|
+
story: buildScenarioStory({
|
|
373
|
+
rubric: scenario.rubric,
|
|
374
|
+
passed: true,
|
|
375
|
+
skipped: true,
|
|
376
|
+
failures: [],
|
|
377
|
+
}),
|
|
273
378
|
});
|
|
274
379
|
continue;
|
|
275
380
|
}
|
|
@@ -282,9 +387,10 @@ async function runSuiteBody(options) {
|
|
|
282
387
|
let failures = [];
|
|
283
388
|
let scenarioTrace;
|
|
284
389
|
let previousAttemptExitCode = previousIsolatedExitCode;
|
|
390
|
+
let childSidecar;
|
|
285
391
|
while (true) {
|
|
286
392
|
attempts++;
|
|
287
|
-
const
|
|
393
|
+
const spawned = await spawnLiveScenario({
|
|
288
394
|
cwd: options.cwd,
|
|
289
395
|
suiteName: suite.name,
|
|
290
396
|
scenarioName: scenario.name,
|
|
@@ -296,6 +402,7 @@ async function runSuiteBody(options) {
|
|
|
296
402
|
worktree: options.worktree,
|
|
297
403
|
judge: options.judge,
|
|
298
404
|
host: defaultHost,
|
|
405
|
+
adapterModules: options.adapterModules,
|
|
299
406
|
scenarioIndex: index + 1,
|
|
300
407
|
scenarioTotal: filteredTotal,
|
|
301
408
|
timeoutMs: resolveLiveTimeoutMs(options.timeoutMs),
|
|
@@ -305,23 +412,34 @@ async function runSuiteBody(options) {
|
|
|
305
412
|
debugDir: options.debugDir,
|
|
306
413
|
previousExitCode: previousAttemptExitCode,
|
|
307
414
|
});
|
|
308
|
-
previousAttemptExitCode = exitCode;
|
|
309
|
-
previousIsolatedExitCode = exitCode;
|
|
415
|
+
previousAttemptExitCode = spawned.exitCode;
|
|
416
|
+
previousIsolatedExitCode = spawned.exitCode;
|
|
310
417
|
failures = [];
|
|
311
418
|
scenarioTrace = undefined;
|
|
312
|
-
|
|
313
|
-
|
|
419
|
+
childSidecar =
|
|
420
|
+
options.stagingSessionId !== undefined
|
|
314
421
|
? await loadStagingResult(getStagingResultPath(options.stagingSessionId, suite.name, scenario.name))
|
|
315
422
|
: undefined;
|
|
316
|
-
|
|
423
|
+
if (spawned.exitCode !== 0) {
|
|
424
|
+
failures.push(...failuresForLiveSubprocessExit(spawned.exitCode, childSidecar, spawned.stderr));
|
|
317
425
|
}
|
|
318
426
|
if (options.stagingSessionId) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
}
|
|
322
434
|
}
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
443
|
}
|
|
326
444
|
}
|
|
327
445
|
const canRetry = failures.length > 0 &&
|
|
@@ -334,38 +452,79 @@ async function runSuiteBody(options) {
|
|
|
334
452
|
break;
|
|
335
453
|
}
|
|
336
454
|
let judgeVerdicts;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
}
|
|
347
475
|
}
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
491
|
}
|
|
351
492
|
}
|
|
352
493
|
}
|
|
353
494
|
const durationMs = Math.round(performance.now() - started);
|
|
354
495
|
const passed = failures.length === 0;
|
|
355
496
|
const usageFields = buildScenarioResultUsage({
|
|
356
|
-
agentUsage:
|
|
497
|
+
agentUsage: compareResult
|
|
498
|
+
? sumUsageParts([compareResult.a.trace?.usage, compareResult.b.trace?.usage])
|
|
499
|
+
: scenarioTrace?.usage,
|
|
357
500
|
judgeVerdicts,
|
|
358
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
|
+
});
|
|
359
516
|
const scenarioResult = {
|
|
360
517
|
suite: suite.name,
|
|
361
518
|
scenario: scenario.name,
|
|
362
|
-
|
|
519
|
+
prompt: scenario.prompt,
|
|
363
520
|
passed,
|
|
364
521
|
failures,
|
|
365
522
|
durationMs,
|
|
366
523
|
attempts,
|
|
367
524
|
judgeVerdicts,
|
|
368
525
|
trace: scenarioTrace,
|
|
526
|
+
compare: compareResult,
|
|
527
|
+
story,
|
|
369
528
|
...usageFields,
|
|
370
529
|
};
|
|
371
530
|
const debugBundleDir = await maybeWriteDebugBundle({
|
|
@@ -396,6 +555,7 @@ async function runSuiteBody(options) {
|
|
|
396
555
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, scenarioTrace?.usage),
|
|
397
556
|
judgeVerdicts,
|
|
398
557
|
failures,
|
|
558
|
+
story,
|
|
399
559
|
debug,
|
|
400
560
|
debugBundleDir,
|
|
401
561
|
});
|
|
@@ -435,10 +595,58 @@ async function runSuiteBody(options) {
|
|
|
435
595
|
summary: summarizeReportResults(results),
|
|
436
596
|
};
|
|
437
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
|
+
}
|
|
438
637
|
function mergeContextSources(defaults, scenarioSources) {
|
|
439
638
|
const merged = [...(defaults ?? []), ...(scenarioSources ?? [])].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
440
639
|
return merged.length > 0 ? merged : undefined;
|
|
441
640
|
}
|
|
641
|
+
function defaultProfileForHost(host) {
|
|
642
|
+
if (host === "cursor") {
|
|
643
|
+
return "cursor";
|
|
644
|
+
}
|
|
645
|
+
if (host === "claude") {
|
|
646
|
+
return "claude";
|
|
647
|
+
}
|
|
648
|
+
return "shared";
|
|
649
|
+
}
|
|
442
650
|
/** Run one real agent scenario. JSON suites delegate to this same execution boundary. */
|
|
443
651
|
export async function runAgentTest(options) {
|
|
444
652
|
const previousStagingRoot = getLiveStagingRootOverride();
|
|
@@ -462,7 +670,7 @@ async function runAgentTestBody(options) {
|
|
|
462
670
|
if (legacyScenario.host === "replay" ||
|
|
463
671
|
legacyDefaults?.host === "replay" ||
|
|
464
672
|
"replayTrace" in legacyScenario) {
|
|
465
|
-
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor or
|
|
673
|
+
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor, Claude, or OpenAI.");
|
|
466
674
|
}
|
|
467
675
|
const suiteName = options.suiteName ?? "direct";
|
|
468
676
|
const defaultHost = options.host ?? options.defaults?.host ?? "cursor";
|
|
@@ -471,7 +679,7 @@ async function runAgentTestBody(options) {
|
|
|
471
679
|
let result;
|
|
472
680
|
while (true) {
|
|
473
681
|
attempts++;
|
|
474
|
-
result = await runAgentTestOnce(options.cwd, suiteName, options.scenario, defaultHost, options.defaults?.profile, options.defaults?.skills, options.defaults?.contextSources, options.defaults?.mcpServers, 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 });
|
|
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 });
|
|
475
683
|
const canRetry = !result.skipped &&
|
|
476
684
|
!result.passed &&
|
|
477
685
|
attempts < maxAttempts &&
|
|
@@ -512,13 +720,14 @@ async function runAgentTestBody(options) {
|
|
|
512
720
|
totalTokens: totalTokensFromScenarioUsage(result.usage, result.trace?.usage),
|
|
513
721
|
judgeVerdicts: result.judgeVerdicts,
|
|
514
722
|
failures: result.failures,
|
|
723
|
+
story: result.story,
|
|
515
724
|
debug,
|
|
516
725
|
debugBundleDir,
|
|
517
726
|
});
|
|
518
727
|
}
|
|
519
728
|
return result;
|
|
520
729
|
}
|
|
521
|
-
async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultProfile, defaultSkills, defaultContextSources, defaultMcpServers, judge, worktree, stagingSessionId, scenarioIndex, scenarioTotal, timeoutMs, allowUserInput, debugFlag, debugDir, suitesDir = "agent-suites", keepRecordings, rubricsDir, runOptions) {
|
|
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) {
|
|
522
731
|
const started = performance.now();
|
|
523
732
|
const debug = isDebugEnabled({ debug: debugFlag });
|
|
524
733
|
const suppressEmit = runOptions?.suppressEmit === true;
|
|
@@ -530,20 +739,38 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
530
739
|
return {
|
|
531
740
|
suite: suiteName,
|
|
532
741
|
scenario: scenario.name,
|
|
742
|
+
prompt: scenario.prompt,
|
|
533
743
|
passed: true,
|
|
534
744
|
failures: [],
|
|
535
745
|
skipped: true,
|
|
536
746
|
durationMs: 0,
|
|
747
|
+
story: buildScenarioStory({
|
|
748
|
+
rubric: scenario.rubric,
|
|
749
|
+
passed: true,
|
|
750
|
+
skipped: true,
|
|
751
|
+
failures: [],
|
|
752
|
+
}),
|
|
537
753
|
};
|
|
538
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
|
+
}
|
|
539
758
|
const host = scenario.host ?? defaultHost;
|
|
540
|
-
const profile = scenario.profile ?? defaultProfile ?? (host
|
|
759
|
+
const profile = scenario.profile ?? defaultProfile ?? defaultProfileForHost(host);
|
|
541
760
|
const skills = scenario.skills ?? defaultSkills;
|
|
542
761
|
const contextSources = mergeContextSources(defaultContextSources, scenario.contextSources);
|
|
543
|
-
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);
|
|
544
768
|
const liveTimeoutMs = resolveLiveTimeoutMs(timeoutMs);
|
|
545
769
|
const failOnUserInput = !allowUserInput;
|
|
546
|
-
if (
|
|
770
|
+
if (runOptions?.compareArm) {
|
|
771
|
+
logPhase(theme.phase("arm", `${runOptions.compareArm.toUpperCase()} ${host}`));
|
|
772
|
+
}
|
|
773
|
+
else if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
547
774
|
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
548
775
|
}
|
|
549
776
|
else {
|
|
@@ -558,9 +785,14 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
558
785
|
callerHeadBefore = await captureCallerHead(cwd);
|
|
559
786
|
setCallerHeadRestore(cwd, callerHeadBefore);
|
|
560
787
|
}
|
|
561
|
-
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
|
+
});
|
|
562
795
|
activeWorktreeCleanup = worktreeHandle.cleanup;
|
|
563
|
-
logPhase(theme.phase("worktree", theme.path(worktreeHandle.path)));
|
|
564
796
|
if (scenario.seedPatch) {
|
|
565
797
|
logPhase(theme.phase("seed", theme.basename(scenario.seedPatch)));
|
|
566
798
|
await seedScenarioWorktree(cwd, worktreeHandle.path, scenario.seedPatch, {
|
|
@@ -571,24 +803,21 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
571
803
|
else {
|
|
572
804
|
logPhase(theme.phase("worktree", theme.phaseDim("disabled (AGENT_TEST_ALLOW_IN_PLACE=1)")));
|
|
573
805
|
}
|
|
574
|
-
const runCwd = worktreeHandle?.path ?? cwd;
|
|
806
|
+
const runCwd = worktreeHandle?.path ?? (workspaceRel ? resolve(cwd, workspaceRel) : cwd);
|
|
575
807
|
try {
|
|
576
|
-
logPhase(theme.phase("context"));
|
|
577
|
-
// Worktree runs code in an isolated checkout; load rules/AGENTS from caller cwd
|
|
578
|
-
// so uncommitted .cursor/rules and AGENTS.md edits apply during dogfood.
|
|
579
|
-
const contextRoot = useWorktree ? cwd : runCwd;
|
|
580
808
|
const context = await loadContext({
|
|
581
|
-
cwd:
|
|
809
|
+
cwd: runCwd,
|
|
582
810
|
profile,
|
|
583
811
|
skills,
|
|
584
812
|
contextSources,
|
|
585
813
|
});
|
|
586
|
-
logPhase(theme.phase("agent"));
|
|
587
814
|
const outputContract = outputContractForRubric(scenario.rubric);
|
|
588
815
|
const agentStartMarkerPath = isChildProcess() && stagingSessionId
|
|
589
816
|
? getStagingAgentStartPath(stagingSessionId, suiteName, scenario.name)
|
|
590
817
|
: undefined;
|
|
818
|
+
logPhase(theme.phase("agent", theme.phaseDim("started")));
|
|
591
819
|
const agentStarted = performance.now();
|
|
820
|
+
let livePreview;
|
|
592
821
|
const session = await withHeartbeat(runAgent({
|
|
593
822
|
host,
|
|
594
823
|
cwd: runCwd,
|
|
@@ -597,12 +826,25 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
597
826
|
prompt: scenario.prompt,
|
|
598
827
|
outputContract,
|
|
599
828
|
mcpServers,
|
|
829
|
+
allowUserSkills,
|
|
600
830
|
timeoutMs: liveTimeoutMs,
|
|
601
831
|
failOnUserInput,
|
|
832
|
+
maxConversationTurns: resolveMaxConversationTurns(),
|
|
602
833
|
onDeadlineStart: agentStartMarkerPath
|
|
603
834
|
? () => writeAgentStartMarker(agentStartMarkerPath)
|
|
604
835
|
: undefined,
|
|
605
|
-
|
|
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 });
|
|
606
848
|
logPhase(theme.phase("agent", `${theme.statusCompleted(session.status)} ${theme.duration(formatDuration(performance.now() - agentStarted))}`));
|
|
607
849
|
let trace = enrichTrace(session.trace);
|
|
608
850
|
const failures = [];
|
|
@@ -629,10 +871,15 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
629
871
|
else if (failOnUserInput && traceHasUserInputTool(trace.toolCalls)) {
|
|
630
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(", ")}`));
|
|
631
873
|
}
|
|
632
|
-
logPhase(theme.phase("rubric"));
|
|
633
874
|
failures.push(...assertRubric(trace, scenario.rubric, {
|
|
634
875
|
skillsMode: context.skillsMode,
|
|
635
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
|
+
}
|
|
636
883
|
if (useWorktree && callerTreeBefore !== undefined) {
|
|
637
884
|
const callerTreeAfter = await captureWorkingTreeStatus(cwd);
|
|
638
885
|
const ignoreRoots = resolveHarnessArtifactIgnoreRoots(cwd, getLiveStagingRootOverride());
|
|
@@ -663,11 +910,13 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
663
910
|
].join("\n")));
|
|
664
911
|
}
|
|
665
912
|
}
|
|
666
|
-
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId);
|
|
913
|
+
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId, runOptions?.compareArm);
|
|
667
914
|
if (stagingTracePath) {
|
|
668
915
|
try {
|
|
669
916
|
const path = await recordTrace(stagingTracePath, trace);
|
|
670
|
-
|
|
917
|
+
if (keepRecordings || debug) {
|
|
918
|
+
logPhase(theme.phase("trace", theme.path(path)));
|
|
919
|
+
}
|
|
671
920
|
}
|
|
672
921
|
catch (error) {
|
|
673
922
|
failures.push(assertionFailure("recordTrace", error instanceof Error ? error.message : "failed to record trace", "recording_error"));
|
|
@@ -676,17 +925,17 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
676
925
|
const deferJudgeToParent = isChildProcess();
|
|
677
926
|
let judgeVerdicts;
|
|
678
927
|
if (judge && !deferJudgeToParent) {
|
|
679
|
-
const criteria =
|
|
928
|
+
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
680
929
|
if (criteria.length > 0) {
|
|
681
930
|
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
682
931
|
}
|
|
683
|
-
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd);
|
|
932
|
+
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd, host);
|
|
684
933
|
failures.push(...judged.failures);
|
|
685
934
|
trace = judged.trace;
|
|
686
935
|
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
687
936
|
}
|
|
688
937
|
const durationMs = Math.round(performance.now() - started);
|
|
689
|
-
if (isChildProcess() && stagingSessionId) {
|
|
938
|
+
if (isChildProcess() && stagingSessionId && runOptions?.writeSidecar !== false) {
|
|
690
939
|
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
691
940
|
passed: failures.length === 0,
|
|
692
941
|
failures,
|
|
@@ -694,10 +943,6 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
694
943
|
});
|
|
695
944
|
}
|
|
696
945
|
if (worktreeHandle) {
|
|
697
|
-
const willJudge = Boolean(judge) &&
|
|
698
|
-
!isChildProcess() &&
|
|
699
|
-
normalizeJudgeCriteria(scenario.rubric.judge).length > 0;
|
|
700
|
-
logPhase(theme.phase("cleanup"), { last: !willJudge });
|
|
701
946
|
await worktreeHandle.cleanup();
|
|
702
947
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
703
948
|
activeWorktreeCleanup = undefined;
|
|
@@ -707,15 +952,24 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
707
952
|
}
|
|
708
953
|
worktreeHandle = undefined;
|
|
709
954
|
}
|
|
955
|
+
const passed = failures.length === 0;
|
|
956
|
+
const story = buildScenarioStory({
|
|
957
|
+
rubric: scenario.rubric,
|
|
958
|
+
trace,
|
|
959
|
+
passed,
|
|
960
|
+
failures,
|
|
961
|
+
judgeVerdicts,
|
|
962
|
+
});
|
|
710
963
|
const scenarioResult = {
|
|
711
964
|
suite: suiteName,
|
|
712
965
|
scenario: scenario.name,
|
|
713
|
-
|
|
714
|
-
passed
|
|
966
|
+
prompt: scenario.prompt,
|
|
967
|
+
passed,
|
|
715
968
|
failures,
|
|
716
969
|
durationMs,
|
|
717
970
|
judgeVerdicts,
|
|
718
971
|
trace,
|
|
972
|
+
story,
|
|
719
973
|
...buildScenarioResultUsage({
|
|
720
974
|
agentUsage: session.usage ?? trace?.usage,
|
|
721
975
|
judgeVerdicts,
|
|
@@ -742,7 +996,7 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
742
996
|
});
|
|
743
997
|
scenarioResult.debugBundleDir = debugBundleDir;
|
|
744
998
|
emitScenarioVerdict({
|
|
745
|
-
passed
|
|
999
|
+
passed,
|
|
746
1000
|
index: scenarioIndex,
|
|
747
1001
|
total: scenarioTotal,
|
|
748
1002
|
name: scenario.name,
|
|
@@ -750,6 +1004,7 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
750
1004
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, trace?.usage),
|
|
751
1005
|
judgeVerdicts,
|
|
752
1006
|
failures,
|
|
1007
|
+
story,
|
|
753
1008
|
debug,
|
|
754
1009
|
debugBundleDir,
|
|
755
1010
|
});
|
|
@@ -766,7 +1021,6 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
766
1021
|
}
|
|
767
1022
|
}
|
|
768
1023
|
if (worktreeHandle) {
|
|
769
|
-
logPhase(theme.phase("cleanup"), { last: true });
|
|
770
1024
|
await worktreeHandle.cleanup();
|
|
771
1025
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
772
1026
|
activeWorktreeCleanup = undefined;
|
|
@@ -777,12 +1031,215 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
777
1031
|
}
|
|
778
1032
|
}
|
|
779
1033
|
}
|
|
780
|
-
async function
|
|
781
|
-
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);
|
|
782
1239
|
if (criteria.length === 0) {
|
|
783
1240
|
return { trace, failures: [], verdicts: [] };
|
|
784
1241
|
}
|
|
785
|
-
const result = await judgeTrace(trace, criteria, { cwd: runCwd });
|
|
1242
|
+
const result = await judgeTrace(trace, criteria, { cwd: runCwd, host });
|
|
786
1243
|
if (result.skipped) {
|
|
787
1244
|
return {
|
|
788
1245
|
trace,
|
|
@@ -820,26 +1277,38 @@ export async function runAllSuites(options) {
|
|
|
820
1277
|
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
821
1278
|
})
|
|
822
1279
|
: suitePaths;
|
|
1280
|
+
const cliHosts = uniqueHosts(options.hosts ?? (options.host ? [options.host] : undefined));
|
|
823
1281
|
const reports = [];
|
|
824
1282
|
for (const suitePath of filtered) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
+
}
|
|
843
1312
|
}
|
|
844
1313
|
return reports;
|
|
845
1314
|
}
|