@post-print/agent-test 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -43
- package/dist/check.d.ts +38 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +169 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +11 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +327 -318
- package/dist/cli.js.map +1 -1
- package/dist/compare-scenario.d.ts +33 -0
- package/dist/compare-scenario.d.ts.map +1 -0
- package/dist/compare-scenario.js +128 -0
- package/dist/compare-scenario.js.map +1 -0
- package/dist/debug-bundle.d.ts +2 -1
- package/dist/debug-bundle.d.ts.map +1 -1
- package/dist/debug-bundle.js +12 -5
- package/dist/debug-bundle.js.map +1 -1
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +133 -11
- package/dist/doctor.js.map +1 -1
- package/dist/expect.d.ts +6 -5
- package/dist/expect.d.ts.map +1 -1
- package/dist/expect.js +67 -57
- package/dist/expect.js.map +1 -1
- package/dist/host-log.d.ts +17 -0
- package/dist/host-log.d.ts.map +1 -0
- package/dist/host-log.js +154 -0
- package/dist/host-log.js.map +1 -0
- package/dist/hosts.d.ts +22 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +84 -0
- package/dist/hosts.js.map +1 -0
- package/dist/html-report.d.ts +0 -13
- package/dist/html-report.d.ts.map +1 -1
- package/dist/html-report.js +381 -318
- package/dist/html-report.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -6
- package/dist/index.js.map +1 -1
- package/dist/live-isolation.d.ts +11 -4
- package/dist/live-isolation.d.ts.map +1 -1
- package/dist/live-isolation.js +31 -10
- package/dist/live-isolation.js.map +1 -1
- package/dist/load-adapters.d.ts +13 -0
- package/dist/load-adapters.d.ts.map +1 -0
- package/dist/load-adapters.js +76 -0
- package/dist/load-adapters.js.map +1 -0
- package/dist/load-suite.d.ts.map +1 -1
- package/dist/load-suite.js +6 -2
- package/dist/load-suite.js.map +1 -1
- package/dist/mcp-config.d.ts +8 -0
- package/dist/mcp-config.d.ts.map +1 -1
- package/dist/mcp-config.js +46 -0
- package/dist/mcp-config.js.map +1 -1
- package/dist/progress.d.ts +7 -0
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +39 -9
- package/dist/progress.js.map +1 -1
- package/dist/record-trace.d.ts +9 -7
- package/dist/record-trace.d.ts.map +1 -1
- package/dist/record-trace.js +5 -16
- package/dist/record-trace.js.map +1 -1
- package/dist/report-preview-server.d.ts +2 -0
- package/dist/report-preview-server.d.ts.map +1 -0
- package/dist/report-preview-server.js +24 -0
- package/dist/report-preview-server.js.map +1 -0
- package/dist/report-preview.d.ts +11 -0
- package/dist/report-preview.d.ts.map +1 -0
- package/dist/report-preview.js +161 -0
- package/dist/report-preview.js.map +1 -0
- package/dist/run-suite.d.ts +34 -1
- package/dist/run-suite.d.ts.map +1 -1
- package/dist/run-suite.js +551 -82
- package/dist/run-suite.js.map +1 -1
- package/dist/scenario-story.d.ts +41 -0
- package/dist/scenario-story.d.ts.map +1 -0
- package/dist/scenario-story.js +198 -0
- package/dist/scenario-story.js.map +1 -0
- package/dist/suite-summary.d.ts +2 -0
- package/dist/suite-summary.d.ts.map +1 -1
- package/dist/suite-summary.js +70 -22
- package/dist/suite-summary.js.map +1 -1
- package/dist/theme.d.ts +29 -5
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +204 -45
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +97 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-seeds.d.ts.map +1 -1
- package/dist/validate-seeds.js +21 -6
- package/dist/validate-seeds.js.map +1 -1
- package/dist/validate-suite.d.ts +3 -1
- package/dist/validate-suite.d.ts.map +1 -1
- package/dist/validate-suite.js +258 -23
- package/dist/validate-suite.js.map +1 -1
- package/package.json +4 -4
- package/dist/compare.d.ts +0 -79
- package/dist/compare.d.ts.map +0 -1
- package/dist/compare.js +0 -232
- package/dist/compare.js.map +0 -1
package/dist/html-report.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
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 { formatUsageStats, summarizeReportResults, summarizeReports } from "./suite-summary.js";
|
|
4
|
+
import { summarizeReports } from "./suite-summary.js";
|
|
6
5
|
function escapeHtml(value) {
|
|
7
6
|
return value
|
|
8
7
|
.replaceAll("&", "&")
|
|
@@ -11,11 +10,19 @@ function escapeHtml(value) {
|
|
|
11
10
|
.replaceAll('"', """)
|
|
12
11
|
.replaceAll("'", "'");
|
|
13
12
|
}
|
|
13
|
+
const INTEGER_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
|
|
14
|
+
const DECIMAL_FORMAT = new Intl.NumberFormat("en-US", {
|
|
15
|
+
minimumFractionDigits: 1,
|
|
16
|
+
maximumFractionDigits: 1,
|
|
17
|
+
});
|
|
18
|
+
function formatInteger(value) {
|
|
19
|
+
return INTEGER_FORMAT.format(Math.round(value));
|
|
20
|
+
}
|
|
14
21
|
function formatDuration(ms) {
|
|
15
22
|
if (ms < 1000) {
|
|
16
|
-
return `${
|
|
23
|
+
return `${formatInteger(ms)}ms`;
|
|
17
24
|
}
|
|
18
|
-
return `${(ms / 1000)
|
|
25
|
+
return `${DECIMAL_FORMAT.format(ms / 1000)}s`;
|
|
19
26
|
}
|
|
20
27
|
function statusLabel(result) {
|
|
21
28
|
if (result.skipped) {
|
|
@@ -29,62 +36,95 @@ function statusClass(result) {
|
|
|
29
36
|
}
|
|
30
37
|
return result.passed ? "status-passed" : "status-failed";
|
|
31
38
|
}
|
|
39
|
+
const MISSING_REQUIREMENT = {
|
|
40
|
+
label: "Missing requirement",
|
|
41
|
+
hint: "A required string did not appear in assistant text, commands, or tool results.",
|
|
42
|
+
};
|
|
43
|
+
const FORBIDDEN_BEHAVIOR = {
|
|
44
|
+
label: "Forbidden behavior",
|
|
45
|
+
hint: "The agent did something it was told not to do.",
|
|
46
|
+
};
|
|
47
|
+
const MISSING_COMMAND = {
|
|
48
|
+
label: "Missing command",
|
|
49
|
+
hint: "An expected command never ran.",
|
|
50
|
+
};
|
|
51
|
+
const MISSING_TOOL = {
|
|
52
|
+
label: "Missing tool call",
|
|
53
|
+
hint: "An expected tool was never invoked.",
|
|
54
|
+
};
|
|
55
|
+
const FORBIDDEN_TOOL = {
|
|
56
|
+
label: "Forbidden tool call",
|
|
57
|
+
hint: "A tool the scenario forbids was invoked.",
|
|
58
|
+
};
|
|
59
|
+
const MISSING_SKILL = {
|
|
60
|
+
label: "Missing skill",
|
|
61
|
+
hint: "The agent did not read a skill it was expected to use.",
|
|
62
|
+
};
|
|
63
|
+
const UNEXPECTED_SKILL = {
|
|
64
|
+
label: "Unexpected skill",
|
|
65
|
+
hint: "The agent read a skill it should have avoided.",
|
|
66
|
+
};
|
|
67
|
+
const MISSING_READ_PATH = {
|
|
68
|
+
label: "Missing read path",
|
|
69
|
+
hint: "Expected a Read tool call whose args mention this path fragment (registry-first / grounding).",
|
|
70
|
+
};
|
|
71
|
+
const FORBIDDEN_READ_PATH = {
|
|
72
|
+
label: "Forbidden read path",
|
|
73
|
+
hint: "A successful Read returned content from a path the scenario forbids (miss attempts do not fail).",
|
|
74
|
+
};
|
|
75
|
+
const ROUTING = {
|
|
76
|
+
label: "Routing",
|
|
77
|
+
hint: "The routing announcement did not match expectations.",
|
|
78
|
+
};
|
|
32
79
|
/** Human-readable label + one-line explanation for the matcher codes assertRubric/judge emit. */
|
|
33
80
|
const MATCHER_LABELS = {
|
|
34
81
|
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.",
|
|
82
|
+
liveScenario: {
|
|
83
|
+
label: "Isolated scenario",
|
|
84
|
+
hint: "The scenario subprocess exited before it wrote a result.",
|
|
48
85
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
must: MISSING_REQUIREMENT,
|
|
87
|
+
mustInclude: MISSING_REQUIREMENT,
|
|
88
|
+
mustNot: FORBIDDEN_BEHAVIOR,
|
|
89
|
+
mustNotInclude: FORBIDDEN_BEHAVIOR,
|
|
90
|
+
mustRun: MISSING_COMMAND,
|
|
91
|
+
toHaveRunCommand: MISSING_COMMAND,
|
|
92
|
+
mustCallTool: MISSING_TOOL,
|
|
93
|
+
toHaveCalledTool: MISSING_TOOL,
|
|
94
|
+
mustNotCallTool: FORBIDDEN_TOOL,
|
|
95
|
+
toHaveNotCalledTool: FORBIDDEN_TOOL,
|
|
96
|
+
mustInvokeSkill: MISSING_SKILL,
|
|
97
|
+
toHaveInvokedSkill: MISSING_SKILL,
|
|
98
|
+
mustNotInvokeSkill: UNEXPECTED_SKILL,
|
|
99
|
+
toHaveNotInvokedSkill: UNEXPECTED_SKILL,
|
|
100
|
+
mustReadPath: MISSING_READ_PATH,
|
|
101
|
+
toHaveReadPath: MISSING_READ_PATH,
|
|
102
|
+
mustNotReadPath: FORBIDDEN_READ_PATH,
|
|
103
|
+
toHaveNotReadPath: FORBIDDEN_READ_PATH,
|
|
104
|
+
routingBlock: ROUTING,
|
|
105
|
+
toIncludeRoutingBlock: ROUTING,
|
|
106
|
+
toHaveRoutingBlockBeforeTools: ROUTING,
|
|
107
|
+
toHaveTier: ROUTING,
|
|
108
|
+
toHaveHandsOnTier: ROUTING,
|
|
109
|
+
toHaveHandsOnTierBeforeTools: ROUTING,
|
|
110
|
+
toHaveReviewDepth: {
|
|
111
|
+
label: "Review depth",
|
|
112
|
+
hint: "The review depth announcement did not match the rubric.",
|
|
52
113
|
},
|
|
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).",
|
|
68
|
-
},
|
|
69
|
-
mustNotReadPath: {
|
|
70
|
-
label: "Forbidden read path",
|
|
71
|
-
hint: "A successful Read returned content from a path the scenario forbids (miss attempts do not fail).",
|
|
72
|
-
},
|
|
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
114
|
workingTreeLeak: {
|
|
83
115
|
label: "Working tree leak",
|
|
84
|
-
hint: "The agent
|
|
116
|
+
hint: "The agent used a path outside the sealed workspace, or edited the caller checkout.",
|
|
85
117
|
},
|
|
86
118
|
recordTrace: { label: "Recording failed", hint: "Saving the trace to disk failed." },
|
|
87
119
|
judge: { label: "Judge", hint: "The LLM judge flagged this scenario." },
|
|
120
|
+
faster: {
|
|
121
|
+
label: "Faster arm",
|
|
122
|
+
hint: "The named arm must finish in less time.",
|
|
123
|
+
},
|
|
124
|
+
cheaper: {
|
|
125
|
+
label: "Cheaper arm",
|
|
126
|
+
hint: "The named arm must use fewer tokens.",
|
|
127
|
+
},
|
|
88
128
|
};
|
|
89
129
|
function humanizeMatcher(matcher) {
|
|
90
130
|
const [base] = matcher.split(":");
|
|
@@ -153,23 +193,45 @@ function buildOrderedTimeline(trace) {
|
|
|
153
193
|
}
|
|
154
194
|
return items.sort((a, b) => a.seq - b.seq);
|
|
155
195
|
}
|
|
156
|
-
function
|
|
196
|
+
function promptAlreadyInTrace(trace, prompt) {
|
|
197
|
+
return (trace?.messages.some((message) => message.role === "user" && message.content.trim() === prompt) ?? false);
|
|
198
|
+
}
|
|
199
|
+
function renderPromptBubble(prompt, trace) {
|
|
200
|
+
const text = prompt?.trim();
|
|
201
|
+
if (!text || promptAlreadyInTrace(trace, text)) {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
return renderMessageBubble({ role: "user", content: text });
|
|
205
|
+
}
|
|
206
|
+
function renderChat(trace, prompt) {
|
|
207
|
+
const promptBubble = renderPromptBubble(prompt, trace);
|
|
157
208
|
if (!trace) {
|
|
209
|
+
if (promptBubble) {
|
|
210
|
+
return `<div class="chat">${promptBubble}</div>`;
|
|
211
|
+
}
|
|
158
212
|
return `<p class="empty">No transcript recorded for this scenario.</p>`;
|
|
159
213
|
}
|
|
160
214
|
const parts = [];
|
|
161
215
|
const timeline = buildOrderedTimeline(trace);
|
|
162
216
|
if (timeline) {
|
|
163
217
|
parts.push(`<div class="chat">`);
|
|
218
|
+
if (promptBubble) {
|
|
219
|
+
parts.push(promptBubble);
|
|
220
|
+
}
|
|
164
221
|
for (const item of timeline) {
|
|
165
222
|
parts.push(item.kind === "message" ? renderMessageBubble(item.message) : renderToolCallCard(item.call));
|
|
166
223
|
}
|
|
167
224
|
parts.push(`</div>`);
|
|
168
225
|
}
|
|
169
|
-
else if (trace.messages.length > 0 || trace.toolCalls.length > 0) {
|
|
170
|
-
|
|
171
|
-
|
|
226
|
+
else if (trace.messages.length > 0 || trace.toolCalls.length > 0 || promptBubble) {
|
|
227
|
+
if (!timeline && (trace.messages.length > 0 || trace.toolCalls.length > 0)) {
|
|
228
|
+
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>`);
|
|
229
|
+
}
|
|
230
|
+
if (promptBubble || trace.messages.length > 0) {
|
|
172
231
|
parts.push(`<div class="chat">`);
|
|
232
|
+
if (promptBubble) {
|
|
233
|
+
parts.push(promptBubble);
|
|
234
|
+
}
|
|
173
235
|
for (const message of trace.messages) {
|
|
174
236
|
parts.push(renderMessageBubble(message));
|
|
175
237
|
}
|
|
@@ -243,14 +305,14 @@ function formatTokensBadge(result) {
|
|
|
243
305
|
return undefined;
|
|
244
306
|
}
|
|
245
307
|
if (typeof usage.totalTokens === "number") {
|
|
246
|
-
return `${usage.totalTokens}
|
|
308
|
+
return `${formatInteger(usage.totalTokens)} tokens`;
|
|
247
309
|
}
|
|
248
310
|
const parts = [];
|
|
249
311
|
if (typeof usage.inputTokens === "number") {
|
|
250
|
-
parts.push(`in ${usage.inputTokens}`);
|
|
312
|
+
parts.push(`in ${formatInteger(usage.inputTokens)}`);
|
|
251
313
|
}
|
|
252
314
|
if (typeof usage.outputTokens === "number") {
|
|
253
|
-
parts.push(`out ${usage.outputTokens}`);
|
|
315
|
+
parts.push(`out ${formatInteger(usage.outputTokens)}`);
|
|
254
316
|
}
|
|
255
317
|
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
256
318
|
}
|
|
@@ -271,7 +333,7 @@ function renderUsageDetail(usage) {
|
|
|
271
333
|
return "";
|
|
272
334
|
}
|
|
273
335
|
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>`)
|
|
336
|
+
.map(([label, value]) => `<div class="meta-item"><span class="meta-key">${escapeHtml(label)}</span><span class="meta-val">${formatInteger(value ?? 0)}</span></div>`)
|
|
275
337
|
.join("");
|
|
276
338
|
return `<section class="scenario-meta"><h3>Token usage</h3><div class="meta-grid">${items}</div></section>`;
|
|
277
339
|
}
|
|
@@ -282,62 +344,66 @@ function renderTraceMeta(result) {
|
|
|
282
344
|
}
|
|
283
345
|
const skills = trace.skillsInvoked?.length ? trace.skillsInvoked.join(", ") : "(none)";
|
|
284
346
|
const items = [
|
|
285
|
-
["Messages",
|
|
286
|
-
["Tool calls",
|
|
347
|
+
["Messages", formatInteger(trace.messages.length)],
|
|
348
|
+
["Tool calls", formatInteger(trace.toolCalls.length)],
|
|
287
349
|
["Skills invoked", skills],
|
|
288
350
|
["Routing tier", trace.routing?.tier ?? "(none)"],
|
|
289
351
|
];
|
|
290
352
|
if (result.attempts !== undefined && result.attempts > 1) {
|
|
291
|
-
items.push(["Attempts",
|
|
353
|
+
items.push(["Attempts", formatInteger(result.attempts)]);
|
|
292
354
|
}
|
|
293
355
|
const html = items
|
|
294
356
|
.map(([label, value]) => `<div class="meta-item"><span class="meta-key">${escapeHtml(label)}</span><span class="meta-val">${escapeHtml(value)}</span></div>`)
|
|
295
357
|
.join("");
|
|
296
358
|
return `<section class="scenario-meta"><h3>Trace stats</h3><div class="meta-grid">${html}</div></section>`;
|
|
297
359
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
360
|
+
function costItem(label, detail, value) {
|
|
361
|
+
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>`;
|
|
362
|
+
}
|
|
363
|
+
function renderCostSection(usage) {
|
|
364
|
+
if (!usage || usage.sumTotalTokens === undefined) {
|
|
300
365
|
return "";
|
|
301
366
|
}
|
|
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
|
-
}
|
|
367
|
+
const items = [costItem("Total", "Cost of this run", formatInteger(usage.sumTotalTokens))];
|
|
314
368
|
if (usage.sumInputTokens !== undefined) {
|
|
315
|
-
|
|
369
|
+
items.push(costItem("In", "Prompt and context the host sent", formatInteger(usage.sumInputTokens)));
|
|
316
370
|
}
|
|
317
371
|
if (usage.sumOutputTokens !== undefined) {
|
|
318
|
-
|
|
372
|
+
items.push(costItem("Out", "Text the agent wrote", formatInteger(usage.sumOutputTokens)));
|
|
319
373
|
}
|
|
320
|
-
|
|
321
|
-
.
|
|
322
|
-
|
|
323
|
-
|
|
374
|
+
if (usage.scenariosWithUsage > 1 && usage.p50TotalTokens !== undefined) {
|
|
375
|
+
items.push(costItem("Typical", "Middle scenario. Half cost less than this.", formatInteger(usage.p50TotalTokens)));
|
|
376
|
+
}
|
|
377
|
+
if (usage.scenariosWithUsage > 1 && usage.maxTotalTokens !== undefined) {
|
|
378
|
+
items.push(costItem("Largest", "Heaviest scenario", formatInteger(usage.maxTotalTokens)));
|
|
379
|
+
}
|
|
380
|
+
return `<section class="cost" aria-labelledby="cost-heading">
|
|
381
|
+
<h2 id="cost-heading">Token cost</h2>
|
|
382
|
+
<p class="cost-lede">Tokens are cost, not the verdict. Pass and fail sit on each scenario.</p>
|
|
383
|
+
<div class="cost-grid">${items.join("")}</div>
|
|
384
|
+
</section>`;
|
|
324
385
|
}
|
|
325
386
|
function formatSigned(value) {
|
|
326
387
|
if (value === undefined) {
|
|
327
388
|
return "n/a";
|
|
328
389
|
}
|
|
329
390
|
if (value > 0) {
|
|
330
|
-
return `+${value}`;
|
|
391
|
+
return `+${formatInteger(value)}`;
|
|
392
|
+
}
|
|
393
|
+
if (value < 0) {
|
|
394
|
+
return `-${formatInteger(Math.abs(value))}`;
|
|
331
395
|
}
|
|
332
|
-
return
|
|
396
|
+
return formatInteger(value);
|
|
333
397
|
}
|
|
334
|
-
function
|
|
335
|
-
if (
|
|
336
|
-
return
|
|
398
|
+
function formatSignedDuration(deltaMs) {
|
|
399
|
+
if (deltaMs === undefined) {
|
|
400
|
+
return "n/a";
|
|
401
|
+
}
|
|
402
|
+
if (deltaMs === 0) {
|
|
403
|
+
return formatDuration(0);
|
|
337
404
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
: `<span class="badge status-failed">fail</span>`;
|
|
405
|
+
const sign = deltaMs > 0 ? "+" : "-";
|
|
406
|
+
return `${sign}${formatDuration(Math.abs(deltaMs))}`;
|
|
341
407
|
}
|
|
342
408
|
function deltaClass(value) {
|
|
343
409
|
if (value === undefined || value === 0) {
|
|
@@ -345,122 +411,116 @@ function deltaClass(value) {
|
|
|
345
411
|
}
|
|
346
412
|
return value > 0 ? "delta-up" : "delta-down";
|
|
347
413
|
}
|
|
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
|
-
|
|
414
|
+
function armDurationMs(arm) {
|
|
415
|
+
return typeof arm.durationMs === "number" ? arm.durationMs : undefined;
|
|
416
|
+
}
|
|
417
|
+
function armToolCount(arm) {
|
|
418
|
+
return arm.trace ? arm.trace.toolCalls.length : undefined;
|
|
419
|
+
}
|
|
420
|
+
function formatArmDuration(arm) {
|
|
421
|
+
const ms = armDurationMs(arm);
|
|
422
|
+
return ms === undefined ? "n/a" : formatDuration(ms);
|
|
423
|
+
}
|
|
424
|
+
function formatArmTokens(arm, key) {
|
|
425
|
+
const usage = arm.trace?.usage;
|
|
426
|
+
const value = key === "total"
|
|
427
|
+
? usage?.totalTokens
|
|
428
|
+
: key === "input"
|
|
429
|
+
? usage?.inputTokens
|
|
430
|
+
: usage?.outputTokens;
|
|
431
|
+
return typeof value === "number" ? formatInteger(value) : "n/a";
|
|
432
|
+
}
|
|
433
|
+
function tokenDelta(a, b, key) {
|
|
434
|
+
const pick = (arm) => {
|
|
435
|
+
const usage = arm.trace?.usage;
|
|
436
|
+
return key === "total"
|
|
437
|
+
? usage?.totalTokens
|
|
438
|
+
: key === "input"
|
|
439
|
+
? usage?.inputTokens
|
|
440
|
+
: usage?.outputTokens;
|
|
441
|
+
};
|
|
442
|
+
const aValue = pick(a);
|
|
443
|
+
const bValue = pick(b);
|
|
444
|
+
if (typeof aValue !== "number" || typeof bValue !== "number") {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
return bValue - aValue;
|
|
448
|
+
}
|
|
449
|
+
function renderCompareMetricRow(label, aText, bText, delta, deltaText) {
|
|
450
|
+
return `<tr>
|
|
451
|
+
<th scope="row">${escapeHtml(label)}</th>
|
|
452
|
+
<td>${escapeHtml(aText)}</td>
|
|
453
|
+
<td>${escapeHtml(bText)}</td>
|
|
454
|
+
<td class="${deltaClass(delta)}">${escapeHtml(deltaText)}</td>
|
|
377
455
|
</tr>`;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
456
|
+
}
|
|
457
|
+
function renderCompareMetrics(result) {
|
|
458
|
+
const compare = result.compare;
|
|
459
|
+
if (!compare) {
|
|
460
|
+
return "";
|
|
461
|
+
}
|
|
462
|
+
const aMs = armDurationMs(compare.a);
|
|
463
|
+
const bMs = armDurationMs(compare.b);
|
|
464
|
+
const durationDelta = aMs !== undefined && bMs !== undefined ? bMs - aMs : undefined;
|
|
465
|
+
const aTools = armToolCount(compare.a);
|
|
466
|
+
const bTools = armToolCount(compare.b);
|
|
467
|
+
const toolDelta = aTools !== undefined && bTools !== undefined ? bTools - aTools : undefined;
|
|
468
|
+
const totalDelta = tokenDelta(compare.a, compare.b, "total");
|
|
386
469
|
return `
|
|
387
470
|
<section class="compare">
|
|
388
471
|
<header class="compare-header">
|
|
389
|
-
<
|
|
390
|
-
<p class="muted">${escapeHtml(
|
|
472
|
+
<h3>Comparison</h3>
|
|
473
|
+
<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
474
|
</header>
|
|
392
|
-
<div class="compare-summary meta-grid">
|
|
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}
|
|
399
475
|
<div class="compare-table-wrap">
|
|
400
476
|
<table class="compare-table">
|
|
401
477
|
<thead>
|
|
402
478
|
<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>
|
|
479
|
+
<th>Metric</th>
|
|
480
|
+
<th>${escapeHtml(compare.a.label)}</th>
|
|
481
|
+
<th>${escapeHtml(compare.b.label)}</th>
|
|
482
|
+
<th>Δ</th>
|
|
411
483
|
</tr>
|
|
412
484
|
</thead>
|
|
413
485
|
<tbody>
|
|
414
|
-
${
|
|
486
|
+
${renderCompareMetricRow("Duration", formatArmDuration(compare.a), formatArmDuration(compare.b), durationDelta, formatSignedDuration(durationDelta))}
|
|
487
|
+
${renderCompareMetricRow("Tokens", formatArmTokens(compare.a, "total"), formatArmTokens(compare.b, "total"), totalDelta, formatSigned(totalDelta))}
|
|
488
|
+
${renderCompareMetricRow("In", formatArmTokens(compare.a, "input"), formatArmTokens(compare.b, "input"), tokenDelta(compare.a, compare.b, "input"), formatSigned(tokenDelta(compare.a, compare.b, "input")))}
|
|
489
|
+
${renderCompareMetricRow("Out", formatArmTokens(compare.a, "output"), formatArmTokens(compare.b, "output"), tokenDelta(compare.a, compare.b, "output"), formatSigned(tokenDelta(compare.a, compare.b, "output")))}
|
|
490
|
+
${renderCompareMetricRow("Tools", aTools === undefined ? "n/a" : formatInteger(aTools), bTools === undefined ? "n/a" : formatInteger(bTools), toolDelta, formatSigned(toolDelta))}
|
|
415
491
|
</tbody>
|
|
416
492
|
</table>
|
|
417
493
|
</div>
|
|
418
494
|
</section>`;
|
|
419
495
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
</header>
|
|
437
|
-
<div class="summary">
|
|
438
|
-
<div class="stats">
|
|
439
|
-
<span class="stat"><strong>${report.summary.pairedCount}</strong> paired</span>
|
|
440
|
-
<span class="stat stat-fail"><strong>${report.summary.passRegressions}</strong> regressions</span>
|
|
441
|
-
<span class="stat stat-pass"><strong>${report.summary.passImprovements}</strong> improvements</span>
|
|
442
|
-
</div>
|
|
443
|
-
<dl>
|
|
444
|
-
<dt>Generated</dt><dd>${escapeHtml(generatedAt.toISOString())}</dd>
|
|
445
|
-
<dt>A</dt><dd>${escapeHtml(report.aSuite)}</dd>
|
|
446
|
-
<dt>B</dt><dd>${escapeHtml(report.bSuite)}</dd>
|
|
447
|
-
</dl>
|
|
448
|
-
</div>
|
|
449
|
-
${renderCompareHtmlSection(report)}
|
|
450
|
-
</main>
|
|
451
|
-
</body>
|
|
452
|
-
</html>
|
|
453
|
-
`;
|
|
496
|
+
function renderStoryList(title, lines) {
|
|
497
|
+
if (!lines || lines.length === 0) {
|
|
498
|
+
return "";
|
|
499
|
+
}
|
|
500
|
+
const items = lines.map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
501
|
+
return `<section class="story-block"><h3>${escapeHtml(title)}</h3><ul class="story-list">${items}</ul></section>`;
|
|
502
|
+
}
|
|
503
|
+
function renderStory(result) {
|
|
504
|
+
const story = result.story;
|
|
505
|
+
if (!story) {
|
|
506
|
+
return "";
|
|
507
|
+
}
|
|
508
|
+
return `<div class="story">
|
|
509
|
+
${renderStoryList("Criteria", story.criteria)}
|
|
510
|
+
${renderStoryList("Result", [...story.result, ...story.verdict])}
|
|
511
|
+
</div>`;
|
|
454
512
|
}
|
|
455
513
|
function renderScenario(result) {
|
|
456
514
|
const open = result.passed || result.skipped ? "" : " open";
|
|
457
|
-
const failures = renderFailures(result);
|
|
515
|
+
const failures = result.story ? "" : renderFailures(result);
|
|
458
516
|
const judgeVerdicts = renderJudgeVerdicts(result);
|
|
459
517
|
const tokens = formatTokensBadge(result);
|
|
460
518
|
const usageDetail = renderUsageDetail(usageOf(result));
|
|
461
519
|
const traceMeta = renderTraceMeta(result);
|
|
462
|
-
const
|
|
520
|
+
const story = renderStory(result);
|
|
521
|
+
const diagnostics = story || failures || judgeVerdicts
|
|
463
522
|
? `<div class="diagnostics">
|
|
523
|
+
${story}
|
|
464
524
|
${failures ? `<section><h3>What went wrong</h3>${failures}</section>` : ""}
|
|
465
525
|
${judgeVerdicts ? `<section><h3>Judge verdict</h3>${judgeVerdicts}</section>` : ""}
|
|
466
526
|
</div>`
|
|
@@ -479,122 +539,142 @@ function renderScenario(result) {
|
|
|
479
539
|
<div class="scenario-body">
|
|
480
540
|
${diagnostics}
|
|
481
541
|
${metaRow}
|
|
542
|
+
${renderCompareMetrics(result)}
|
|
543
|
+
${result.compare
|
|
544
|
+
? `<section class="conversation">
|
|
545
|
+
<h3>Conversation · ${escapeHtml(result.compare.a.label)}</h3>
|
|
546
|
+
${renderChat(result.compare.a.trace, result.compare.a.prompt)}
|
|
547
|
+
</section>
|
|
482
548
|
<section class="conversation">
|
|
549
|
+
<h3>Conversation · ${escapeHtml(result.compare.b.label)}</h3>
|
|
550
|
+
${renderChat(result.compare.b.trace, result.compare.b.prompt)}
|
|
551
|
+
</section>`
|
|
552
|
+
: `<section class="conversation">
|
|
483
553
|
<h3>Conversation</h3>
|
|
484
|
-
${renderChat(result.trace)}
|
|
485
|
-
</section
|
|
554
|
+
${renderChat(result.trace, result.prompt)}
|
|
555
|
+
</section>`}
|
|
486
556
|
</div>
|
|
487
557
|
</details>`;
|
|
488
558
|
}
|
|
489
559
|
function renderSuite(report) {
|
|
490
560
|
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
561
|
return `
|
|
496
562
|
<section class="suite">
|
|
497
563
|
<header class="suite-header">
|
|
498
|
-
<div
|
|
499
|
-
|
|
500
|
-
<
|
|
501
|
-
${usageLine}
|
|
564
|
+
<div class="suite-title">
|
|
565
|
+
<h2>${escapeHtml(report.suite)}</h2>
|
|
566
|
+
<span class="host">${escapeHtml(report.host)}</span>
|
|
502
567
|
</div>
|
|
568
|
+
<p class="suite-counts"><span>${formatInteger(report.passed)} passed</span><span>${formatInteger(report.failed)} failed</span><span>${formatInteger(report.skipped)} skipped</span></p>
|
|
503
569
|
</header>
|
|
504
|
-
${usage ? renderUsageStatsBlock(usage, "Suite token usage") : ""}
|
|
505
570
|
${scenarios}
|
|
506
571
|
</section>`;
|
|
507
572
|
}
|
|
508
573
|
function sharedReportCss() {
|
|
509
574
|
return `
|
|
575
|
+
@layer reset, base, layout, components;
|
|
510
576
|
:root {
|
|
511
|
-
|
|
512
|
-
--
|
|
513
|
-
--panel
|
|
514
|
-
--
|
|
515
|
-
--
|
|
516
|
-
--
|
|
517
|
-
--
|
|
518
|
-
--
|
|
519
|
-
--
|
|
520
|
-
--
|
|
521
|
-
--
|
|
522
|
-
--
|
|
523
|
-
--
|
|
524
|
-
--tool
|
|
525
|
-
--
|
|
526
|
-
--regress: #f07178;
|
|
527
|
-
}
|
|
528
|
-
* { box-sizing: border-box; }
|
|
529
|
-
body {
|
|
530
|
-
margin: 0;
|
|
531
|
-
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
|
|
532
|
-
background: var(--bg);
|
|
533
|
-
color: var(--text);
|
|
534
|
-
line-height: 1.45;
|
|
535
|
-
}
|
|
536
|
-
main { max-width: 1180px; margin: 0 auto; padding: 1.25rem 1.25rem 3rem; }
|
|
537
|
-
h1 { font-size: 1.45rem; margin: 0; letter-spacing: -0.02em; }
|
|
538
|
-
h2 { font-size: 1.1rem; margin: 0; }
|
|
539
|
-
h3 { font-size: 0.78rem; margin: 0 0 0.5rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.07em; }
|
|
540
|
-
h4 { font-size: 0.72rem; margin: 0.9rem 0 0.35rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
541
|
-
.muted { color: var(--muted); }
|
|
542
|
-
.empty { color: var(--muted); font-size: 0.85rem; font-style: italic; margin: 0; }
|
|
543
|
-
.empty.note { margin-bottom: 0.5rem; }
|
|
544
|
-
.report-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.65rem; }
|
|
545
|
-
.report-kicker { color: var(--muted); font-size: 0.78rem; }
|
|
546
|
-
.summary {
|
|
547
|
-
background: var(--panel);
|
|
548
|
-
border: 1px solid var(--border);
|
|
549
|
-
border-radius: 10px;
|
|
550
|
-
padding: 0.7rem 0.9rem;
|
|
551
|
-
display: flex;
|
|
552
|
-
align-items: center;
|
|
553
|
-
justify-content: space-between;
|
|
554
|
-
gap: 1rem;
|
|
577
|
+
color-scheme: dark;
|
|
578
|
+
--bg: oklch(0.18 0.02 250);
|
|
579
|
+
--panel: oklch(0.23 0.025 250);
|
|
580
|
+
--panel-2: oklch(0.2 0.022 250);
|
|
581
|
+
--text: oklch(0.93 0.015 250);
|
|
582
|
+
--muted: oklch(0.72 0.03 250);
|
|
583
|
+
--border: oklch(0.32 0.03 250);
|
|
584
|
+
--pass: oklch(0.8 0.16 155);
|
|
585
|
+
--fail: oklch(0.72 0.16 20);
|
|
586
|
+
--skip: oklch(0.84 0.14 85);
|
|
587
|
+
--user-bubble: oklch(0.3 0.06 250);
|
|
588
|
+
--assistant-bubble: oklch(0.26 0.04 155);
|
|
589
|
+
--system-bubble: oklch(0.26 0.04 300);
|
|
590
|
+
--tool: oklch(0.82 0.12 80);
|
|
591
|
+
--tool-bubble: oklch(0.26 0.04 80);
|
|
555
592
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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;
|
|
593
|
+
@layer reset {
|
|
594
|
+
* { box-sizing: border-box; }
|
|
595
|
+
body { margin: 0; }
|
|
596
|
+
h1, h2, h3, h4, p, ol { margin: 0; }
|
|
597
|
+
dl, dd { margin: 0; }
|
|
569
598
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
margin: 0 0 0.7rem;
|
|
599
|
+
@layer base {
|
|
600
|
+
body {
|
|
601
|
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
602
|
+
background: var(--bg);
|
|
603
|
+
color: var(--text);
|
|
604
|
+
line-height: 1.5;
|
|
605
|
+
word-spacing: 0.16em;
|
|
606
|
+
}
|
|
607
|
+
main { max-width: 68rem; margin-inline: auto; padding: 1.5rem 1.25rem 3rem; }
|
|
608
|
+
h1 { font-size: 1.7rem; font-weight: 650; letter-spacing: -0.02em; }
|
|
609
|
+
h2 { font-size: 1.05rem; font-weight: 650; }
|
|
610
|
+
h3 { font-size: 0.8rem; color: var(--muted); font-weight: 650; }
|
|
611
|
+
h4 { font-size: 0.75rem; margin-block: 0.9rem 0.35rem; color: var(--muted); font-weight: 650; }
|
|
612
|
+
.muted { color: var(--muted); }
|
|
613
|
+
.empty { color: var(--muted); font-size: 0.85rem; font-style: italic; }
|
|
614
|
+
.empty.note { margin-bottom: 0.5rem; }
|
|
587
615
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
616
|
+
@layer layout {
|
|
617
|
+
.report-header { display: grid; gap: 0.25rem; margin-bottom: 1rem; }
|
|
618
|
+
.brand { color: var(--muted); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
619
|
+
.lede, .when { color: var(--muted); font-size: 0.85rem; }
|
|
620
|
+
.summary, .cost, .guide, .compare {
|
|
621
|
+
background: var(--panel);
|
|
622
|
+
border: 1px solid var(--border);
|
|
623
|
+
border-radius: 12px;
|
|
624
|
+
padding: 0.9rem 1rem;
|
|
625
|
+
}
|
|
626
|
+
.summary { display: grid; gap: 0.65rem; }
|
|
627
|
+
.stats { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
628
|
+
.suite { margin-top: 1.75rem; display: grid; gap: 0.55rem; }
|
|
629
|
+
.suite-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; }
|
|
630
|
+
.suite-title { display: flex; align-items: center; gap: 0.5rem; }
|
|
631
|
+
.cost { margin-top: 0.85rem; display: grid; gap: 0.65rem; }
|
|
632
|
+
.cost-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.75rem; }
|
|
633
|
+
.guide { margin-top: 0.85rem; }
|
|
634
|
+
.diagnostics { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 0.7rem; margin-bottom: 0.9rem; }
|
|
635
|
+
.meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.45rem 0.75rem; }
|
|
593
636
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
637
|
+
@layer components {
|
|
638
|
+
.stat {
|
|
639
|
+
display: inline-flex;
|
|
640
|
+
align-items: baseline;
|
|
641
|
+
gap: 0.35rem;
|
|
642
|
+
padding: 0.4rem 0.65rem;
|
|
643
|
+
border-radius: 8px;
|
|
644
|
+
background: var(--panel-2);
|
|
645
|
+
font-size: 0.85rem;
|
|
646
|
+
}
|
|
647
|
+
.stat strong { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
|
|
648
|
+
.stat-pass strong { color: var(--pass); }
|
|
649
|
+
.stat-fail strong { color: var(--fail); }
|
|
650
|
+
.stat-skip strong { color: var(--skip); }
|
|
651
|
+
.guide h2 { font-size: 0.95rem; }
|
|
652
|
+
.guide ol { margin: 0.65rem 0 0; padding-inline-start: 1.2rem; display: grid; gap: 0.35rem; color: var(--muted); font-size: 0.88rem; }
|
|
653
|
+
.cost-lede { color: var(--muted); font-size: 0.85rem; }
|
|
654
|
+
.cost-item { display: grid; gap: 0.15rem; }
|
|
655
|
+
.cost-value { font-size: 1.2rem; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
656
|
+
.cost-label { font-size: 0.8rem; font-weight: 650; }
|
|
657
|
+
.cost-detail { color: var(--muted); font-size: 0.75rem; }
|
|
658
|
+
.host {
|
|
659
|
+
color: var(--muted);
|
|
660
|
+
font-size: 0.72rem;
|
|
661
|
+
background: var(--panel-2);
|
|
662
|
+
border: 1px solid var(--border);
|
|
663
|
+
border-radius: 999px;
|
|
664
|
+
padding: 0.1rem 0.5rem;
|
|
665
|
+
}
|
|
666
|
+
.suite-counts { display: flex; flex-wrap: wrap; gap: 0.75rem; color: var(--muted); font-size: 0.85rem; }
|
|
667
|
+
.tokens { color: var(--muted); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
|
|
668
|
+
.usage-summary, .scenario-meta {
|
|
669
|
+
background: var(--panel-2);
|
|
670
|
+
border: 1px solid var(--border);
|
|
671
|
+
border-radius: 8px;
|
|
672
|
+
padding: 0.65rem 0.7rem;
|
|
673
|
+
}
|
|
674
|
+
.meta-item { display: grid; gap: 0.1rem; }
|
|
675
|
+
.meta-key { color: var(--muted); font-size: 0.75rem; }
|
|
676
|
+
.meta-val { font-size: 0.88rem; font-variant-numeric: tabular-nums; word-break: break-word; }
|
|
677
|
+
.meta-row { margin-bottom: 0.7rem; }
|
|
598
678
|
details.scenario {
|
|
599
679
|
background: var(--panel);
|
|
600
680
|
border: 1px solid var(--border);
|
|
@@ -633,6 +713,9 @@ function sharedReportCss() {
|
|
|
633
713
|
.diagnostics section { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.65rem 0.7rem; min-width: 0; }
|
|
634
714
|
.conversation { min-width: 0; }
|
|
635
715
|
|
|
716
|
+
.story { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.85rem; margin-bottom: 0.85rem; }
|
|
717
|
+
.story-block h3 { margin: 0 0 0.35rem; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
|
|
718
|
+
.story-list { margin: 0; padding-left: 1.1rem; display: flex; flex-direction: column; gap: 0.25rem; color: var(--text); }
|
|
636
719
|
.failures { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
|
|
637
720
|
.failures li { border-left: 3px solid var(--fail); padding-left: 0.6rem; }
|
|
638
721
|
.failure-label { margin: 0; font-weight: 600; font-size: 0.85rem; color: var(--fail); }
|
|
@@ -709,8 +792,7 @@ function sharedReportCss() {
|
|
|
709
792
|
padding: 0.85rem 0.9rem 1rem;
|
|
710
793
|
}
|
|
711
794
|
.compare-header { margin-bottom: 0.65rem; }
|
|
712
|
-
.compare-header
|
|
713
|
-
.compare-summary { margin-bottom: 0.75rem; }
|
|
795
|
+
.compare-header h3 { margin-bottom: 0.2rem; }
|
|
714
796
|
.compare-table-wrap { overflow-x: auto; }
|
|
715
797
|
.compare-table {
|
|
716
798
|
width: 100%;
|
|
@@ -725,21 +807,17 @@ function sharedReportCss() {
|
|
|
725
807
|
vertical-align: middle;
|
|
726
808
|
}
|
|
727
809
|
.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
810
|
.delta-up { color: var(--fail); }
|
|
732
811
|
.delta-down { color: var(--pass); }
|
|
733
812
|
.delta-flat { color: var(--muted); }
|
|
734
813
|
|
|
735
814
|
@media (max-width: 720px) {
|
|
736
815
|
main { padding: 0.75rem; }
|
|
737
|
-
.summary { align-items: flex-start; flex-direction: column; }
|
|
738
|
-
.summary dl { justify-content: flex-start; }
|
|
739
816
|
.suite-header { align-items: flex-start; flex-direction: column; gap: 0.2rem; }
|
|
740
817
|
.diagnostics { grid-template-columns: 1fr; }
|
|
741
818
|
.bubble, .tool-card { max-width: 92%; }
|
|
742
819
|
}
|
|
820
|
+
}
|
|
743
821
|
`;
|
|
744
822
|
}
|
|
745
823
|
/** Build a self-contained HTML report for a completed suite run. */
|
|
@@ -748,22 +826,11 @@ export function renderHtmlReport(reports, meta = {}) {
|
|
|
748
826
|
const totalPassed = reports.reduce((sum, report) => sum + report.passed, 0);
|
|
749
827
|
const totalFailed = reports.reduce((sum, report) => sum + report.failed, 0);
|
|
750
828
|
const totalSkipped = reports.reduce((sum, report) => sum + report.skipped, 0);
|
|
751
|
-
const
|
|
829
|
+
const hostNames = [...new Set(reports.map((report) => report.host).filter(Boolean))];
|
|
830
|
+
const host = meta.host ?? (hostNames.length > 0 ? hostNames.join(", ") : "unknown");
|
|
752
831
|
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
832
|
const suitesHtml = reports.map(renderSuite).join("\n");
|
|
833
|
+
const suiteWord = reports.length === 1 ? "suite" : "suites";
|
|
767
834
|
return `<!DOCTYPE html>
|
|
768
835
|
<html lang="en">
|
|
769
836
|
<head>
|
|
@@ -775,32 +842,28 @@ export function renderHtmlReport(reports, meta = {}) {
|
|
|
775
842
|
<body>
|
|
776
843
|
<main>
|
|
777
844
|
<header class="report-header">
|
|
778
|
-
<
|
|
779
|
-
<
|
|
845
|
+
<p class="brand">agent-test</p>
|
|
846
|
+
<h1>Run report</h1>
|
|
847
|
+
<p class="lede">${escapeHtml(String(host))} · ${formatInteger(reports.length)} ${suiteWord}${meta.suitesDir ? ` · ${escapeHtml(meta.suitesDir)}` : ""}</p>
|
|
780
848
|
</header>
|
|
781
|
-
<
|
|
849
|
+
<section class="summary" aria-label="Run verdict">
|
|
782
850
|
<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
|
-
: ""}
|
|
851
|
+
<span class="stat stat-pass"><strong>${formatInteger(totalPassed)}</strong><span>passed</span></span>
|
|
852
|
+
<span class="stat stat-fail"><strong>${formatInteger(totalFailed)}</strong><span>failed</span></span>
|
|
853
|
+
<span class="stat stat-skip"><strong>${formatInteger(totalSkipped)}</strong><span>skipped</span></span>
|
|
789
854
|
</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}
|
|
855
|
+
<p class="when">Generated ${escapeHtml(generatedAt.toISOString())}</p>
|
|
856
|
+
</section>
|
|
857
|
+
<section class="guide" aria-labelledby="guide-heading">
|
|
858
|
+
<h2 id="guide-heading">How to read this report</h2>
|
|
859
|
+
<ol>
|
|
860
|
+
<li>The verdict is pass or fail. Tokens are cost, not the score.</li>
|
|
861
|
+
<li>Open a scenario for the criteria and the result. A compare scenario shows a time and token table, then two conversations.</li>
|
|
862
|
+
<li>Typical is the middle scenario cost. Largest is the heaviest scenario.</li>
|
|
863
|
+
<li>In is prompt and context. Out is generated text.</li>
|
|
864
|
+
</ol>
|
|
865
|
+
</section>
|
|
866
|
+
${renderCostSection(runUsage)}
|
|
804
867
|
${suitesHtml}
|
|
805
868
|
</main>
|
|
806
869
|
</body>
|