@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/html-report.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { describeCompareOutcome } from "./compare-scenario.js";
|
|
5
|
+
import { summarizeReports } from "./suite-summary.js";
|
|
6
6
|
function escapeHtml(value) {
|
|
7
7
|
return value
|
|
8
8
|
.replaceAll("&", "&")
|
|
@@ -11,11 +11,19 @@ function escapeHtml(value) {
|
|
|
11
11
|
.replaceAll('"', """)
|
|
12
12
|
.replaceAll("'", "'");
|
|
13
13
|
}
|
|
14
|
+
const INTEGER_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
|
|
15
|
+
const DECIMAL_FORMAT = new Intl.NumberFormat("en-US", {
|
|
16
|
+
minimumFractionDigits: 1,
|
|
17
|
+
maximumFractionDigits: 1,
|
|
18
|
+
});
|
|
19
|
+
function formatInteger(value) {
|
|
20
|
+
return INTEGER_FORMAT.format(Math.round(value));
|
|
21
|
+
}
|
|
14
22
|
function formatDuration(ms) {
|
|
15
23
|
if (ms < 1000) {
|
|
16
|
-
return `${
|
|
24
|
+
return `${formatInteger(ms)}ms`;
|
|
17
25
|
}
|
|
18
|
-
return `${(ms / 1000)
|
|
26
|
+
return `${DECIMAL_FORMAT.format(ms / 1000)}s`;
|
|
19
27
|
}
|
|
20
28
|
function statusLabel(result) {
|
|
21
29
|
if (result.skipped) {
|
|
@@ -29,62 +37,95 @@ function statusClass(result) {
|
|
|
29
37
|
}
|
|
30
38
|
return result.passed ? "status-passed" : "status-failed";
|
|
31
39
|
}
|
|
40
|
+
const MISSING_REQUIREMENT = {
|
|
41
|
+
label: "Missing requirement",
|
|
42
|
+
hint: "A required string did not appear in assistant text, commands, or tool results.",
|
|
43
|
+
};
|
|
44
|
+
const FORBIDDEN_BEHAVIOR = {
|
|
45
|
+
label: "Forbidden behavior",
|
|
46
|
+
hint: "The agent did something it was told not to do.",
|
|
47
|
+
};
|
|
48
|
+
const MISSING_COMMAND = {
|
|
49
|
+
label: "Missing command",
|
|
50
|
+
hint: "An expected command never ran.",
|
|
51
|
+
};
|
|
52
|
+
const MISSING_TOOL = {
|
|
53
|
+
label: "Missing tool call",
|
|
54
|
+
hint: "An expected tool was never invoked.",
|
|
55
|
+
};
|
|
56
|
+
const FORBIDDEN_TOOL = {
|
|
57
|
+
label: "Forbidden tool call",
|
|
58
|
+
hint: "A tool the scenario forbids was invoked.",
|
|
59
|
+
};
|
|
60
|
+
const MISSING_SKILL = {
|
|
61
|
+
label: "Missing skill",
|
|
62
|
+
hint: "The agent did not read a skill it was expected to use.",
|
|
63
|
+
};
|
|
64
|
+
const UNEXPECTED_SKILL = {
|
|
65
|
+
label: "Unexpected skill",
|
|
66
|
+
hint: "The agent read a skill it should have avoided.",
|
|
67
|
+
};
|
|
68
|
+
const MISSING_READ_PATH = {
|
|
69
|
+
label: "Missing read path",
|
|
70
|
+
hint: "Expected a Read tool call whose args mention this path fragment (registry-first / grounding).",
|
|
71
|
+
};
|
|
72
|
+
const FORBIDDEN_READ_PATH = {
|
|
73
|
+
label: "Forbidden read path",
|
|
74
|
+
hint: "A successful Read returned content from a path the scenario forbids (miss attempts do not fail).",
|
|
75
|
+
};
|
|
76
|
+
const ROUTING = {
|
|
77
|
+
label: "Routing",
|
|
78
|
+
hint: "The routing announcement did not match expectations.",
|
|
79
|
+
};
|
|
32
80
|
/** Human-readable label + one-line explanation for the matcher codes assertRubric/judge emit. */
|
|
33
81
|
const MATCHER_LABELS = {
|
|
34
82
|
runAgent: { label: "Agent run", hint: "The agent session itself failed or was cut short." },
|
|
35
|
-
|
|
36
|
-
label: "
|
|
37
|
-
hint: "
|
|
38
|
-
},
|
|
39
|
-
mustNot: { label: "Forbidden behavior", hint: "The agent did something it was told not to do." },
|
|
40
|
-
mustRun: { label: "Missing command", hint: "An expected command never ran." },
|
|
41
|
-
mustCallTool: {
|
|
42
|
-
label: "Missing tool call",
|
|
43
|
-
hint: "An expected tool was never invoked.",
|
|
44
|
-
},
|
|
45
|
-
mustNotCallTool: {
|
|
46
|
-
label: "Forbidden tool call",
|
|
47
|
-
hint: "A tool the scenario forbids was invoked.",
|
|
48
|
-
},
|
|
49
|
-
toHaveCalledTool: {
|
|
50
|
-
label: "Missing tool call",
|
|
51
|
-
hint: "An expected tool was never invoked.",
|
|
52
|
-
},
|
|
53
|
-
toHaveNotCalledTool: {
|
|
54
|
-
label: "Forbidden tool call",
|
|
55
|
-
hint: "A tool the scenario forbids was invoked.",
|
|
56
|
-
},
|
|
57
|
-
mustInvokeSkill: {
|
|
58
|
-
label: "Missing skill",
|
|
59
|
-
hint: "The agent didn't read a skill it was expected to use.",
|
|
60
|
-
},
|
|
61
|
-
mustNotInvokeSkill: {
|
|
62
|
-
label: "Unexpected skill",
|
|
63
|
-
hint: "The agent read a skill it should have avoided.",
|
|
64
|
-
},
|
|
65
|
-
mustReadPath: {
|
|
66
|
-
label: "Missing read path",
|
|
67
|
-
hint: "Expected a Read tool call whose args mention this path fragment (registry-first / grounding).",
|
|
83
|
+
liveScenario: {
|
|
84
|
+
label: "Isolated scenario",
|
|
85
|
+
hint: "The scenario subprocess exited before it wrote a result.",
|
|
68
86
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
must: MISSING_REQUIREMENT,
|
|
88
|
+
mustInclude: MISSING_REQUIREMENT,
|
|
89
|
+
mustNot: FORBIDDEN_BEHAVIOR,
|
|
90
|
+
mustNotInclude: FORBIDDEN_BEHAVIOR,
|
|
91
|
+
mustRun: MISSING_COMMAND,
|
|
92
|
+
toHaveRunCommand: MISSING_COMMAND,
|
|
93
|
+
mustCallTool: MISSING_TOOL,
|
|
94
|
+
toHaveCalledTool: MISSING_TOOL,
|
|
95
|
+
mustNotCallTool: FORBIDDEN_TOOL,
|
|
96
|
+
toHaveNotCalledTool: FORBIDDEN_TOOL,
|
|
97
|
+
mustInvokeSkill: MISSING_SKILL,
|
|
98
|
+
toHaveInvokedSkill: MISSING_SKILL,
|
|
99
|
+
mustNotInvokeSkill: UNEXPECTED_SKILL,
|
|
100
|
+
toHaveNotInvokedSkill: UNEXPECTED_SKILL,
|
|
101
|
+
mustReadPath: MISSING_READ_PATH,
|
|
102
|
+
toHaveReadPath: MISSING_READ_PATH,
|
|
103
|
+
mustNotReadPath: FORBIDDEN_READ_PATH,
|
|
104
|
+
toHaveNotReadPath: FORBIDDEN_READ_PATH,
|
|
105
|
+
routingBlock: ROUTING,
|
|
106
|
+
toIncludeRoutingBlock: ROUTING,
|
|
107
|
+
toHaveRoutingBlockBeforeTools: ROUTING,
|
|
108
|
+
toHaveTier: ROUTING,
|
|
109
|
+
toHaveHandsOnTier: ROUTING,
|
|
110
|
+
toHaveHandsOnTierBeforeTools: ROUTING,
|
|
111
|
+
toHaveReviewDepth: {
|
|
112
|
+
label: "Review depth",
|
|
113
|
+
hint: "The review depth announcement did not match the rubric.",
|
|
72
114
|
},
|
|
73
|
-
toHaveReadPath: {
|
|
74
|
-
label: "Missing read path",
|
|
75
|
-
hint: "Expected a Read tool call whose args mention this path fragment (registry-first / grounding).",
|
|
76
|
-
},
|
|
77
|
-
toHaveNotReadPath: {
|
|
78
|
-
label: "Forbidden read path",
|
|
79
|
-
hint: "A successful Read returned content from a path the scenario forbids (miss attempts do not fail).",
|
|
80
|
-
},
|
|
81
|
-
routingBlock: { label: "Routing", hint: "The routing announcement didn't match expectations." },
|
|
82
115
|
workingTreeLeak: {
|
|
83
116
|
label: "Working tree leak",
|
|
84
|
-
hint: "The agent
|
|
117
|
+
hint: "The agent used a path outside the sealed workspace, or edited the caller checkout.",
|
|
85
118
|
},
|
|
86
119
|
recordTrace: { label: "Recording failed", hint: "Saving the trace to disk failed." },
|
|
87
120
|
judge: { label: "Judge", hint: "The LLM judge flagged this scenario." },
|
|
121
|
+
faster: {
|
|
122
|
+
label: "Faster arm",
|
|
123
|
+
hint: "The named arm must finish in less time.",
|
|
124
|
+
},
|
|
125
|
+
cheaper: {
|
|
126
|
+
label: "Cheaper arm",
|
|
127
|
+
hint: "The named arm must use fewer tokens.",
|
|
128
|
+
},
|
|
88
129
|
};
|
|
89
130
|
function humanizeMatcher(matcher) {
|
|
90
131
|
const [base] = matcher.split(":");
|
|
@@ -153,23 +194,45 @@ function buildOrderedTimeline(trace) {
|
|
|
153
194
|
}
|
|
154
195
|
return items.sort((a, b) => a.seq - b.seq);
|
|
155
196
|
}
|
|
156
|
-
function
|
|
197
|
+
function promptAlreadyInTrace(trace, prompt) {
|
|
198
|
+
return (trace?.messages.some((message) => message.role === "user" && message.content.trim() === prompt) ?? false);
|
|
199
|
+
}
|
|
200
|
+
function renderPromptBubble(prompt, trace) {
|
|
201
|
+
const text = prompt?.trim();
|
|
202
|
+
if (!text || promptAlreadyInTrace(trace, text)) {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
return renderMessageBubble({ role: "user", content: text });
|
|
206
|
+
}
|
|
207
|
+
function renderChat(trace, prompt) {
|
|
208
|
+
const promptBubble = renderPromptBubble(prompt, trace);
|
|
157
209
|
if (!trace) {
|
|
210
|
+
if (promptBubble) {
|
|
211
|
+
return `<div class="chat">${promptBubble}</div>`;
|
|
212
|
+
}
|
|
158
213
|
return `<p class="empty">No transcript recorded for this scenario.</p>`;
|
|
159
214
|
}
|
|
160
215
|
const parts = [];
|
|
161
216
|
const timeline = buildOrderedTimeline(trace);
|
|
162
217
|
if (timeline) {
|
|
163
218
|
parts.push(`<div class="chat">`);
|
|
219
|
+
if (promptBubble) {
|
|
220
|
+
parts.push(promptBubble);
|
|
221
|
+
}
|
|
164
222
|
for (const item of timeline) {
|
|
165
223
|
parts.push(item.kind === "message" ? renderMessageBubble(item.message) : renderToolCallCard(item.call));
|
|
166
224
|
}
|
|
167
225
|
parts.push(`</div>`);
|
|
168
226
|
}
|
|
169
|
-
else if (trace.messages.length > 0 || trace.toolCalls.length > 0) {
|
|
170
|
-
|
|
171
|
-
|
|
227
|
+
else if (trace.messages.length > 0 || trace.toolCalls.length > 0 || promptBubble) {
|
|
228
|
+
if (!timeline && (trace.messages.length > 0 || trace.toolCalls.length > 0)) {
|
|
229
|
+
parts.push(`<p class="empty note">Emission order wasn't recorded for this trace — messages and tool calls are shown in separate groups below.</p>`);
|
|
230
|
+
}
|
|
231
|
+
if (promptBubble || trace.messages.length > 0) {
|
|
172
232
|
parts.push(`<div class="chat">`);
|
|
233
|
+
if (promptBubble) {
|
|
234
|
+
parts.push(promptBubble);
|
|
235
|
+
}
|
|
173
236
|
for (const message of trace.messages) {
|
|
174
237
|
parts.push(renderMessageBubble(message));
|
|
175
238
|
}
|
|
@@ -243,14 +306,14 @@ function formatTokensBadge(result) {
|
|
|
243
306
|
return undefined;
|
|
244
307
|
}
|
|
245
308
|
if (typeof usage.totalTokens === "number") {
|
|
246
|
-
return `${usage.totalTokens}
|
|
309
|
+
return `${formatInteger(usage.totalTokens)} tokens`;
|
|
247
310
|
}
|
|
248
311
|
const parts = [];
|
|
249
312
|
if (typeof usage.inputTokens === "number") {
|
|
250
|
-
parts.push(`in ${usage.inputTokens}`);
|
|
313
|
+
parts.push(`in ${formatInteger(usage.inputTokens)}`);
|
|
251
314
|
}
|
|
252
315
|
if (typeof usage.outputTokens === "number") {
|
|
253
|
-
parts.push(`out ${usage.outputTokens}`);
|
|
316
|
+
parts.push(`out ${formatInteger(usage.outputTokens)}`);
|
|
254
317
|
}
|
|
255
318
|
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
256
319
|
}
|
|
@@ -271,7 +334,7 @@ function renderUsageDetail(usage) {
|
|
|
271
334
|
return "";
|
|
272
335
|
}
|
|
273
336
|
const items = present
|
|
274
|
-
.map(([label, value]) => `<div class="meta-item"><span class="meta-key">${escapeHtml(label)}</span><span class="meta-val">${value}</span></div>`)
|
|
337
|
+
.map(([label, value]) => `<div class="meta-item"><span class="meta-key">${escapeHtml(label)}</span><span class="meta-val">${formatInteger(value ?? 0)}</span></div>`)
|
|
275
338
|
.join("");
|
|
276
339
|
return `<section class="scenario-meta"><h3>Token usage</h3><div class="meta-grid">${items}</div></section>`;
|
|
277
340
|
}
|
|
@@ -282,62 +345,66 @@ function renderTraceMeta(result) {
|
|
|
282
345
|
}
|
|
283
346
|
const skills = trace.skillsInvoked?.length ? trace.skillsInvoked.join(", ") : "(none)";
|
|
284
347
|
const items = [
|
|
285
|
-
["Messages",
|
|
286
|
-
["Tool calls",
|
|
348
|
+
["Messages", formatInteger(trace.messages.length)],
|
|
349
|
+
["Tool calls", formatInteger(trace.toolCalls.length)],
|
|
287
350
|
["Skills invoked", skills],
|
|
288
351
|
["Routing tier", trace.routing?.tier ?? "(none)"],
|
|
289
352
|
];
|
|
290
353
|
if (result.attempts !== undefined && result.attempts > 1) {
|
|
291
|
-
items.push(["Attempts",
|
|
354
|
+
items.push(["Attempts", formatInteger(result.attempts)]);
|
|
292
355
|
}
|
|
293
356
|
const html = items
|
|
294
357
|
.map(([label, value]) => `<div class="meta-item"><span class="meta-key">${escapeHtml(label)}</span><span class="meta-val">${escapeHtml(value)}</span></div>`)
|
|
295
358
|
.join("");
|
|
296
359
|
return `<section class="scenario-meta"><h3>Trace stats</h3><div class="meta-grid">${html}</div></section>`;
|
|
297
360
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
361
|
+
function costItem(label, detail, value) {
|
|
362
|
+
return `<div class="cost-item"><span class="cost-value">${escapeHtml(value)}</span><span class="cost-label">${escapeHtml(label)}</span><span class="cost-detail">${escapeHtml(detail)}</span></div>`;
|
|
363
|
+
}
|
|
364
|
+
function renderCostSection(usage) {
|
|
365
|
+
if (!usage || usage.sumTotalTokens === undefined) {
|
|
300
366
|
return "";
|
|
301
367
|
}
|
|
302
|
-
const
|
|
303
|
-
["Scenarios with usage", String(usage.scenariosWithUsage)],
|
|
304
|
-
];
|
|
305
|
-
if (usage.sumTotalTokens !== undefined) {
|
|
306
|
-
rows.push(["Sum totalTokens", String(usage.sumTotalTokens)]);
|
|
307
|
-
}
|
|
308
|
-
if (usage.p50TotalTokens !== undefined) {
|
|
309
|
-
rows.push(["p50 totalTokens", String(usage.p50TotalTokens)]);
|
|
310
|
-
}
|
|
311
|
-
if (usage.p95TotalTokens !== undefined) {
|
|
312
|
-
rows.push(["p95 totalTokens", String(usage.p95TotalTokens)]);
|
|
313
|
-
}
|
|
368
|
+
const items = [costItem("Total", "Cost of this run", formatInteger(usage.sumTotalTokens))];
|
|
314
369
|
if (usage.sumInputTokens !== undefined) {
|
|
315
|
-
|
|
370
|
+
items.push(costItem("In", "Prompt and context the host sent", formatInteger(usage.sumInputTokens)));
|
|
316
371
|
}
|
|
317
372
|
if (usage.sumOutputTokens !== undefined) {
|
|
318
|
-
|
|
373
|
+
items.push(costItem("Out", "Text the agent wrote", formatInteger(usage.sumOutputTokens)));
|
|
319
374
|
}
|
|
320
|
-
|
|
321
|
-
.
|
|
322
|
-
|
|
323
|
-
|
|
375
|
+
if (usage.scenariosWithUsage > 1 && usage.p50TotalTokens !== undefined) {
|
|
376
|
+
items.push(costItem("Typical", "Middle scenario. Half cost less than this.", formatInteger(usage.p50TotalTokens)));
|
|
377
|
+
}
|
|
378
|
+
if (usage.scenariosWithUsage > 1 && usage.maxTotalTokens !== undefined) {
|
|
379
|
+
items.push(costItem("Largest", "Heaviest scenario", formatInteger(usage.maxTotalTokens)));
|
|
380
|
+
}
|
|
381
|
+
return `<section class="cost" aria-labelledby="cost-heading">
|
|
382
|
+
<h2 id="cost-heading">Token cost</h2>
|
|
383
|
+
<p class="cost-lede">Tokens are cost, not the verdict. Pass and fail sit on each scenario.</p>
|
|
384
|
+
<div class="cost-grid">${items.join("")}</div>
|
|
385
|
+
</section>`;
|
|
324
386
|
}
|
|
325
387
|
function formatSigned(value) {
|
|
326
388
|
if (value === undefined) {
|
|
327
389
|
return "n/a";
|
|
328
390
|
}
|
|
329
391
|
if (value > 0) {
|
|
330
|
-
return `+${value}`;
|
|
392
|
+
return `+${formatInteger(value)}`;
|
|
393
|
+
}
|
|
394
|
+
if (value < 0) {
|
|
395
|
+
return `-${formatInteger(Math.abs(value))}`;
|
|
331
396
|
}
|
|
332
|
-
return
|
|
397
|
+
return formatInteger(value);
|
|
333
398
|
}
|
|
334
|
-
function
|
|
335
|
-
if (
|
|
336
|
-
return
|
|
399
|
+
function formatSignedDuration(deltaMs) {
|
|
400
|
+
if (deltaMs === undefined) {
|
|
401
|
+
return "n/a";
|
|
402
|
+
}
|
|
403
|
+
if (deltaMs === 0) {
|
|
404
|
+
return formatDuration(0);
|
|
337
405
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
: `<span class="badge status-failed">fail</span>`;
|
|
406
|
+
const sign = deltaMs > 0 ? "+" : "-";
|
|
407
|
+
return `${sign}${formatDuration(Math.abs(deltaMs))}`;
|
|
341
408
|
}
|
|
342
409
|
function deltaClass(value) {
|
|
343
410
|
if (value === undefined || value === 0) {
|
|
@@ -345,122 +412,181 @@ function deltaClass(value) {
|
|
|
345
412
|
}
|
|
346
413
|
return value > 0 ? "delta-up" : "delta-down";
|
|
347
414
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
415
|
+
function armDurationMs(arm) {
|
|
416
|
+
return typeof arm.durationMs === "number" ? arm.durationMs : undefined;
|
|
417
|
+
}
|
|
418
|
+
function armToolCount(arm) {
|
|
419
|
+
return arm.trace ? arm.trace.toolCalls.length : undefined;
|
|
420
|
+
}
|
|
421
|
+
function formatArmDuration(arm) {
|
|
422
|
+
const ms = armDurationMs(arm);
|
|
423
|
+
return ms === undefined ? "n/a" : formatDuration(ms);
|
|
424
|
+
}
|
|
425
|
+
function formatArmTokens(arm, key) {
|
|
426
|
+
const usage = arm.trace?.usage;
|
|
427
|
+
const value = key === "total"
|
|
428
|
+
? usage?.totalTokens
|
|
429
|
+
: key === "input"
|
|
430
|
+
? usage?.inputTokens
|
|
431
|
+
: usage?.outputTokens;
|
|
432
|
+
return typeof value === "number" ? formatInteger(value) : "n/a";
|
|
433
|
+
}
|
|
434
|
+
function tokenDelta(a, b, key) {
|
|
435
|
+
const pick = (arm) => {
|
|
436
|
+
const usage = arm.trace?.usage;
|
|
437
|
+
return key === "total"
|
|
438
|
+
? usage?.totalTokens
|
|
439
|
+
: key === "input"
|
|
440
|
+
? usage?.inputTokens
|
|
441
|
+
: usage?.outputTokens;
|
|
442
|
+
};
|
|
443
|
+
const aValue = pick(a);
|
|
444
|
+
const bValue = pick(b);
|
|
445
|
+
if (typeof aValue !== "number" || typeof bValue !== "number") {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
return bValue - aValue;
|
|
449
|
+
}
|
|
450
|
+
function winnerCellClass(delta, side) {
|
|
451
|
+
if (delta === undefined || delta === 0) {
|
|
452
|
+
return "";
|
|
453
|
+
}
|
|
454
|
+
if (side === "a") {
|
|
455
|
+
return delta > 0 ? "is-better" : "is-worse";
|
|
456
|
+
}
|
|
457
|
+
return delta < 0 ? "is-better" : "is-worse";
|
|
458
|
+
}
|
|
459
|
+
function renderCompareMetricRow(label, aText, bText, delta, deltaText) {
|
|
460
|
+
return `<tr>
|
|
461
|
+
<th scope="row">${escapeHtml(label)}</th>
|
|
462
|
+
<td class="${winnerCellClass(delta, "a")}">${escapeHtml(aText)}</td>
|
|
463
|
+
<td class="${winnerCellClass(delta, "b")}">${escapeHtml(bText)}</td>
|
|
464
|
+
<td class="${deltaClass(delta)}">${escapeHtml(deltaText)}</td>
|
|
377
465
|
</tr>`;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
466
|
+
}
|
|
467
|
+
function renderCompareMetrics(result) {
|
|
468
|
+
const compare = result.compare;
|
|
469
|
+
if (!compare) {
|
|
470
|
+
return "";
|
|
471
|
+
}
|
|
472
|
+
const aMs = armDurationMs(compare.a);
|
|
473
|
+
const bMs = armDurationMs(compare.b);
|
|
474
|
+
const durationDelta = aMs !== undefined && bMs !== undefined ? bMs - aMs : undefined;
|
|
475
|
+
const aTools = armToolCount(compare.a);
|
|
476
|
+
const bTools = armToolCount(compare.b);
|
|
477
|
+
const toolDelta = aTools !== undefined && bTools !== undefined ? bTools - aTools : undefined;
|
|
478
|
+
const totalDelta = tokenDelta(compare.a, compare.b, "total");
|
|
479
|
+
const callouts = describeCompareOutcome(compare);
|
|
480
|
+
const calloutList = callouts.length > 0
|
|
481
|
+
? `<ul class="compare-callouts">${callouts.map((line) => `<li>${escapeHtml(line)}</li>`).join("")}</ul>`
|
|
385
482
|
: "";
|
|
386
483
|
return `
|
|
387
484
|
<section class="compare">
|
|
388
485
|
<header class="compare-header">
|
|
389
|
-
<
|
|
390
|
-
<p class="muted">${escapeHtml(
|
|
486
|
+
<h3>Comparison</h3>
|
|
487
|
+
<p class="muted">${escapeHtml(compare.a.label)} vs ${escapeHtml(compare.b.label)}. Δ is B minus A. A lower time and a lower token count is better.</p>
|
|
391
488
|
</header>
|
|
392
|
-
|
|
393
|
-
<div class="meta-item"><span class="meta-key">Mean Δ durationMs</span><span class="meta-val">${escapeHtml(formatSigned(report.summary.meanDurationDeltaMs !== undefined ? Math.round(report.summary.meanDurationDeltaMs) : undefined))}</span></div>
|
|
394
|
-
<div class="meta-item"><span class="meta-key">Mean Δ tools</span><span class="meta-val">${escapeHtml(formatSigned(report.summary.meanToolCallDelta !== undefined ? Math.round(report.summary.meanToolCallDelta) : undefined))}</span></div>
|
|
395
|
-
<div class="meta-item"><span class="meta-key">Mean Δ totalTokens</span><span class="meta-val">${escapeHtml(formatSigned(report.summary.meanTotalTokensDelta !== undefined ? Math.round(report.summary.meanTotalTokensDelta) : undefined))}</span></div>
|
|
396
|
-
</div>
|
|
397
|
-
${onlyA}
|
|
398
|
-
${onlyB}
|
|
489
|
+
${calloutList}
|
|
399
490
|
<div class="compare-table-wrap">
|
|
400
491
|
<table class="compare-table">
|
|
401
492
|
<thead>
|
|
402
493
|
<tr>
|
|
403
|
-
<th>
|
|
404
|
-
<th
|
|
405
|
-
<th
|
|
406
|
-
<th
|
|
407
|
-
<th>Δ tools</th>
|
|
408
|
-
<th>Δ skills</th>
|
|
409
|
-
<th>Δ registry</th>
|
|
410
|
-
<th>Δ tokens</th>
|
|
494
|
+
<th>Metric</th>
|
|
495
|
+
<th>${escapeHtml(compare.a.label)}</th>
|
|
496
|
+
<th>${escapeHtml(compare.b.label)}</th>
|
|
497
|
+
<th>Δ</th>
|
|
411
498
|
</tr>
|
|
412
499
|
</thead>
|
|
413
500
|
<tbody>
|
|
414
|
-
${
|
|
501
|
+
${renderCompareMetricRow("Duration", formatArmDuration(compare.a), formatArmDuration(compare.b), durationDelta, formatSignedDuration(durationDelta))}
|
|
502
|
+
${renderCompareMetricRow("Tokens", formatArmTokens(compare.a, "total"), formatArmTokens(compare.b, "total"), totalDelta, formatSigned(totalDelta))}
|
|
503
|
+
${renderCompareMetricRow("In", formatArmTokens(compare.a, "input"), formatArmTokens(compare.b, "input"), tokenDelta(compare.a, compare.b, "input"), formatSigned(tokenDelta(compare.a, compare.b, "input")))}
|
|
504
|
+
${renderCompareMetricRow("Out", formatArmTokens(compare.a, "output"), formatArmTokens(compare.b, "output"), tokenDelta(compare.a, compare.b, "output"), formatSigned(tokenDelta(compare.a, compare.b, "output")))}
|
|
505
|
+
${renderCompareMetricRow("Tools", aTools === undefined ? "n/a" : formatInteger(aTools), bTools === undefined ? "n/a" : formatInteger(bTools), toolDelta, formatSigned(toolDelta))}
|
|
415
506
|
</tbody>
|
|
416
507
|
</table>
|
|
417
508
|
</div>
|
|
418
509
|
</section>`;
|
|
419
510
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
511
|
+
function renderArmMetrics(arm) {
|
|
512
|
+
const parts = [];
|
|
513
|
+
const ms = armDurationMs(arm);
|
|
514
|
+
if (ms !== undefined) {
|
|
515
|
+
parts.push(formatDuration(ms));
|
|
516
|
+
}
|
|
517
|
+
const tokens = formatArmTokens(arm, "total");
|
|
518
|
+
if (tokens !== "n/a") {
|
|
519
|
+
parts.push(`${tokens} tokens`);
|
|
520
|
+
}
|
|
521
|
+
const tools = armToolCount(arm);
|
|
522
|
+
if (tools !== undefined) {
|
|
523
|
+
parts.push(tools === 1 ? "1 tool" : `${formatInteger(tools)} tools`);
|
|
524
|
+
}
|
|
525
|
+
if (parts.length === 0) {
|
|
526
|
+
return "";
|
|
527
|
+
}
|
|
528
|
+
return `<p class="compare-arm-metrics">${escapeHtml(parts.join(" · "))}</p>`;
|
|
529
|
+
}
|
|
530
|
+
function renderArmColumn(arm, side) {
|
|
531
|
+
const description = arm.description
|
|
532
|
+
? `<p class="compare-arm-description">${escapeHtml(arm.description)}</p>`
|
|
533
|
+
: "";
|
|
534
|
+
return `
|
|
535
|
+
<article class="compare-arm compare-arm-${side}">
|
|
536
|
+
<header class="compare-arm-header">
|
|
537
|
+
<p class="compare-arm-kicker">${side === "a" ? "Arm A" : "Arm B"}</p>
|
|
538
|
+
<h3>${escapeHtml(arm.label)}</h3>
|
|
539
|
+
${description}
|
|
540
|
+
${renderArmMetrics(arm)}
|
|
436
541
|
</header>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
542
|
+
${renderChat(arm.trace, arm.prompt)}
|
|
543
|
+
</article>`;
|
|
544
|
+
}
|
|
545
|
+
function renderCompareConversations(result) {
|
|
546
|
+
const compare = result.compare;
|
|
547
|
+
if (!compare) {
|
|
548
|
+
return `<section class="conversation">
|
|
549
|
+
<h3>Conversation</h3>
|
|
550
|
+
${renderChat(result.trace, result.prompt)}
|
|
551
|
+
</section>`;
|
|
552
|
+
}
|
|
553
|
+
return `
|
|
554
|
+
<div class="compare-layout">
|
|
555
|
+
<div class="compare-arms">
|
|
556
|
+
${renderArmColumn(compare.a, "a")}
|
|
557
|
+
${renderArmColumn(compare.b, "b")}
|
|
448
558
|
</div>
|
|
449
|
-
${
|
|
450
|
-
</
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
559
|
+
${renderCompareMetrics(result)}
|
|
560
|
+
</div>`;
|
|
561
|
+
}
|
|
562
|
+
function renderStoryList(title, lines) {
|
|
563
|
+
if (!lines || lines.length === 0) {
|
|
564
|
+
return "";
|
|
565
|
+
}
|
|
566
|
+
const items = lines.map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
567
|
+
return `<section class="story-block"><h3>${escapeHtml(title)}</h3><ul class="story-list">${items}</ul></section>`;
|
|
568
|
+
}
|
|
569
|
+
function renderStory(result) {
|
|
570
|
+
const story = result.story;
|
|
571
|
+
if (!story) {
|
|
572
|
+
return "";
|
|
573
|
+
}
|
|
574
|
+
return `<div class="story">
|
|
575
|
+
${renderStoryList("Criteria", story.criteria)}
|
|
576
|
+
${renderStoryList("Result", [...story.result, ...story.verdict])}
|
|
577
|
+
</div>`;
|
|
454
578
|
}
|
|
455
579
|
function renderScenario(result) {
|
|
456
|
-
const open = result.passed || result.
|
|
457
|
-
const failures = renderFailures(result);
|
|
580
|
+
const open = result.skipped ? "" : !result.passed || result.compare ? " open" : "";
|
|
581
|
+
const failures = result.story ? "" : renderFailures(result);
|
|
458
582
|
const judgeVerdicts = renderJudgeVerdicts(result);
|
|
459
583
|
const tokens = formatTokensBadge(result);
|
|
460
|
-
const usageDetail = renderUsageDetail(usageOf(result));
|
|
461
|
-
const traceMeta = renderTraceMeta(result);
|
|
462
|
-
const
|
|
584
|
+
const usageDetail = result.compare ? "" : renderUsageDetail(usageOf(result));
|
|
585
|
+
const traceMeta = result.compare ? "" : renderTraceMeta(result);
|
|
586
|
+
const story = renderStory(result);
|
|
587
|
+
const diagnostics = story || failures || judgeVerdicts
|
|
463
588
|
? `<div class="diagnostics">
|
|
589
|
+
${story}
|
|
464
590
|
${failures ? `<section><h3>What went wrong</h3>${failures}</section>` : ""}
|
|
465
591
|
${judgeVerdicts ? `<section><h3>Judge verdict</h3>${judgeVerdicts}</section>` : ""}
|
|
466
592
|
</div>`
|
|
@@ -468,133 +594,150 @@ function renderScenario(result) {
|
|
|
468
594
|
const metaRow = usageDetail || traceMeta
|
|
469
595
|
? `<div class="diagnostics meta-row">${usageDetail}${traceMeta}</div>`
|
|
470
596
|
: "";
|
|
597
|
+
const compareClass = result.compare ? " compare-scenario" : "";
|
|
598
|
+
const description = result.description
|
|
599
|
+
? `<span class="scenario-lede">${escapeHtml(result.description)}</span>`
|
|
600
|
+
: "";
|
|
471
601
|
return `
|
|
472
|
-
<details class="scenario ${statusClass(result)}"${open}>
|
|
602
|
+
<details class="scenario ${statusClass(result)}${compareClass}"${open}>
|
|
473
603
|
<summary>
|
|
474
604
|
<span class="badge ${statusClass(result)}">${statusLabel(result)}</span>
|
|
475
|
-
<span class="scenario-
|
|
605
|
+
<span class="scenario-heading">
|
|
606
|
+
<span class="scenario-name">${escapeHtml(result.scenario)}</span>
|
|
607
|
+
${description}
|
|
608
|
+
</span>
|
|
476
609
|
${tokens ? `<span class="tokens">${escapeHtml(tokens)}</span>` : ""}
|
|
477
610
|
<span class="duration">${escapeHtml(formatDuration(result.durationMs))}</span>
|
|
478
611
|
</summary>
|
|
479
612
|
<div class="scenario-body">
|
|
480
613
|
${diagnostics}
|
|
481
614
|
${metaRow}
|
|
482
|
-
|
|
483
|
-
<h3>Conversation</h3>
|
|
484
|
-
${renderChat(result.trace)}
|
|
485
|
-
</section>
|
|
615
|
+
${renderCompareConversations(result)}
|
|
486
616
|
</div>
|
|
487
617
|
</details>`;
|
|
488
618
|
}
|
|
489
619
|
function renderSuite(report) {
|
|
490
620
|
const scenarios = report.results.map(renderScenario).join("\n");
|
|
491
|
-
const usage = (report.summary ?? summarizeReportResults(report.results)).usage;
|
|
492
|
-
const usageLine = usage
|
|
493
|
-
? `<p class="suite-usage">${escapeHtml(formatUsageStats(usage))}</p>`
|
|
494
|
-
: "";
|
|
495
621
|
return `
|
|
496
622
|
<section class="suite">
|
|
497
623
|
<header class="suite-header">
|
|
498
|
-
<div
|
|
499
|
-
|
|
500
|
-
<
|
|
501
|
-
${usageLine}
|
|
624
|
+
<div class="suite-title">
|
|
625
|
+
<h2>${escapeHtml(report.suite)}</h2>
|
|
626
|
+
<span class="host">${escapeHtml(report.host)}</span>
|
|
502
627
|
</div>
|
|
628
|
+
<p class="suite-counts"><span>${formatInteger(report.passed)} passed</span><span>${formatInteger(report.failed)} failed</span><span>${formatInteger(report.skipped)} skipped</span></p>
|
|
503
629
|
</header>
|
|
504
|
-
${usage ? renderUsageStatsBlock(usage, "Suite token usage") : ""}
|
|
505
630
|
${scenarios}
|
|
506
631
|
</section>`;
|
|
507
632
|
}
|
|
508
633
|
function sharedReportCss() {
|
|
509
634
|
return `
|
|
635
|
+
@layer reset, base, layout, components;
|
|
510
636
|
:root {
|
|
511
|
-
|
|
512
|
-
--
|
|
513
|
-
--panel
|
|
514
|
-
--
|
|
515
|
-
--
|
|
516
|
-
--
|
|
517
|
-
--
|
|
518
|
-
--
|
|
519
|
-
--
|
|
520
|
-
--
|
|
521
|
-
--
|
|
522
|
-
--
|
|
523
|
-
--
|
|
524
|
-
--tool
|
|
525
|
-
--
|
|
526
|
-
--
|
|
637
|
+
color-scheme: dark;
|
|
638
|
+
--bg: oklch(0.18 0.02 250);
|
|
639
|
+
--panel: oklch(0.23 0.025 250);
|
|
640
|
+
--panel-2: oklch(0.2 0.022 250);
|
|
641
|
+
--text: oklch(0.93 0.015 250);
|
|
642
|
+
--muted: oklch(0.72 0.03 250);
|
|
643
|
+
--border: oklch(0.32 0.03 250);
|
|
644
|
+
--pass: oklch(0.8 0.16 155);
|
|
645
|
+
--fail: oklch(0.72 0.16 20);
|
|
646
|
+
--skip: oklch(0.84 0.14 85);
|
|
647
|
+
--user-bubble: oklch(0.3 0.06 250);
|
|
648
|
+
--assistant-bubble: oklch(0.26 0.04 155);
|
|
649
|
+
--system-bubble: oklch(0.26 0.04 300);
|
|
650
|
+
--tool: oklch(0.82 0.12 80);
|
|
651
|
+
--tool-bubble: oklch(0.26 0.04 80);
|
|
652
|
+
--arm-a: oklch(0.72 0.12 250);
|
|
653
|
+
--arm-b: oklch(0.78 0.14 75);
|
|
527
654
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
margin: 0;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
color: var(--text);
|
|
534
|
-
line-height: 1.45;
|
|
655
|
+
@layer reset {
|
|
656
|
+
* { box-sizing: border-box; }
|
|
657
|
+
body { margin: 0; }
|
|
658
|
+
h1, h2, h3, h4, p, ol { margin: 0; }
|
|
659
|
+
dl, dd { margin: 0; }
|
|
535
660
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
justify-content: space-between;
|
|
554
|
-
gap: 1rem;
|
|
555
|
-
}
|
|
556
|
-
.stats { display: flex; gap: 0.45rem; align-items: center; flex-wrap: wrap; }
|
|
557
|
-
.stat { padding: 0.3rem 0.55rem; border-radius: 6px; background: #111923; font-size: 0.8rem; }
|
|
558
|
-
.stat strong { font-size: 1rem; margin-right: 0.25rem; }
|
|
559
|
-
.stat-pass strong { color: var(--pass); }
|
|
560
|
-
.stat-fail strong { color: var(--fail); }
|
|
561
|
-
.stat-skip strong { color: var(--skip); }
|
|
562
|
-
.summary dl {
|
|
563
|
-
display: flex;
|
|
564
|
-
flex-wrap: wrap;
|
|
565
|
-
justify-content: flex-end;
|
|
566
|
-
gap: 0.15rem 1rem;
|
|
567
|
-
margin: 0;
|
|
568
|
-
font-size: 0.78rem;
|
|
569
|
-
}
|
|
570
|
-
.summary dt { color: var(--muted); margin-right: -0.7rem; }
|
|
571
|
-
.summary dd { margin: 0; }
|
|
572
|
-
.report-usage { margin-top: 0.75rem; }
|
|
573
|
-
.suite { margin-top: 1.25rem; }
|
|
574
|
-
.suite-header { display: flex; align-items: end; justify-content: space-between; margin: 0 0 0.5rem; padding: 0 0.2rem; }
|
|
575
|
-
.suite-header > div { display: flex; align-items: center; gap: 0.5rem; }
|
|
576
|
-
.host { color: var(--muted); font-size: 0.72rem; background: var(--panel); border: 1px solid var(--border); border-radius: 999px; padding: 0.1rem 0.4rem; }
|
|
577
|
-
.suite-meta { text-align: right; }
|
|
578
|
-
.suite-counts { color: var(--muted); margin: 0; font-size: 0.78rem; }
|
|
579
|
-
.suite-usage { color: var(--muted); margin: 0.15rem 0 0; font-size: 0.72rem; font-variant-numeric: tabular-nums; }
|
|
580
|
-
.tokens { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
|
|
581
|
-
.usage-summary, .scenario-meta {
|
|
582
|
-
background: var(--panel-2);
|
|
583
|
-
border: 1px solid var(--border);
|
|
584
|
-
border-radius: 8px;
|
|
585
|
-
padding: 0.65rem 0.7rem;
|
|
586
|
-
margin: 0 0 0.7rem;
|
|
661
|
+
@layer base {
|
|
662
|
+
body {
|
|
663
|
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
664
|
+
background: var(--bg);
|
|
665
|
+
color: var(--text);
|
|
666
|
+
line-height: 1.5;
|
|
667
|
+
word-spacing: 0.16em;
|
|
668
|
+
}
|
|
669
|
+
main { max-width: 68rem; margin-inline: auto; padding: 1.5rem 1.25rem 3rem; }
|
|
670
|
+
main:has(.compare-layout) { max-width: 96rem; }
|
|
671
|
+
h1 { font-size: 1.7rem; font-weight: 650; letter-spacing: -0.02em; }
|
|
672
|
+
h2 { font-size: 1.05rem; font-weight: 650; }
|
|
673
|
+
h3 { font-size: 0.8rem; color: var(--muted); font-weight: 650; }
|
|
674
|
+
h4 { font-size: 0.75rem; margin-block: 0.9rem 0.35rem; color: var(--muted); font-weight: 650; }
|
|
675
|
+
.muted { color: var(--muted); }
|
|
676
|
+
.empty { color: var(--muted); font-size: 0.85rem; font-style: italic; }
|
|
677
|
+
.empty.note { margin-bottom: 0.5rem; }
|
|
587
678
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
679
|
+
@layer layout {
|
|
680
|
+
.report-header { display: grid; gap: 0.25rem; margin-bottom: 1rem; }
|
|
681
|
+
.brand { color: var(--muted); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
682
|
+
.lede, .when { color: var(--muted); font-size: 0.85rem; }
|
|
683
|
+
.summary, .cost, .guide, .compare {
|
|
684
|
+
background: var(--panel);
|
|
685
|
+
border: 1px solid var(--border);
|
|
686
|
+
border-radius: 12px;
|
|
687
|
+
padding: 0.9rem 1rem;
|
|
688
|
+
}
|
|
689
|
+
.summary { display: grid; gap: 0.65rem; }
|
|
690
|
+
.stats { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
691
|
+
.suite { margin-top: 1.75rem; display: grid; gap: 0.55rem; }
|
|
692
|
+
.suite-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; }
|
|
693
|
+
.suite-title { display: flex; align-items: center; gap: 0.5rem; }
|
|
694
|
+
.cost { margin-top: 0.85rem; display: grid; gap: 0.65rem; }
|
|
695
|
+
.cost-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.75rem; }
|
|
696
|
+
.guide { margin-top: 0.85rem; }
|
|
697
|
+
.diagnostics { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 0.7rem; margin-bottom: 0.9rem; }
|
|
698
|
+
.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.45rem 0.75rem; }
|
|
593
699
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
700
|
+
@layer components {
|
|
701
|
+
.stat {
|
|
702
|
+
display: inline-flex;
|
|
703
|
+
align-items: baseline;
|
|
704
|
+
gap: 0.35rem;
|
|
705
|
+
padding: 0.4rem 0.65rem;
|
|
706
|
+
border-radius: 8px;
|
|
707
|
+
background: var(--panel-2);
|
|
708
|
+
font-size: 0.85rem;
|
|
709
|
+
}
|
|
710
|
+
.stat strong { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
|
|
711
|
+
.stat-pass strong { color: var(--pass); }
|
|
712
|
+
.stat-fail strong { color: var(--fail); }
|
|
713
|
+
.stat-skip strong { color: var(--skip); }
|
|
714
|
+
.guide h2 { font-size: 0.95rem; }
|
|
715
|
+
.guide ol { margin: 0.65rem 0 0; padding-inline-start: 1.2rem; display: grid; gap: 0.35rem; color: var(--muted); font-size: 0.88rem; }
|
|
716
|
+
.cost-lede { color: var(--muted); font-size: 0.85rem; }
|
|
717
|
+
.cost-item { display: grid; gap: 0.15rem; }
|
|
718
|
+
.cost-value { font-size: 1.2rem; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
719
|
+
.cost-label { font-size: 0.8rem; font-weight: 650; }
|
|
720
|
+
.cost-detail { color: var(--muted); font-size: 0.75rem; }
|
|
721
|
+
.host {
|
|
722
|
+
color: var(--muted);
|
|
723
|
+
font-size: 0.72rem;
|
|
724
|
+
background: var(--panel-2);
|
|
725
|
+
border: 1px solid var(--border);
|
|
726
|
+
border-radius: 999px;
|
|
727
|
+
padding: 0.1rem 0.5rem;
|
|
728
|
+
}
|
|
729
|
+
.suite-counts { display: flex; flex-wrap: wrap; gap: 0.75rem; color: var(--muted); font-size: 0.85rem; }
|
|
730
|
+
.tokens { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
|
|
731
|
+
.usage-summary, .scenario-meta {
|
|
732
|
+
background: var(--panel-2);
|
|
733
|
+
border: 1px solid var(--border);
|
|
734
|
+
border-radius: 8px;
|
|
735
|
+
padding: 0.65rem 0.7rem;
|
|
736
|
+
}
|
|
737
|
+
.meta-item { display: grid; gap: 0.1rem; }
|
|
738
|
+
.meta-key { color: var(--muted); font-size: 0.75rem; }
|
|
739
|
+
.meta-val { font-size: 0.88rem; font-variant-numeric: tabular-nums; word-break: break-word; }
|
|
740
|
+
.meta-row { margin-bottom: 0.7rem; }
|
|
598
741
|
details.scenario {
|
|
599
742
|
background: var(--panel);
|
|
600
743
|
border: 1px solid var(--border);
|
|
@@ -613,7 +756,9 @@ function sharedReportCss() {
|
|
|
613
756
|
}
|
|
614
757
|
details.scenario[open] summary { border-bottom: 1px solid var(--border); background: #17202d; }
|
|
615
758
|
details.scenario summary::-webkit-details-marker { display: none; }
|
|
759
|
+
.scenario-heading { display: grid; gap: 0.1rem; min-width: 12rem; flex: 1 1 16rem; }
|
|
616
760
|
.scenario-name { font-weight: 600; font-size: 0.9rem; }
|
|
761
|
+
.scenario-lede { color: var(--muted); font-size: 0.78rem; font-weight: 400; }
|
|
617
762
|
.duration { color: var(--muted); margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
618
763
|
.badge {
|
|
619
764
|
display: inline-block;
|
|
@@ -633,6 +778,9 @@ function sharedReportCss() {
|
|
|
633
778
|
.diagnostics section { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.65rem 0.7rem; min-width: 0; }
|
|
634
779
|
.conversation { min-width: 0; }
|
|
635
780
|
|
|
781
|
+
.story { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.85rem; margin-bottom: 0.85rem; }
|
|
782
|
+
.story-block h3 { margin: 0 0 0.35rem; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
|
|
783
|
+
.story-list { margin: 0; padding-left: 1.1rem; display: flex; flex-direction: column; gap: 0.25rem; color: var(--text); }
|
|
636
784
|
.failures { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
|
|
637
785
|
.failures li { border-left: 3px solid var(--fail); padding-left: 0.6rem; }
|
|
638
786
|
.failure-label { margin: 0; font-weight: 600; font-size: 0.85rem; color: var(--fail); }
|
|
@@ -701,16 +849,61 @@ function sharedReportCss() {
|
|
|
701
849
|
.shell-commands li { font-size: 0.8rem; background: #0b1017; border: 1px solid var(--border); border-radius: 6px; padding: 0.3rem 0.5rem; }
|
|
702
850
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85em; }
|
|
703
851
|
|
|
852
|
+
.compare-layout {
|
|
853
|
+
container-type: inline-size;
|
|
854
|
+
display: grid;
|
|
855
|
+
gap: 0.9rem;
|
|
856
|
+
}
|
|
857
|
+
.compare-arms {
|
|
858
|
+
display: grid;
|
|
859
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
860
|
+
gap: 0.85rem;
|
|
861
|
+
align-items: stretch;
|
|
862
|
+
}
|
|
863
|
+
.compare-arm {
|
|
864
|
+
min-width: 0;
|
|
865
|
+
display: grid;
|
|
866
|
+
align-content: start;
|
|
867
|
+
gap: 0.65rem;
|
|
868
|
+
background: var(--panel-2);
|
|
869
|
+
border: 1px solid var(--border);
|
|
870
|
+
border-radius: 10px;
|
|
871
|
+
padding: 0.75rem 0.8rem 0.85rem;
|
|
872
|
+
border-block-start-width: 3px;
|
|
873
|
+
}
|
|
874
|
+
.compare-arm-a { border-block-start-color: var(--arm-a); }
|
|
875
|
+
.compare-arm-b { border-block-start-color: var(--arm-b); }
|
|
876
|
+
.compare-arm-header { display: grid; gap: 0.15rem; padding-bottom: 0.55rem; border-bottom: 1px solid var(--border); }
|
|
877
|
+
.compare-arm-kicker {
|
|
878
|
+
color: var(--muted);
|
|
879
|
+
font-size: 0.66rem;
|
|
880
|
+
font-weight: 700;
|
|
881
|
+
letter-spacing: 0.06em;
|
|
882
|
+
text-transform: uppercase;
|
|
883
|
+
}
|
|
884
|
+
.compare-arm-a .compare-arm-kicker { color: var(--arm-a); }
|
|
885
|
+
.compare-arm-b .compare-arm-kicker { color: var(--arm-b); }
|
|
886
|
+
.compare-arm-header h3 { color: var(--text); font-size: 0.95rem; }
|
|
887
|
+
.compare-arm-description { color: var(--muted); font-size: 0.8rem; }
|
|
888
|
+
.compare-arm-metrics { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
|
|
889
|
+
.compare-arm .bubble, .compare-arm .tool-card { max-width: 100%; }
|
|
890
|
+
|
|
704
891
|
.compare {
|
|
705
|
-
margin-top:
|
|
892
|
+
margin-top: 0;
|
|
706
893
|
background: var(--panel);
|
|
707
894
|
border: 1px solid var(--border);
|
|
708
895
|
border-radius: 10px;
|
|
709
896
|
padding: 0.85rem 0.9rem 1rem;
|
|
710
897
|
}
|
|
711
898
|
.compare-header { margin-bottom: 0.65rem; }
|
|
712
|
-
.compare-header
|
|
713
|
-
.compare-
|
|
899
|
+
.compare-header h3 { margin-bottom: 0.2rem; }
|
|
900
|
+
.compare-callouts {
|
|
901
|
+
margin: 0 0 0.75rem;
|
|
902
|
+
padding-left: 1.1rem;
|
|
903
|
+
display: grid;
|
|
904
|
+
gap: 0.25rem;
|
|
905
|
+
font-size: 0.85rem;
|
|
906
|
+
}
|
|
714
907
|
.compare-table-wrap { overflow-x: auto; }
|
|
715
908
|
.compare-table {
|
|
716
909
|
width: 100%;
|
|
@@ -725,20 +918,23 @@ function sharedReportCss() {
|
|
|
725
918
|
vertical-align: middle;
|
|
726
919
|
}
|
|
727
920
|
.compare-table th { color: var(--muted); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
728
|
-
.compare-name { font-weight: 600; }
|
|
729
|
-
.compare-regress { background: color-mix(in srgb, var(--regress) 10%, transparent); }
|
|
730
|
-
.compare-improve { background: color-mix(in srgb, var(--improve) 10%, transparent); }
|
|
731
921
|
.delta-up { color: var(--fail); }
|
|
732
922
|
.delta-down { color: var(--pass); }
|
|
733
923
|
.delta-flat { color: var(--muted); }
|
|
924
|
+
.is-better { color: var(--pass); font-weight: 650; }
|
|
925
|
+
.is-worse { color: var(--muted); }
|
|
926
|
+
|
|
927
|
+
@container (max-width: 44rem) {
|
|
928
|
+
.compare-arms { grid-template-columns: minmax(0, 1fr); }
|
|
929
|
+
}
|
|
734
930
|
|
|
735
931
|
@media (max-width: 720px) {
|
|
736
932
|
main { padding: 0.75rem; }
|
|
737
|
-
.summary { align-items: flex-start; flex-direction: column; }
|
|
738
|
-
.summary dl { justify-content: flex-start; }
|
|
739
933
|
.suite-header { align-items: flex-start; flex-direction: column; gap: 0.2rem; }
|
|
740
934
|
.diagnostics { grid-template-columns: 1fr; }
|
|
741
935
|
.bubble, .tool-card { max-width: 92%; }
|
|
936
|
+
.compare-arms { grid-template-columns: minmax(0, 1fr); }
|
|
937
|
+
}
|
|
742
938
|
}
|
|
743
939
|
`;
|
|
744
940
|
}
|
|
@@ -748,22 +944,11 @@ export function renderHtmlReport(reports, meta = {}) {
|
|
|
748
944
|
const totalPassed = reports.reduce((sum, report) => sum + report.passed, 0);
|
|
749
945
|
const totalFailed = reports.reduce((sum, report) => sum + report.failed, 0);
|
|
750
946
|
const totalSkipped = reports.reduce((sum, report) => sum + report.skipped, 0);
|
|
751
|
-
const
|
|
947
|
+
const hostNames = [...new Set(reports.map((report) => report.host).filter(Boolean))];
|
|
948
|
+
const host = meta.host ?? (hostNames.length > 0 ? hostNames.join(", ") : "unknown");
|
|
752
949
|
const runUsage = summarizeReports(reports).usage;
|
|
753
|
-
// Embed A/B only when explicitly requested (compare-pairs / compare labels), not for every 2-suite run.
|
|
754
|
-
const includeCompare = reports.length === 2 &&
|
|
755
|
-
(meta.includeCompare === true ||
|
|
756
|
-
meta.compareALabel !== undefined ||
|
|
757
|
-
meta.compareBLabel !== undefined);
|
|
758
|
-
const compareSection = includeCompare && reports[0] && reports[1]
|
|
759
|
-
? renderCompareHtmlSection(compareSuiteReports({
|
|
760
|
-
aLabel: meta.compareALabel ?? reports[0].suite,
|
|
761
|
-
bLabel: meta.compareBLabel ?? reports[1].suite,
|
|
762
|
-
a: reports[0],
|
|
763
|
-
b: reports[1],
|
|
764
|
-
}))
|
|
765
|
-
: "";
|
|
766
950
|
const suitesHtml = reports.map(renderSuite).join("\n");
|
|
951
|
+
const suiteWord = reports.length === 1 ? "suite" : "suites";
|
|
767
952
|
return `<!DOCTYPE html>
|
|
768
953
|
<html lang="en">
|
|
769
954
|
<head>
|
|
@@ -775,32 +960,28 @@ export function renderHtmlReport(reports, meta = {}) {
|
|
|
775
960
|
<body>
|
|
776
961
|
<main>
|
|
777
962
|
<header class="report-header">
|
|
778
|
-
<
|
|
779
|
-
<
|
|
963
|
+
<p class="brand">agent-test</p>
|
|
964
|
+
<h1>Run report</h1>
|
|
965
|
+
<p class="lede">${escapeHtml(String(host))} · ${formatInteger(reports.length)} ${suiteWord}${meta.suitesDir ? ` · ${escapeHtml(meta.suitesDir)}` : ""}</p>
|
|
780
966
|
</header>
|
|
781
|
-
<
|
|
967
|
+
<section class="summary" aria-label="Run verdict">
|
|
782
968
|
<div class="stats">
|
|
783
|
-
<span class="stat stat-pass"><strong>${totalPassed}</strong>
|
|
784
|
-
<span class="stat stat-fail"><strong>${totalFailed}</strong>
|
|
785
|
-
<span class="stat stat-skip"><strong>${totalSkipped}</strong>
|
|
786
|
-
${runUsage?.sumTotalTokens !== undefined
|
|
787
|
-
? `<span class="stat"><strong>${runUsage.sumTotalTokens}</strong> tokens</span>`
|
|
788
|
-
: ""}
|
|
969
|
+
<span class="stat stat-pass"><strong>${formatInteger(totalPassed)}</strong><span>passed</span></span>
|
|
970
|
+
<span class="stat stat-fail"><strong>${formatInteger(totalFailed)}</strong><span>failed</span></span>
|
|
971
|
+
<span class="stat stat-skip"><strong>${formatInteger(totalSkipped)}</strong><span>skipped</span></span>
|
|
789
972
|
</div>
|
|
790
|
-
<
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
${runUsage ? `<div class="report-usage">${renderUsageStatsBlock(runUsage, "Run token usage")}</div>` : ""}
|
|
803
|
-
${compareSection}
|
|
973
|
+
<p class="when">Generated ${escapeHtml(generatedAt.toISOString())}</p>
|
|
974
|
+
</section>
|
|
975
|
+
<section class="guide" aria-labelledby="guide-heading">
|
|
976
|
+
<h2 id="guide-heading">How to read this report</h2>
|
|
977
|
+
<ol>
|
|
978
|
+
<li>The verdict is pass or fail. Tokens are cost, not the score.</li>
|
|
979
|
+
<li>Open a scenario for the criteria and the result. A compare scenario shows the two arms side by side, then a comparison of time, tokens, and tools.</li>
|
|
980
|
+
<li>Typical is the middle scenario cost. Largest is the heaviest scenario.</li>
|
|
981
|
+
<li>In is prompt and context. Out is generated text.</li>
|
|
982
|
+
</ol>
|
|
983
|
+
</section>
|
|
984
|
+
${renderCostSection(runUsage)}
|
|
804
985
|
${suitesHtml}
|
|
805
986
|
</main>
|
|
806
987
|
</body>
|