@post-print/agent-test 0.3.4 → 0.3.6
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 +17 -61
- 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 +41 -0
- package/dist/compare-scenario.d.ts.map +1 -0
- package/dist/compare-scenario.js +194 -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 +504 -323
- 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 +588 -82
- package/dist/run-suite.js.map +1 -1
- package/dist/scenario-story.d.ts +43 -0
- package/dist/scenario-story.d.ts.map +1 -0
- package/dist/scenario-story.js +215 -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 +30 -5
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +207 -45
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +105 -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 +268 -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, compareArmDescription, compareArmLabel, compareStoryFields, plainDescription, 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,23 @@ async function restoreActiveCallerHead() {
|
|
|
38
42
|
function isChildProcess() {
|
|
39
43
|
return process.env.AGENT_TEST_CHILD === "1";
|
|
40
44
|
}
|
|
45
|
+
function logScenarioDescription(description) {
|
|
46
|
+
const text = plainDescription(description);
|
|
47
|
+
if (!text) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const line of theme.scenarioDescription(text)) {
|
|
51
|
+
logProgress(line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function resolveMaxConversationTurns() {
|
|
55
|
+
const raw = process.env.AGENT_TEST_MAX_TURNS?.trim();
|
|
56
|
+
if (!raw) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const parsed = Number(raw);
|
|
60
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
61
|
+
}
|
|
41
62
|
/** Parent process prints suite headers and final verdicts; children only print phases. */
|
|
42
63
|
export function shouldPrintSuiteChrome() {
|
|
43
64
|
return !isChildProcess();
|
|
@@ -52,6 +73,7 @@ export function registerLiveRunHandlers() {
|
|
|
52
73
|
killActiveLiveChildren();
|
|
53
74
|
cancelActiveCursorRun();
|
|
54
75
|
cancelActiveClaudeRun();
|
|
76
|
+
cancelActiveOpenaiRun();
|
|
55
77
|
const cleanup = activeWorktreeCleanup;
|
|
56
78
|
const headRestore = activeCallerHeadRestore;
|
|
57
79
|
if (!cleanup && !headRestore) {
|
|
@@ -99,6 +121,82 @@ function normalizeJudgeCriteria(judge) {
|
|
|
99
121
|
return { id: item.id ?? `judge-${index}`, question: item.question };
|
|
100
122
|
});
|
|
101
123
|
}
|
|
124
|
+
/** Explicit judge questions plus skill-invoke questions for mustInvokeSkill. */
|
|
125
|
+
export function collectJudgeCriteria(rubric) {
|
|
126
|
+
return [
|
|
127
|
+
...normalizeJudgeCriteria(rubric.judge),
|
|
128
|
+
...skillInvokeJudgeCriteria(rubric.mustInvokeSkill ?? []),
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
/** Pairwise judge questions only. Skill-follow checks stay per-arm and deterministic. */
|
|
132
|
+
export function collectCompareJudgeCriteria(rubric) {
|
|
133
|
+
return normalizeJudgeCriteria(rubric.judge);
|
|
134
|
+
}
|
|
135
|
+
/** Judge auth and the judge call apply only when a rubric has judge work. */
|
|
136
|
+
export function judgeAuthRequired(judge, rubrics) {
|
|
137
|
+
return judge !== false && rubrics.some((rubric) => collectJudgeCriteria(rubric).length > 0);
|
|
138
|
+
}
|
|
139
|
+
/** Pairwise judge on compare. Skill-follow judge questions stay on non-compare scenarios. */
|
|
140
|
+
export function scenarioNeedsJudge(judge, scenario) {
|
|
141
|
+
if (judge === false) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (scenario.compare) {
|
|
145
|
+
return collectCompareJudgeCriteria(scenario.rubric).length > 0;
|
|
146
|
+
}
|
|
147
|
+
return collectJudgeCriteria(scenario.rubric).length > 0;
|
|
148
|
+
}
|
|
149
|
+
/** True when the selected live run will call the judge. */
|
|
150
|
+
export async function selectedRunNeedsJudge(options) {
|
|
151
|
+
if (options.judge === false) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
155
|
+
const filtered = options.filter
|
|
156
|
+
? suitePaths.filter((suitePath) => {
|
|
157
|
+
const suiteName = suiteNameFromPath(suitePath);
|
|
158
|
+
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
159
|
+
})
|
|
160
|
+
: suitePaths;
|
|
161
|
+
for (const suitePath of filtered) {
|
|
162
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
163
|
+
for (const scenario of suite.scenarios) {
|
|
164
|
+
if (scenario.skip) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (options.scenarioFilter && scenario.name !== options.scenarioFilter) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (scenarioNeedsJudge(true, scenario)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
export async function loadSelectedRubrics(options) {
|
|
178
|
+
const suitePaths = await discoverSuites(resolve(options.cwd, options.suitesDir));
|
|
179
|
+
const filtered = options.filter
|
|
180
|
+
? suitePaths.filter((suitePath) => {
|
|
181
|
+
const suiteName = suiteNameFromPath(suitePath);
|
|
182
|
+
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
183
|
+
})
|
|
184
|
+
: suitePaths;
|
|
185
|
+
const rubrics = [];
|
|
186
|
+
for (const suitePath of filtered) {
|
|
187
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
188
|
+
for (const scenario of suite.scenarios) {
|
|
189
|
+
if (scenario.skip) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (options.scenarioFilter && scenario.name !== options.scenarioFilter) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
rubrics.push(scenario.rubric);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return rubrics;
|
|
199
|
+
}
|
|
102
200
|
function questionForCriterion(criteria, id) {
|
|
103
201
|
return criteria.find((c) => c.id === id)?.question ?? id;
|
|
104
202
|
}
|
|
@@ -145,6 +243,7 @@ function emitScenarioVerdict(options) {
|
|
|
145
243
|
judgeVerdicts: options.judgeVerdicts,
|
|
146
244
|
rubricFailures: rubricFailuresOnly(options.failures),
|
|
147
245
|
failureCategory: options.failures[0]?.category,
|
|
246
|
+
story: options.story,
|
|
148
247
|
debug: options.debug,
|
|
149
248
|
debugBundleDir: options.debugBundleDir,
|
|
150
249
|
}));
|
|
@@ -156,7 +255,7 @@ async function maybeWriteDebugBundle(options) {
|
|
|
156
255
|
if (!options.stagingSessionId) {
|
|
157
256
|
return undefined;
|
|
158
257
|
}
|
|
159
|
-
const dir = getDebugBundleDir(options.stagingSessionId, options.suiteName, options.scenario.name, getLiveStagingSessionRoot);
|
|
258
|
+
const dir = getDebugBundleDir(options.stagingSessionId, options.suiteName, options.scenario.name, getLiveStagingSessionRoot, options.host);
|
|
160
259
|
const cliPath = process.argv[1] ?? resolve(options.cwd, "node_modules/@post-print/agent-test/dist/cli.js");
|
|
161
260
|
try {
|
|
162
261
|
await writeDebugBundle({
|
|
@@ -235,12 +334,20 @@ async function runSuiteBody(options) {
|
|
|
235
334
|
}
|
|
236
335
|
const defaultHost = options.host ?? suite.defaults?.host ?? "cursor";
|
|
237
336
|
const results = [];
|
|
238
|
-
const
|
|
337
|
+
const selected = options.scenarioFilter
|
|
239
338
|
? suite.scenarios.filter((scenario) => scenario.name === options.scenarioFilter)
|
|
240
339
|
: suite.scenarios;
|
|
241
|
-
if (options.scenarioFilter &&
|
|
340
|
+
if (options.scenarioFilter && selected.length === 0) {
|
|
242
341
|
throw new Error(`Scenario not found: ${options.scenarioFilter}`);
|
|
243
342
|
}
|
|
343
|
+
const hostLocked = options.hostLocked === true;
|
|
344
|
+
const scenarios = hostLocked
|
|
345
|
+
? selected.filter((scenario) => scenarioRunsOnHost(scenario, defaultHost, true))
|
|
346
|
+
: selected;
|
|
347
|
+
if (hostLocked && options.scenarioFilter && selected.length > 0 && scenarios.length === 0) {
|
|
348
|
+
const pinned = selected[0]?.host;
|
|
349
|
+
throw new Error(`Scenario ${options.scenarioFilter} is pinned to ${pinned} (this run is ${defaultHost})`);
|
|
350
|
+
}
|
|
244
351
|
const filteredTotal = scenarios.length;
|
|
245
352
|
const parentCounters = parentScenarioCounters();
|
|
246
353
|
const displayTotal = parentCounters?.total ?? filteredTotal;
|
|
@@ -249,7 +356,7 @@ async function runSuiteBody(options) {
|
|
|
249
356
|
if (shouldPrintSuiteChrome()) {
|
|
250
357
|
logProgress(`\n${theme.suiteHeader(suite.name, defaultHost, displayTotal)}`);
|
|
251
358
|
if (isolateLive) {
|
|
252
|
-
logProgress(`
|
|
359
|
+
logProgress(` ${theme.isolationNote()}`);
|
|
253
360
|
}
|
|
254
361
|
}
|
|
255
362
|
for (let index = 0; index < scenarios.length; index++) {
|
|
@@ -263,13 +370,22 @@ async function runSuiteBody(options) {
|
|
|
263
370
|
if (scenario.skip) {
|
|
264
371
|
const skipLabel = `[${scenarioIndex}/${scenarioTotal}] ${scenario.name}`;
|
|
265
372
|
logProgress(theme.skipped(skipLabel));
|
|
373
|
+
logScenarioDescription(scenario.description);
|
|
266
374
|
results.push({
|
|
267
375
|
suite: suite.name,
|
|
268
376
|
scenario: scenario.name,
|
|
377
|
+
description: plainDescription(scenario.description),
|
|
378
|
+
prompt: scenario.prompt,
|
|
269
379
|
passed: true,
|
|
270
380
|
failures: [],
|
|
271
381
|
skipped: true,
|
|
272
382
|
durationMs: 0,
|
|
383
|
+
story: buildScenarioStory({
|
|
384
|
+
rubric: scenario.rubric,
|
|
385
|
+
passed: true,
|
|
386
|
+
skipped: true,
|
|
387
|
+
failures: [],
|
|
388
|
+
}),
|
|
273
389
|
});
|
|
274
390
|
continue;
|
|
275
391
|
}
|
|
@@ -282,9 +398,10 @@ async function runSuiteBody(options) {
|
|
|
282
398
|
let failures = [];
|
|
283
399
|
let scenarioTrace;
|
|
284
400
|
let previousAttemptExitCode = previousIsolatedExitCode;
|
|
401
|
+
let childSidecar;
|
|
285
402
|
while (true) {
|
|
286
403
|
attempts++;
|
|
287
|
-
const
|
|
404
|
+
const spawned = await spawnLiveScenario({
|
|
288
405
|
cwd: options.cwd,
|
|
289
406
|
suiteName: suite.name,
|
|
290
407
|
scenarioName: scenario.name,
|
|
@@ -296,6 +413,7 @@ async function runSuiteBody(options) {
|
|
|
296
413
|
worktree: options.worktree,
|
|
297
414
|
judge: options.judge,
|
|
298
415
|
host: defaultHost,
|
|
416
|
+
adapterModules: options.adapterModules,
|
|
299
417
|
scenarioIndex: index + 1,
|
|
300
418
|
scenarioTotal: filteredTotal,
|
|
301
419
|
timeoutMs: resolveLiveTimeoutMs(options.timeoutMs),
|
|
@@ -305,23 +423,34 @@ async function runSuiteBody(options) {
|
|
|
305
423
|
debugDir: options.debugDir,
|
|
306
424
|
previousExitCode: previousAttemptExitCode,
|
|
307
425
|
});
|
|
308
|
-
previousAttemptExitCode = exitCode;
|
|
309
|
-
previousIsolatedExitCode = exitCode;
|
|
426
|
+
previousAttemptExitCode = spawned.exitCode;
|
|
427
|
+
previousIsolatedExitCode = spawned.exitCode;
|
|
310
428
|
failures = [];
|
|
311
429
|
scenarioTrace = undefined;
|
|
312
|
-
|
|
313
|
-
|
|
430
|
+
childSidecar =
|
|
431
|
+
options.stagingSessionId !== undefined
|
|
314
432
|
? await loadStagingResult(getStagingResultPath(options.stagingSessionId, suite.name, scenario.name))
|
|
315
433
|
: undefined;
|
|
316
|
-
|
|
434
|
+
if (spawned.exitCode !== 0) {
|
|
435
|
+
failures.push(...failuresForLiveSubprocessExit(spawned.exitCode, childSidecar, spawned.stderr));
|
|
317
436
|
}
|
|
318
437
|
if (options.stagingSessionId) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
438
|
+
if (scenario.compare) {
|
|
439
|
+
try {
|
|
440
|
+
scenarioTrace = await loadStagingTrace(getStagingTracePath(options.stagingSessionId, suite.name, scenario.name, "a"));
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Trace may be missing when the child crashed before recording.
|
|
444
|
+
}
|
|
322
445
|
}
|
|
323
|
-
|
|
324
|
-
|
|
446
|
+
else {
|
|
447
|
+
const tracePath = getStagingTracePath(options.stagingSessionId, suite.name, scenario.name);
|
|
448
|
+
try {
|
|
449
|
+
scenarioTrace = await loadStagingTrace(tracePath);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Trace may be missing when the child crashed before recording.
|
|
453
|
+
}
|
|
325
454
|
}
|
|
326
455
|
}
|
|
327
456
|
const canRetry = failures.length > 0 &&
|
|
@@ -334,38 +463,82 @@ async function runSuiteBody(options) {
|
|
|
334
463
|
break;
|
|
335
464
|
}
|
|
336
465
|
let judgeVerdicts;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
466
|
+
let compareResult;
|
|
467
|
+
const judgeHost = options.host ?? scenario.host ?? suite.defaults?.host ?? "cursor";
|
|
468
|
+
if (scenario.compare && options.stagingSessionId) {
|
|
469
|
+
compareResult = applySidecarCompareDurations(await loadCompareResultFromStaging(options.stagingSessionId, suite.name, scenario), childSidecar);
|
|
470
|
+
scenarioTrace = compareResult.a.trace ?? scenarioTrace;
|
|
471
|
+
}
|
|
472
|
+
if (failures.length === 0 && options.judge !== false) {
|
|
473
|
+
if (scenario.compare && compareResult?.a.trace && compareResult.b.trace) {
|
|
474
|
+
const criteria = collectCompareJudgeCriteria(scenario.rubric);
|
|
475
|
+
if (criteria.length > 0) {
|
|
476
|
+
releaseLiveMemory();
|
|
477
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
478
|
+
try {
|
|
479
|
+
const judged = await runCompareJudgeRubric(compareResult, scenario.rubric, options.cwd, judgeHost);
|
|
480
|
+
failures.push(...judged.failures);
|
|
481
|
+
judgeVerdicts = toJudgeVerdictResults({ ...compareResult.a.trace, judgeVerdicts: judged.verdicts }, criteria, judged.verdicts);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to judge compare traces", "judge_infra"));
|
|
485
|
+
}
|
|
347
486
|
}
|
|
348
|
-
|
|
349
|
-
|
|
487
|
+
}
|
|
488
|
+
else if (scenarioTrace && !scenario.compare) {
|
|
489
|
+
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
490
|
+
if (criteria.length > 0) {
|
|
491
|
+
releaseLiveMemory();
|
|
492
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
493
|
+
try {
|
|
494
|
+
const judged = await runJudgeRubric(scenarioTrace, scenario.rubric, options.cwd, judgeHost);
|
|
495
|
+
failures.push(...judged.failures);
|
|
496
|
+
scenarioTrace = judged.trace;
|
|
497
|
+
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
failures.push(assertionFailure("judge", error instanceof Error ? error.message : "failed to load staging trace for judge", "judge_infra"));
|
|
501
|
+
}
|
|
350
502
|
}
|
|
351
503
|
}
|
|
352
504
|
}
|
|
353
505
|
const durationMs = Math.round(performance.now() - started);
|
|
354
506
|
const passed = failures.length === 0;
|
|
355
507
|
const usageFields = buildScenarioResultUsage({
|
|
356
|
-
agentUsage:
|
|
508
|
+
agentUsage: compareResult
|
|
509
|
+
? sumUsageParts([compareResult.a.trace?.usage, compareResult.b.trace?.usage])
|
|
510
|
+
: scenarioTrace?.usage,
|
|
511
|
+
judgeVerdicts,
|
|
512
|
+
});
|
|
513
|
+
const story = buildScenarioStory({
|
|
514
|
+
rubric: scenario.rubric,
|
|
515
|
+
trace: scenarioTrace,
|
|
516
|
+
passed,
|
|
517
|
+
failures,
|
|
357
518
|
judgeVerdicts,
|
|
519
|
+
compare: compareResult
|
|
520
|
+
? {
|
|
521
|
+
...compareStoryFields(scenario),
|
|
522
|
+
aTrace: compareResult.a.trace,
|
|
523
|
+
bTrace: compareResult.b.trace,
|
|
524
|
+
aDurationMs: compareResult.a.durationMs,
|
|
525
|
+
bDurationMs: compareResult.b.durationMs,
|
|
526
|
+
}
|
|
527
|
+
: undefined,
|
|
358
528
|
});
|
|
359
529
|
const scenarioResult = {
|
|
360
530
|
suite: suite.name,
|
|
361
531
|
scenario: scenario.name,
|
|
362
|
-
|
|
532
|
+
description: plainDescription(scenario.description),
|
|
533
|
+
prompt: scenario.prompt,
|
|
363
534
|
passed,
|
|
364
535
|
failures,
|
|
365
536
|
durationMs,
|
|
366
537
|
attempts,
|
|
367
538
|
judgeVerdicts,
|
|
368
539
|
trace: scenarioTrace,
|
|
540
|
+
compare: compareResult,
|
|
541
|
+
story,
|
|
369
542
|
...usageFields,
|
|
370
543
|
};
|
|
371
544
|
const debugBundleDir = await maybeWriteDebugBundle({
|
|
@@ -396,6 +569,7 @@ async function runSuiteBody(options) {
|
|
|
396
569
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, scenarioTrace?.usage),
|
|
397
570
|
judgeVerdicts,
|
|
398
571
|
failures,
|
|
572
|
+
story,
|
|
399
573
|
debug,
|
|
400
574
|
debugBundleDir,
|
|
401
575
|
});
|
|
@@ -435,10 +609,58 @@ async function runSuiteBody(options) {
|
|
|
435
609
|
summary: summarizeReportResults(results),
|
|
436
610
|
};
|
|
437
611
|
}
|
|
612
|
+
/** Suite trees and MCP scripts from the caller, so uncommitted fixtures reach the host. */
|
|
613
|
+
function liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers) {
|
|
614
|
+
const extras = [...(contextSources ?? [])];
|
|
615
|
+
const suitesRel = overlayRelPath(cwd, suitesDir);
|
|
616
|
+
if (suitesRel && !extras.includes(suitesRel)) {
|
|
617
|
+
extras.push(suitesRel);
|
|
618
|
+
}
|
|
619
|
+
for (const script of mcpStdioScriptPaths(mcpServers)) {
|
|
620
|
+
if (!extras.includes(script)) {
|
|
621
|
+
extras.push(script);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return extras;
|
|
625
|
+
}
|
|
626
|
+
function overlayRelPath(cwd, path) {
|
|
627
|
+
const trimmed = path.replace(/^\.\//, "").trim();
|
|
628
|
+
if (trimmed.length === 0) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
if (!trimmed.startsWith("/")) {
|
|
632
|
+
return trimmed;
|
|
633
|
+
}
|
|
634
|
+
const root = resolve(cwd);
|
|
635
|
+
if (trimmed === root) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
if (trimmed.startsWith(`${root}/`)) {
|
|
639
|
+
return trimmed.slice(root.length + 1);
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
function resolveRunWorkspace(defaultWorkspace, scenarioWorkspace) {
|
|
644
|
+
const raw = scenarioWorkspace !== undefined ? scenarioWorkspace : defaultWorkspace;
|
|
645
|
+
const parsed = parseScenarioWorkspace(raw);
|
|
646
|
+
if (!parsed.ok) {
|
|
647
|
+
throw new Error(parsed.message);
|
|
648
|
+
}
|
|
649
|
+
return parsed.rel;
|
|
650
|
+
}
|
|
438
651
|
function mergeContextSources(defaults, scenarioSources) {
|
|
439
652
|
const merged = [...(defaults ?? []), ...(scenarioSources ?? [])].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
440
653
|
return merged.length > 0 ? merged : undefined;
|
|
441
654
|
}
|
|
655
|
+
function defaultProfileForHost(host) {
|
|
656
|
+
if (host === "cursor") {
|
|
657
|
+
return "cursor";
|
|
658
|
+
}
|
|
659
|
+
if (host === "claude") {
|
|
660
|
+
return "claude";
|
|
661
|
+
}
|
|
662
|
+
return "shared";
|
|
663
|
+
}
|
|
442
664
|
/** Run one real agent scenario. JSON suites delegate to this same execution boundary. */
|
|
443
665
|
export async function runAgentTest(options) {
|
|
444
666
|
const previousStagingRoot = getLiveStagingRootOverride();
|
|
@@ -462,7 +684,7 @@ async function runAgentTestBody(options) {
|
|
|
462
684
|
if (legacyScenario.host === "replay" ||
|
|
463
685
|
legacyDefaults?.host === "replay" ||
|
|
464
686
|
"replayTrace" in legacyScenario) {
|
|
465
|
-
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor or
|
|
687
|
+
throw new Error("Replay-based testing is deprecated and no longer supported; use Cursor, Claude, or OpenAI.");
|
|
466
688
|
}
|
|
467
689
|
const suiteName = options.suiteName ?? "direct";
|
|
468
690
|
const defaultHost = options.host ?? options.defaults?.host ?? "cursor";
|
|
@@ -471,7 +693,7 @@ async function runAgentTestBody(options) {
|
|
|
471
693
|
let result;
|
|
472
694
|
while (true) {
|
|
473
695
|
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 });
|
|
696
|
+
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
697
|
const canRetry = !result.skipped &&
|
|
476
698
|
!result.passed &&
|
|
477
699
|
attempts < maxAttempts &&
|
|
@@ -512,13 +734,14 @@ async function runAgentTestBody(options) {
|
|
|
512
734
|
totalTokens: totalTokensFromScenarioUsage(result.usage, result.trace?.usage),
|
|
513
735
|
judgeVerdicts: result.judgeVerdicts,
|
|
514
736
|
failures: result.failures,
|
|
737
|
+
story: result.story,
|
|
515
738
|
debug,
|
|
516
739
|
debugBundleDir,
|
|
517
740
|
});
|
|
518
741
|
}
|
|
519
742
|
return result;
|
|
520
743
|
}
|
|
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) {
|
|
744
|
+
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
745
|
const started = performance.now();
|
|
523
746
|
const debug = isDebugEnabled({ debug: debugFlag });
|
|
524
747
|
const suppressEmit = runOptions?.suppressEmit === true;
|
|
@@ -527,27 +750,49 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
527
750
|
? `[${scenarioIndex}/${scenarioTotal}] ${scenario.name}`
|
|
528
751
|
: scenario.name;
|
|
529
752
|
logProgress(theme.skipped(skipLabel));
|
|
753
|
+
logScenarioDescription(scenario.description);
|
|
530
754
|
return {
|
|
531
755
|
suite: suiteName,
|
|
532
756
|
scenario: scenario.name,
|
|
757
|
+
description: plainDescription(scenario.description),
|
|
758
|
+
prompt: scenario.prompt,
|
|
533
759
|
passed: true,
|
|
534
760
|
failures: [],
|
|
535
761
|
skipped: true,
|
|
536
762
|
durationMs: 0,
|
|
763
|
+
story: buildScenarioStory({
|
|
764
|
+
rubric: scenario.rubric,
|
|
765
|
+
passed: true,
|
|
766
|
+
skipped: true,
|
|
767
|
+
failures: [],
|
|
768
|
+
}),
|
|
537
769
|
};
|
|
538
770
|
}
|
|
771
|
+
if (scenario.compare && runOptions?.compareArm === undefined) {
|
|
772
|
+
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 });
|
|
773
|
+
}
|
|
539
774
|
const host = scenario.host ?? defaultHost;
|
|
540
|
-
const profile = scenario.profile ?? defaultProfile ?? (host
|
|
775
|
+
const profile = scenario.profile ?? defaultProfile ?? defaultProfileForHost(host);
|
|
541
776
|
const skills = scenario.skills ?? defaultSkills;
|
|
542
777
|
const contextSources = mergeContextSources(defaultContextSources, scenario.contextSources);
|
|
543
|
-
const
|
|
778
|
+
const workspaceRel = resolveRunWorkspace(defaultWorkspace, scenario.workspace);
|
|
779
|
+
const fixtureWorkspace = workspaceRel !== undefined;
|
|
780
|
+
const allowUserSkills = resolveAllowUserSkills(scenario.allowUserSkills, defaultAllowUserSkills);
|
|
781
|
+
const mcpServers = fixtureWorkspace
|
|
782
|
+
? bindMcpServersToCaller(mergeMcpServers(defaultMcpServers, scenario.mcpServers), cwd)
|
|
783
|
+
: mergeMcpServers(defaultMcpServers, scenario.mcpServers);
|
|
544
784
|
const liveTimeoutMs = resolveLiveTimeoutMs(timeoutMs);
|
|
545
785
|
const failOnUserInput = !allowUserInput;
|
|
546
|
-
if (
|
|
786
|
+
if (runOptions?.compareArm) {
|
|
787
|
+
logPhase(theme.phase("arm", `${runOptions.compareArm.toUpperCase()} ${host}`));
|
|
788
|
+
}
|
|
789
|
+
else if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
547
790
|
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
791
|
+
logScenarioDescription(scenario.description);
|
|
548
792
|
}
|
|
549
793
|
else {
|
|
550
794
|
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
795
|
+
logScenarioDescription(scenario.description);
|
|
551
796
|
}
|
|
552
797
|
const useWorktree = worktree !== false && !process.env.AGENT_TEST_NO_WORKTREE;
|
|
553
798
|
let worktreeHandle;
|
|
@@ -558,9 +803,14 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
558
803
|
callerHeadBefore = await captureCallerHead(cwd);
|
|
559
804
|
setCallerHeadRestore(cwd, callerHeadBefore);
|
|
560
805
|
}
|
|
561
|
-
worktreeHandle = await
|
|
806
|
+
worktreeHandle = await createSealedWorkspace({
|
|
807
|
+
callerCwd: cwd,
|
|
808
|
+
workspace: workspaceRel,
|
|
809
|
+
overlayPaths: fixtureWorkspace
|
|
810
|
+
? undefined
|
|
811
|
+
: defaultSealedOverlayPaths(liveOverlayExtras(cwd, suitesDir, contextSources, mcpServers), skillPathsFromSetting(skills)),
|
|
812
|
+
});
|
|
562
813
|
activeWorktreeCleanup = worktreeHandle.cleanup;
|
|
563
|
-
logPhase(theme.phase("worktree", theme.path(worktreeHandle.path)));
|
|
564
814
|
if (scenario.seedPatch) {
|
|
565
815
|
logPhase(theme.phase("seed", theme.basename(scenario.seedPatch)));
|
|
566
816
|
await seedScenarioWorktree(cwd, worktreeHandle.path, scenario.seedPatch, {
|
|
@@ -571,24 +821,21 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
571
821
|
else {
|
|
572
822
|
logPhase(theme.phase("worktree", theme.phaseDim("disabled (AGENT_TEST_ALLOW_IN_PLACE=1)")));
|
|
573
823
|
}
|
|
574
|
-
const runCwd = worktreeHandle?.path ?? cwd;
|
|
824
|
+
const runCwd = worktreeHandle?.path ?? (workspaceRel ? resolve(cwd, workspaceRel) : cwd);
|
|
575
825
|
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
826
|
const context = await loadContext({
|
|
581
|
-
cwd:
|
|
827
|
+
cwd: runCwd,
|
|
582
828
|
profile,
|
|
583
829
|
skills,
|
|
584
830
|
contextSources,
|
|
585
831
|
});
|
|
586
|
-
logPhase(theme.phase("agent"));
|
|
587
832
|
const outputContract = outputContractForRubric(scenario.rubric);
|
|
588
833
|
const agentStartMarkerPath = isChildProcess() && stagingSessionId
|
|
589
834
|
? getStagingAgentStartPath(stagingSessionId, suiteName, scenario.name)
|
|
590
835
|
: undefined;
|
|
836
|
+
logPhase(theme.phase("agent", theme.phaseDim("started")));
|
|
591
837
|
const agentStarted = performance.now();
|
|
838
|
+
let livePreview;
|
|
592
839
|
const session = await withHeartbeat(runAgent({
|
|
593
840
|
host,
|
|
594
841
|
cwd: runCwd,
|
|
@@ -597,12 +844,25 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
597
844
|
prompt: scenario.prompt,
|
|
598
845
|
outputContract,
|
|
599
846
|
mcpServers,
|
|
847
|
+
allowUserSkills,
|
|
600
848
|
timeoutMs: liveTimeoutMs,
|
|
601
849
|
failOnUserInput,
|
|
850
|
+
maxConversationTurns: resolveMaxConversationTurns(),
|
|
602
851
|
onDeadlineStart: agentStartMarkerPath
|
|
603
852
|
? () => writeAgentStartMarker(agentStartMarkerPath)
|
|
604
853
|
: undefined,
|
|
605
|
-
|
|
854
|
+
onAgentEvent: (event) => {
|
|
855
|
+
if (event.type === "tool") {
|
|
856
|
+
logLive(theme.liveTool(event.name, pathFromArgs(event.args)));
|
|
857
|
+
livePreview = undefined;
|
|
858
|
+
refreshHeartbeat();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
// Clock tick paints the preview. Do not rewrite here — a long
|
|
862
|
+
// line wraps and `\r` cannot clear the leftover row.
|
|
863
|
+
livePreview = quoteExcerpt(event.text);
|
|
864
|
+
},
|
|
865
|
+
}), { started: agentStarted, preview: () => livePreview });
|
|
606
866
|
logPhase(theme.phase("agent", `${theme.statusCompleted(session.status)} ${theme.duration(formatDuration(performance.now() - agentStarted))}`));
|
|
607
867
|
let trace = enrichTrace(session.trace);
|
|
608
868
|
const failures = [];
|
|
@@ -629,10 +889,15 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
629
889
|
else if (failOnUserInput && traceHasUserInputTool(trace.toolCalls)) {
|
|
630
890
|
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
891
|
}
|
|
632
|
-
logPhase(theme.phase("rubric"));
|
|
633
892
|
failures.push(...assertRubric(trace, scenario.rubric, {
|
|
634
893
|
skillsMode: context.skillsMode,
|
|
635
894
|
}));
|
|
895
|
+
if (worktreeHandle) {
|
|
896
|
+
const escaped = toolPathsOutsideWorkspace(trace, worktreeHandle.path);
|
|
897
|
+
if (escaped.length > 0) {
|
|
898
|
+
failures.push(assertionFailure("workingTreeLeak", `agent used paths outside the sealed workspace: ${escaped.join(", ")}`, "worktree_leak", `escaped=${escaped.join(", ")}`));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
636
901
|
if (useWorktree && callerTreeBefore !== undefined) {
|
|
637
902
|
const callerTreeAfter = await captureWorkingTreeStatus(cwd);
|
|
638
903
|
const ignoreRoots = resolveHarnessArtifactIgnoreRoots(cwd, getLiveStagingRootOverride());
|
|
@@ -663,11 +928,13 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
663
928
|
].join("\n")));
|
|
664
929
|
}
|
|
665
930
|
}
|
|
666
|
-
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId);
|
|
931
|
+
const stagingTracePath = resolveRecordingPath(suiteName, scenario.name, stagingSessionId, runOptions?.compareArm);
|
|
667
932
|
if (stagingTracePath) {
|
|
668
933
|
try {
|
|
669
934
|
const path = await recordTrace(stagingTracePath, trace);
|
|
670
|
-
|
|
935
|
+
if (keepRecordings || debug) {
|
|
936
|
+
logPhase(theme.phase("trace", theme.path(path)));
|
|
937
|
+
}
|
|
671
938
|
}
|
|
672
939
|
catch (error) {
|
|
673
940
|
failures.push(assertionFailure("recordTrace", error instanceof Error ? error.message : "failed to record trace", "recording_error"));
|
|
@@ -676,17 +943,17 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
676
943
|
const deferJudgeToParent = isChildProcess();
|
|
677
944
|
let judgeVerdicts;
|
|
678
945
|
if (judge && !deferJudgeToParent) {
|
|
679
|
-
const criteria =
|
|
946
|
+
const criteria = collectJudgeCriteria(scenario.rubric);
|
|
680
947
|
if (criteria.length > 0) {
|
|
681
948
|
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
682
949
|
}
|
|
683
|
-
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd);
|
|
950
|
+
const judged = await runJudgeRubric(trace, scenario.rubric, runCwd, host);
|
|
684
951
|
failures.push(...judged.failures);
|
|
685
952
|
trace = judged.trace;
|
|
686
953
|
judgeVerdicts = toJudgeVerdictResults(judged.trace, criteria, judged.verdicts);
|
|
687
954
|
}
|
|
688
955
|
const durationMs = Math.round(performance.now() - started);
|
|
689
|
-
if (isChildProcess() && stagingSessionId) {
|
|
956
|
+
if (isChildProcess() && stagingSessionId && runOptions?.writeSidecar !== false) {
|
|
690
957
|
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
691
958
|
passed: failures.length === 0,
|
|
692
959
|
failures,
|
|
@@ -694,10 +961,6 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
694
961
|
});
|
|
695
962
|
}
|
|
696
963
|
if (worktreeHandle) {
|
|
697
|
-
const willJudge = Boolean(judge) &&
|
|
698
|
-
!isChildProcess() &&
|
|
699
|
-
normalizeJudgeCriteria(scenario.rubric.judge).length > 0;
|
|
700
|
-
logPhase(theme.phase("cleanup"), { last: !willJudge });
|
|
701
964
|
await worktreeHandle.cleanup();
|
|
702
965
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
703
966
|
activeWorktreeCleanup = undefined;
|
|
@@ -707,15 +970,25 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
707
970
|
}
|
|
708
971
|
worktreeHandle = undefined;
|
|
709
972
|
}
|
|
973
|
+
const passed = failures.length === 0;
|
|
974
|
+
const story = buildScenarioStory({
|
|
975
|
+
rubric: scenario.rubric,
|
|
976
|
+
trace,
|
|
977
|
+
passed,
|
|
978
|
+
failures,
|
|
979
|
+
judgeVerdicts,
|
|
980
|
+
});
|
|
710
981
|
const scenarioResult = {
|
|
711
982
|
suite: suiteName,
|
|
712
983
|
scenario: scenario.name,
|
|
713
|
-
|
|
714
|
-
|
|
984
|
+
description: plainDescription(scenario.description),
|
|
985
|
+
prompt: scenario.prompt,
|
|
986
|
+
passed,
|
|
715
987
|
failures,
|
|
716
988
|
durationMs,
|
|
717
989
|
judgeVerdicts,
|
|
718
990
|
trace,
|
|
991
|
+
story,
|
|
719
992
|
...buildScenarioResultUsage({
|
|
720
993
|
agentUsage: session.usage ?? trace?.usage,
|
|
721
994
|
judgeVerdicts,
|
|
@@ -742,7 +1015,7 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
742
1015
|
});
|
|
743
1016
|
scenarioResult.debugBundleDir = debugBundleDir;
|
|
744
1017
|
emitScenarioVerdict({
|
|
745
|
-
passed
|
|
1018
|
+
passed,
|
|
746
1019
|
index: scenarioIndex,
|
|
747
1020
|
total: scenarioTotal,
|
|
748
1021
|
name: scenario.name,
|
|
@@ -750,6 +1023,7 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
750
1023
|
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, trace?.usage),
|
|
751
1024
|
judgeVerdicts,
|
|
752
1025
|
failures,
|
|
1026
|
+
story,
|
|
753
1027
|
debug,
|
|
754
1028
|
debugBundleDir,
|
|
755
1029
|
});
|
|
@@ -766,7 +1040,6 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
766
1040
|
}
|
|
767
1041
|
}
|
|
768
1042
|
if (worktreeHandle) {
|
|
769
|
-
logPhase(theme.phase("cleanup"), { last: true });
|
|
770
1043
|
await worktreeHandle.cleanup();
|
|
771
1044
|
if (activeWorktreeCleanup === worktreeHandle.cleanup) {
|
|
772
1045
|
activeWorktreeCleanup = undefined;
|
|
@@ -777,12 +1050,233 @@ async function runAgentTestOnce(cwd, suiteName, scenario, defaultHost, defaultPr
|
|
|
777
1050
|
}
|
|
778
1051
|
}
|
|
779
1052
|
}
|
|
780
|
-
async function
|
|
781
|
-
const
|
|
1053
|
+
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) {
|
|
1054
|
+
const started = performance.now();
|
|
1055
|
+
const debug = isDebugEnabled({ debug: debugFlag });
|
|
1056
|
+
const suppressEmit = runOptions?.suppressEmit === true;
|
|
1057
|
+
const host = scenario.host ?? defaultHost;
|
|
1058
|
+
if (scenarioIndex !== undefined && scenarioTotal !== undefined) {
|
|
1059
|
+
logProgress(theme.scenarioTitle(scenarioIndex, scenarioTotal, scenario.name, host));
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
logProgress(theme.scenarioLabel(scenario.name, host));
|
|
1063
|
+
}
|
|
1064
|
+
logScenarioDescription(scenario.description);
|
|
1065
|
+
const aScenario = applyCompareArm(scenario, "a");
|
|
1066
|
+
const bScenario = applyCompareArm(scenario, "b");
|
|
1067
|
+
const aLabel = compareArmLabel(scenario.compare?.a, "a");
|
|
1068
|
+
const bLabel = compareArmLabel(scenario.compare?.b, "b");
|
|
1069
|
+
const shared = {
|
|
1070
|
+
cwd,
|
|
1071
|
+
suiteName,
|
|
1072
|
+
defaultHost,
|
|
1073
|
+
defaultProfile,
|
|
1074
|
+
defaultSkills,
|
|
1075
|
+
defaultContextSources,
|
|
1076
|
+
defaultMcpServers,
|
|
1077
|
+
defaultWorkspace,
|
|
1078
|
+
defaultAllowUserSkills,
|
|
1079
|
+
worktree,
|
|
1080
|
+
stagingSessionId,
|
|
1081
|
+
timeoutMs,
|
|
1082
|
+
allowUserInput,
|
|
1083
|
+
debugFlag,
|
|
1084
|
+
debugDir,
|
|
1085
|
+
suitesDir,
|
|
1086
|
+
keepRecordings,
|
|
1087
|
+
rubricsDir,
|
|
1088
|
+
};
|
|
1089
|
+
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 });
|
|
1090
|
+
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 });
|
|
1091
|
+
const failures = [
|
|
1092
|
+
...prefixCompareFailures(aLabel, aResult.failures),
|
|
1093
|
+
...prefixCompareFailures(bLabel, bResult.failures),
|
|
1094
|
+
];
|
|
1095
|
+
const compareResult = {
|
|
1096
|
+
a: {
|
|
1097
|
+
id: "a",
|
|
1098
|
+
label: aLabel,
|
|
1099
|
+
description: compareArmDescription(scenario.compare?.a),
|
|
1100
|
+
prompt: aScenario.prompt,
|
|
1101
|
+
trace: aResult.trace,
|
|
1102
|
+
durationMs: aResult.durationMs,
|
|
1103
|
+
},
|
|
1104
|
+
b: {
|
|
1105
|
+
id: "b",
|
|
1106
|
+
label: bLabel,
|
|
1107
|
+
description: compareArmDescription(scenario.compare?.b),
|
|
1108
|
+
prompt: bScenario.prompt,
|
|
1109
|
+
trace: bResult.trace,
|
|
1110
|
+
durationMs: bResult.durationMs,
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
if (scenario.compare) {
|
|
1114
|
+
failures.push(...assertCompareMetrics(scenario.compare, compareResult));
|
|
1115
|
+
}
|
|
1116
|
+
const deferJudgeToParent = isChildProcess();
|
|
1117
|
+
let judgeVerdicts;
|
|
1118
|
+
if (judge &&
|
|
1119
|
+
!deferJudgeToParent &&
|
|
1120
|
+
failures.length === 0 &&
|
|
1121
|
+
compareResult.a.trace &&
|
|
1122
|
+
compareResult.b.trace) {
|
|
1123
|
+
const criteria = collectCompareJudgeCriteria(scenario.rubric);
|
|
1124
|
+
if (criteria.length > 0) {
|
|
1125
|
+
logPhase(theme.judgePhase(criteria.length), { last: true });
|
|
1126
|
+
}
|
|
1127
|
+
const judged = await runCompareJudgeRubric(compareResult, scenario.rubric, cwd, host);
|
|
1128
|
+
failures.push(...judged.failures);
|
|
1129
|
+
judgeVerdicts = toJudgeVerdictResults({ ...compareResult.a.trace, judgeVerdicts: judged.verdicts }, criteria, judged.verdicts);
|
|
1130
|
+
}
|
|
1131
|
+
const durationMs = Math.round(performance.now() - started);
|
|
1132
|
+
if (isChildProcess() && stagingSessionId) {
|
|
1133
|
+
await writeStagingResult(getStagingResultPath(stagingSessionId, suiteName, scenario.name), {
|
|
1134
|
+
passed: failures.length === 0,
|
|
1135
|
+
failures,
|
|
1136
|
+
durationMs,
|
|
1137
|
+
compare: {
|
|
1138
|
+
a: { durationMs: aResult.durationMs },
|
|
1139
|
+
b: { durationMs: bResult.durationMs },
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const passed = failures.length === 0;
|
|
1144
|
+
const story = buildScenarioStory({
|
|
1145
|
+
rubric: scenario.rubric,
|
|
1146
|
+
trace: compareResult.a.trace,
|
|
1147
|
+
passed,
|
|
1148
|
+
failures,
|
|
1149
|
+
judgeVerdicts,
|
|
1150
|
+
compare: {
|
|
1151
|
+
...compareStoryFields(scenario),
|
|
1152
|
+
aTrace: compareResult.a.trace,
|
|
1153
|
+
bTrace: compareResult.b.trace,
|
|
1154
|
+
aDurationMs: compareResult.a.durationMs,
|
|
1155
|
+
bDurationMs: compareResult.b.durationMs,
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
const scenarioResult = {
|
|
1159
|
+
suite: suiteName,
|
|
1160
|
+
scenario: scenario.name,
|
|
1161
|
+
description: plainDescription(scenario.description),
|
|
1162
|
+
prompt: scenario.prompt,
|
|
1163
|
+
passed,
|
|
1164
|
+
failures,
|
|
1165
|
+
durationMs,
|
|
1166
|
+
judgeVerdicts,
|
|
1167
|
+
trace: compareResult.a.trace,
|
|
1168
|
+
compare: compareResult,
|
|
1169
|
+
story,
|
|
1170
|
+
...buildScenarioResultUsage({
|
|
1171
|
+
agentUsage: sumUsageParts([aResult.agentUsage, bResult.agentUsage]),
|
|
1172
|
+
judgeVerdicts,
|
|
1173
|
+
}),
|
|
1174
|
+
};
|
|
1175
|
+
if (!suppressEmit) {
|
|
1176
|
+
const debugBundleDir = await maybeWriteDebugBundle({
|
|
1177
|
+
debug,
|
|
1178
|
+
cwd,
|
|
1179
|
+
suitesDir,
|
|
1180
|
+
rubricsDir,
|
|
1181
|
+
stagingSessionId,
|
|
1182
|
+
debugDir,
|
|
1183
|
+
suiteName,
|
|
1184
|
+
scenario,
|
|
1185
|
+
host,
|
|
1186
|
+
result: scenarioResult,
|
|
1187
|
+
trace: compareResult.a.trace,
|
|
1188
|
+
timeoutMs,
|
|
1189
|
+
worktree,
|
|
1190
|
+
judge,
|
|
1191
|
+
allowUserInput,
|
|
1192
|
+
keepRecordings,
|
|
1193
|
+
});
|
|
1194
|
+
scenarioResult.debugBundleDir = debugBundleDir;
|
|
1195
|
+
emitScenarioVerdict({
|
|
1196
|
+
passed,
|
|
1197
|
+
index: scenarioIndex,
|
|
1198
|
+
total: scenarioTotal,
|
|
1199
|
+
name: scenario.name,
|
|
1200
|
+
durationMs,
|
|
1201
|
+
totalTokens: totalTokensFromScenarioUsage(scenarioResult.usage, compareResult.a.trace?.usage),
|
|
1202
|
+
judgeVerdicts,
|
|
1203
|
+
failures,
|
|
1204
|
+
story,
|
|
1205
|
+
debug,
|
|
1206
|
+
debugBundleDir,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return scenarioResult;
|
|
1210
|
+
}
|
|
1211
|
+
async function loadCompareResultFromStaging(stagingSessionId, suiteName, scenario) {
|
|
1212
|
+
const aLabel = compareArmLabel(scenario.compare?.a, "a");
|
|
1213
|
+
const bLabel = compareArmLabel(scenario.compare?.b, "b");
|
|
1214
|
+
const aScenario = applyCompareArm(scenario, "a");
|
|
1215
|
+
const bScenario = applyCompareArm(scenario, "b");
|
|
1216
|
+
const loadArm = async (side) => {
|
|
1217
|
+
try {
|
|
1218
|
+
return await loadStagingTrace(getStagingTracePath(stagingSessionId, suiteName, scenario.name, side));
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
return undefined;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
return {
|
|
1225
|
+
a: {
|
|
1226
|
+
id: "a",
|
|
1227
|
+
label: aLabel,
|
|
1228
|
+
description: compareArmDescription(scenario.compare?.a),
|
|
1229
|
+
prompt: aScenario.prompt,
|
|
1230
|
+
trace: await loadArm("a"),
|
|
1231
|
+
},
|
|
1232
|
+
b: {
|
|
1233
|
+
id: "b",
|
|
1234
|
+
label: bLabel,
|
|
1235
|
+
description: compareArmDescription(scenario.compare?.b),
|
|
1236
|
+
prompt: bScenario.prompt,
|
|
1237
|
+
trace: await loadArm("b"),
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
async function runCompareJudgeRubric(compare, rubric, runCwd, host) {
|
|
1242
|
+
const criteria = collectCompareJudgeCriteria(rubric);
|
|
1243
|
+
if (criteria.length === 0 || !compare.a.trace || !compare.b.trace) {
|
|
1244
|
+
return { failures: [], verdicts: [] };
|
|
1245
|
+
}
|
|
1246
|
+
const result = await judgeCompareTraces({
|
|
1247
|
+
aLabel: compare.a.label,
|
|
1248
|
+
a: compare.a.trace,
|
|
1249
|
+
bLabel: compare.b.label,
|
|
1250
|
+
b: compare.b.trace,
|
|
1251
|
+
}, criteria, { cwd: runCwd, host });
|
|
1252
|
+
if (result.skipped) {
|
|
1253
|
+
return {
|
|
1254
|
+
failures: [assertionFailure("judge", result.error ?? "judge skipped", "judge_infra")],
|
|
1255
|
+
verdicts: [],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const failures = [];
|
|
1259
|
+
for (const verdict of result.verdicts) {
|
|
1260
|
+
if (!verdict.pass) {
|
|
1261
|
+
const category = verdict.infraError
|
|
1262
|
+
? "judge_infra"
|
|
1263
|
+
: verdict.parseError
|
|
1264
|
+
? "judge_parse"
|
|
1265
|
+
: "rubric_miss";
|
|
1266
|
+
failures.push(assertionFailure(`judge:${verdict.id}`, verdict.rationale, category));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (result.error && !result.verdicts.some((verdict) => !verdict.pass)) {
|
|
1270
|
+
failures.push(assertionFailure("judge", result.error, "judge_infra"));
|
|
1271
|
+
}
|
|
1272
|
+
return { failures, verdicts: result.verdicts };
|
|
1273
|
+
}
|
|
1274
|
+
async function runJudgeRubric(trace, rubric, runCwd, host) {
|
|
1275
|
+
const criteria = collectJudgeCriteria(rubric);
|
|
782
1276
|
if (criteria.length === 0) {
|
|
783
1277
|
return { trace, failures: [], verdicts: [] };
|
|
784
1278
|
}
|
|
785
|
-
const result = await judgeTrace(trace, criteria, { cwd: runCwd });
|
|
1279
|
+
const result = await judgeTrace(trace, criteria, { cwd: runCwd, host });
|
|
786
1280
|
if (result.skipped) {
|
|
787
1281
|
return {
|
|
788
1282
|
trace,
|
|
@@ -820,26 +1314,38 @@ export async function runAllSuites(options) {
|
|
|
820
1314
|
return suiteName === options.filter || suitePath.includes(`/${options.filter}/`);
|
|
821
1315
|
})
|
|
822
1316
|
: suitePaths;
|
|
1317
|
+
const cliHosts = uniqueHosts(options.hosts ?? (options.host ? [options.host] : undefined));
|
|
823
1318
|
const reports = [];
|
|
824
1319
|
for (const suitePath of filtered) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1320
|
+
const suite = await loadSuiteFile(suitePath, { rubricsDir: options.rubricsDir });
|
|
1321
|
+
const hosts = resolveSuiteHosts({
|
|
1322
|
+
cliHosts,
|
|
1323
|
+
suiteHosts: suite.hosts,
|
|
1324
|
+
defaultHost: suite.defaults?.host,
|
|
1325
|
+
});
|
|
1326
|
+
const hostLocked = hosts.length > 1;
|
|
1327
|
+
for (const host of hosts) {
|
|
1328
|
+
reports.push(await runSuite({
|
|
1329
|
+
cwd: options.cwd,
|
|
1330
|
+
suitePath,
|
|
1331
|
+
host,
|
|
1332
|
+
hostLocked,
|
|
1333
|
+
scenarioFilter: options.scenarioFilter,
|
|
1334
|
+
judge: options.judge,
|
|
1335
|
+
worktree: options.worktree,
|
|
1336
|
+
stagingSessionId: options.stagingSessionId,
|
|
1337
|
+
keepRecordings: options.keepRecordings,
|
|
1338
|
+
suitesDir: options.suitesDir,
|
|
1339
|
+
suiteFilter: options.filter,
|
|
1340
|
+
timeoutMs: options.timeoutMs,
|
|
1341
|
+
allowUserInput: options.allowUserInput,
|
|
1342
|
+
debug: options.debug,
|
|
1343
|
+
debugDir: options.debugDir,
|
|
1344
|
+
scenarioRetries: options.scenarioRetries,
|
|
1345
|
+
rubricsDir: options.rubricsDir,
|
|
1346
|
+
adapterModules: options.adapterModules,
|
|
1347
|
+
}));
|
|
1348
|
+
}
|
|
843
1349
|
}
|
|
844
1350
|
return reports;
|
|
845
1351
|
}
|