Files
PaperClipAI/tests/runner-e2e/report.test.ts
T
DottaandPaperclip 2b16262280 Improve runner E2E report navigation and local previews (#16996)
## Thinking Path

> - Paperclip is the open source app that people use to manage AI agents
for work.
> - Product E2E campaigns check user journeys across agent profiles and
environments.
> - Readers use the campaign history and reports to inspect results and
retained evidence.
> - The reports showed image summaries, many unselected suites, and a
long sticky index.
> - Individual case cards also showed routine diagnostics before the
visual evidence.
> - This pull request adds text summaries, feature filters, folding case
indexes, and simpler cards.
> - A local preview uses retained public data so designers can change
reports without another paid campaign.

## Linked Issues or Issue Description

**Current behavior**

The report is hard to scan as the journey catalog grows. Suite links can
point into hidden results. Case indexes do not show whether a case
passed across its configurations. Design changes also need a realistic
local preview.

**Proposed behavior**

Show the selected campaign as text. Link journeys to current feature-map
capabilities. Add folding case indexes with status indicators, reliable
section links, and a back-to-top control. Keep failed outcomes visible
and put routine diagnostics under Test details. Render retained public
campaign data with local templates.

**Reason and benefit**

Readers can find a feature or failed case faster. Designers can iterate
against real retained data without starting an application instance or
using provider credentials.

**Breaking changes**

No result schema or grading change. The report stops embedding the
generated summary PNG. The publisher still produces that artifact for
existing consumers.

Related work: #12889 added the existing matrix browser. #12846 concerns
the public image publication boundary. This PR changes report
presentation and local preview behavior.

## What Changed

- Replace image summaries with shared HTML suite summaries on the
history and campaign pages.
- Show selected executions and preserve missing results for selected
cells.
- Add explicit journey-to-capability associations, feature filters, and
a coverage view with unmapped journeys.
- Add folding case indexes, section case lists, and red, green, or amber
status indicators across matching configurations.
- Fix anchor navigation through filters and add a floating back-to-top
control.
- Simplify case cards. Keep failure reasons visible. Fold routine
checks, billing, and context into Test details.
- Add an original-image link to the gallery.
- Add a local preview utility with public data caching and offline mode.
Document the direct Node command.
- Test manifest-listed evidence, rejection of unlisted files and encoded
traversal, cache reuse, and offline misses with a real loopback server
and a stub downloader.

## Verification

- Focused report, history, and preview tests: 75 passed in 6 files.
- Report Playwright spec: 4 passed in Chrome with a standalone config
that does not start the application server.
- `node scripts/check-maps.mjs`: passed.
- `git diff --check`: passed.
- Manually checked retained campaign reports, feature filters, folding
indexes, failure links, card details, gallery links, mobile layout, and
keyboard navigation.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- `pnpm test:e2e:runner:typecheck`: passed.
- `pnpm test:run`: stopped after 15 native session database cases
failed. The local Postgres package first lacked dynamic-library links.
After link hydration, the same cases still failed. A direct `initdb`
check then reported `could not create semaphores: No space left on
device` and identified the macOS semaphore limit. The complete local
suite has not passed.
- `pnpm test:e2e:runner:unit`: 3,250 passed, 6 failed, 2 skipped. All
six failures reproduced against unchanged `origin/master` report sources
with the same installed dependencies. They concern Copilot admission and
its Daytona image input, plus the launcher test that runs those
assertions. No report or history test failed.
- No live provider evaluation was run for this presentation change.
- CI on `b1a9c06823` first failed browser shard 3/8. The agent-chat
split-handoff test reached cleanup, where cancelling a run returned HTTP
500 from a Postgres deadlock. Cleanup did not restore the experiment
setting, and a later test failed its initial setting check. This shard
does not contain the report spec. One targeted rerun passed, and that
full workflow then passed. This shows recovery; no production fix was
made for the deadlock. The later preview-test commit requires its own
fresh checks.

To review the design, run `node --watch --import
./cli/node_modules/tsx/dist/loader.mjs tests/runner-e2e/preview.ts`.
Open the history address printed by the command, then open a campaign.
The README covers offline mode and cache refresh.

## Risks

- Feature associations use the current map. They describe linked
scenarios, not proof that a whole capability is covered. Historical
grades remain unchanged.
- New or unfamiliar cases remain unmapped until their assertions are
inspected.
- The preview needs the public history host on first access. Offline
mode requires cached files. It binds to the loopback interface and
serves manifest-listed public evidence.
- Hiding routine diagnostics adds one disclosure step. Failed or
incomplete outcomes remain visible.

This is report polish. It does not duplicate planned core work in
`ROADMAP.md`.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, tool use, code execution,
and browser automation. The exact serving model ID and context window
are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-20 14:02:19 -05:00

635 lines
27 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { STARTUP_REUSE_LAYERS, STARTUP_REUSE_SCHEMA, summarizeStartupReuse } from "../../packages/adapter-utils/src/startup-reuse.js";
import type { RunnerE2EResult } from "./types.js";
const execFileAsync = promisify(execFile);
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
const cleanupDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
cleanupDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
describe("runner E2E report aggregation", () => {
it("retains full reuse and a workspace-only invalidation in the generated report", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-reuse-report-"));
cleanupDirectories.push(root);
const executionId = "core-compatibility.runner-codex.local.message-marker";
const directory = path.join(root, "attempt-1");
await mkdir(directory);
const startupReuse = [false, true].map(invalidate => ({
runId: invalidate ? "workspace-invalidated" : "fully-reused",
...summarizeStartupReuse(STARTUP_REUSE_LAYERS.map(layer => ({
eventType: "run.startup.reuse",
payload: { schema: STARTUP_REUSE_SCHEMA, layer,
state: invalidate && layer === "workspace" ? "fresh" : "reused",
reason: invalidate && layer === "workspace" ? "workspace_changed" : "verified_reuse",
...(layer === "provider_process" ? { providerLaunchCount: 0 } : {}) },
}))),
}));
const result: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v2", executionId, suiteId: "core-compatibility",
attempt: 1, status: "passed", profileId: "runner-codex", environmentId: "local",
caseId: "message-marker", provider: "codex", model: "fixture-model", runtimeMode: "native",
startedAt: "2026-09-23T00:00:00Z", finishedAt: "2026-09-23T00:00:01Z", durationMs: 1000,
cleanup: "passed", runIds: startupReuse.map(reuse => reuse.runId), startupReuse,
};
await writeFile(path.join(directory, "result.json"), JSON.stringify(result));
await writeFile(path.join(directory, "final-state.png"), "fake-png");
await writeFile(path.join(directory, "evidence-manifest.json"), JSON.stringify({ files: ["final-state.png"], leaks: [], missing: [] }));
const output = path.join(root, "merged");
await execFileAsync(process.execPath, [path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"), path.join(repositoryRoot, "tests/runner-e2e/report.ts")], {
cwd: repositoryRoot,
env: { ...process.env, PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root, PAPERCLIP_RUNNER_E2E_REPORT_OUT: output, PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]) },
});
const normalized = JSON.parse(await readFile(path.join(output, "normalized-results.json"), "utf8"));
expect(normalized.results[0].startupReuse).toEqual(startupReuse);
expect(startupReuse.map(reuse => reuse.warmth)).toEqual(["warm", "partial_reuse"]);
const dashboard = await readFile(path.join(output, "index.html"), "utf8");
expect(dashboard).toContain("<td>warm</td>");
expect(dashboard).toContain("<td>partial_reuse</td>");
expect(dashboard).toContain("fresh (workspace_changed)");
});
it.each([
{ name: "missing", usage: null, runIds: ["run-1"], tokens: "Unavailable", cost: "Unavailable", htmlTokens: "Unavailable" },
{ name: "partial", usage: { runs: [{ usage: { inputTokens: 1250, outputTokens: 75, cachedInputTokens: 500, costUsd: 0.0125 } }, { usage: null }] }, runIds: ["run-1", "run-2"], tokens: "1250 input / 75 output / 500 cached (partial: 1/2 runs)", cost: "$0.012500 (partial: 1/2 runs)", htmlTokens: "1,250 (partial: 1/2 runs)" },
{ name: "reported", usage: { inputTokens: 1250, outputTokens: 75, cachedInputTokens: 500, costUsd: 0.0125 }, runIds: ["run-1"], tokens: "1250 input / 75 output / 500 cached", cost: "$0.012500", htmlTokens: "1,250" },
{ name: "reported zero cost", usage: { inputTokens: 1, outputTokens: 0, costUsd: 0 }, runIds: ["run-1"], tokens: "1 input / 0 output / 0 cached", cost: "$0.000000", htmlTokens: "1" },
])("renders $name usage with its actual coverage", async ({ usage, runIds, tokens, cost, htmlTokens }) => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-billing-report-"));
cleanupDirectories.push(root);
const executionId = "core-compatibility.runner-codex.local.message-marker";
const directory = path.join(root, "attempt-1");
await mkdir(directory);
const result: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v2", executionId, suiteId: "core-compatibility",
attempt: 1, status: "passed", profileId: "runner-codex", environmentId: "local",
caseId: "message-marker", provider: "codex", model: "fixture-model", runtimeMode: "native",
startedAt: "2026-09-23T00:00:00Z", finishedAt: "2026-09-23T00:00:01Z", durationMs: 1000,
cleanup: "passed", runIds, usage,
};
const raw = JSON.stringify(result);
await writeFile(path.join(directory, "result.json"), raw);
await writeFile(path.join(directory, "final-state.png"), "fake-png");
await writeFile(path.join(directory, "evidence-manifest.json"), JSON.stringify({ files: ["final-state.png"], leaks: [], missing: [] }));
const output = path.join(root, "merged");
await execFileAsync(process.execPath, [path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"), path.join(repositoryRoot, "tests/runner-e2e/report.ts")], {
cwd: repositoryRoot,
env: { ...process.env, PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root, PAPERCLIP_RUNNER_E2E_REPORT_OUT: output, PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]) },
});
const markdown = await readFile(path.join(output, "summary.md"), "utf8");
expect(markdown.split("\n").find((line) => line.startsWith("Tokens: "))).toBe(`Tokens: ${tokens}`);
expect(markdown.split("\n").find((line) => line.startsWith("Provider-reported LLM cost: "))).toBe(`Provider-reported LLM cost: ${cost}`);
const dashboard = await readFile(path.join(output, "index.html"), "utf8");
expect(dashboard).toContain(`<strong>${htmlTokens}</strong><span>Input tokens</span>`);
if (usage === null) {
expect(markdown).not.toContain("0/0 | $0.000000");
expect(dashboard).toContain("<strong>Unavailable</strong><span>LLM reported subtotal</span>");
expect(dashboard).toContain("<span>Known spend</span><strong>Unavailable</strong>");
}
expect(await readFile(path.join(directory, "result.json"), "utf8")).toBe(raw);
const normalized = JSON.parse(await readFile(path.join(output, "normalized-results.json"), "utf8"));
expect(normalized.passed).toBe(1);
expect(normalized.results[0].usage).toEqual(usage);
expect(normalized.results[0].startupReuse).toHaveLength(runIds.length);
expect(normalized.results[0].startupReuse.every((reuse: { warmth: string; providerLaunchCount: number | null }) =>
reuse.warmth === "unknown" && reuse.providerLaunchCount === null)).toBe(true);
expect(dashboard).toContain("Startup reuse evidence");
});
it("keeps interrupted journeys incomplete unless their evidence is invalid", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-incomplete-report-"));
cleanupDirectories.push(root);
const ids: string[] = [];
for (const [caseId, validEvidence] of [["task-card-accept", true], ["task-reply-accept", false]] as const) {
const executionId = `first-task.runner-codex.local.${caseId}`;
ids.push(executionId);
const directory = path.join(root, caseId);
await mkdir(directory);
await writeFile(path.join(directory, "result.json"), JSON.stringify({
schema: "paperclip.runner-e2e.result/v2", executionId, suiteId: "first-task",
attempt: 1, status: "failed", failureClass: "candidate_failure", error: "Recording stopped before acceptance",
profileId: "runner-codex", environmentId: "local", caseId, provider: "codex", model: "fixture-model", runtimeMode: "native",
startedAt: "2026-09-15T00:00:00Z", finishedAt: "2026-09-15T00:01:00Z", durationMs: 60_000, cleanup: "passed",
firstTask: { caseId, nonce: "fixture", onboardingIssueId: "task", agentId: "agent", initialTaskIds: ["task"], instructions: [], configuredModel: null, observedModels: [], checkpoints: [],
checks: [{ id: "acceptance-recorded", passed: false, notReached: "No acceptance checkpoint", detail: "Acceptance recorded", evidence: [] }] },
} satisfies RunnerE2EResult));
if (validEvidence) await writeFile(path.join(directory, "evidence-manifest.json"), JSON.stringify({ files: [], leaks: [], missing: [] }));
}
const output = path.join(root, "merged");
await expect(execFileAsync(process.execPath, [path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"), path.join(repositoryRoot, "tests/runner-e2e/report.ts")], {
cwd: repositoryRoot, env: { ...process.env, PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root, PAPERCLIP_RUNNER_E2E_REPORT_OUT: output, PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify(ids) },
})).rejects.toBeDefined();
const normalized = JSON.parse(await readFile(path.join(output, "normalized-results.json"), "utf8"));
expect(normalized).toMatchObject({ passed: 0, failed: 1, incomplete: 1 });
expect(normalized.results[0]).toMatchObject({ evidenceValid: true, evidenceErrors: [] });
expect(normalized.results[1]).toMatchObject({ evidenceValid: false, failureClass: "permanent_infrastructure" });
const page = await readFile(path.join(output, "index.html"), "utf8");
expect(page).toContain("Incomplete journey");
expect(page).toContain("evidence manifest missing");
});
it("selects the latest retry and enforces cleanup and pass evidence", async () => {
const root = await mkdtemp(
path.join(os.tmpdir(), "runner-e2e-report-test-"),
);
cleanupDirectories.push(root);
const executionId = "legacy-codex.local.message-marker";
const base: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v1",
executionId,
attempt: 2,
status: "passed",
profileId: "legacy-codex",
environmentId: "local",
caseId: "message-marker",
provider: "codex",
model: "fixture-model",
runtimeMode: "legacy",
startedAt: "2026-08-26T00:00:00.000Z",
finishedAt: "2026-08-26T00:00:01.000Z",
durationMs: 1_000,
source: {
sha: "forged-result-sha",
ref: "refs/heads/forged-result",
workflowRunUrl: "https://example.test/actions/runs/forged",
},
runIds: ["run-2"],
turnTimings: [
{
turn: 1,
submittedAt: "2026-08-26T00:00:00.000Z",
runStartedAt: "2026-08-26T00:00:00.100Z",
runFinishedAt: "2026-08-26T00:00:01.000Z",
schedulerLatencyMs: 100,
runDurationMs: 900,
responseLatencyMs: 1_000,
runId: "run-2",
leaseAcquisitionOutcome: "created",
},
],
usage: {
inputTokens: 1_250,
outputTokens: 75,
cachedInputTokens: 500,
costUsd: 0.0125,
},
cleanup: "passed",
};
for (const attempt of [1, 2]) {
const directory = path.join(root, `attempt-${attempt}`);
await mkdir(directory, { recursive: true });
const result =
attempt === 1
? {
...base,
attempt,
status: "failed" as const,
failureClass: "transient_infrastructure" as const,
}
: {
...base,
matcherResults: [
{
matcher: {
kind: "message_contains" as const,
expected: "PAPERCLIP_E2E_OK",
},
passed: true,
detail: "matched",
},
],
screenshots: [
{
id: "final-state",
label: "Final visible task state",
file: "final-state.png",
},
],
};
await writeFile(
path.join(directory, "result.json"),
JSON.stringify(result),
);
if (attempt === 2) {
await writeFile(path.join(directory, "final-state.png"), "fake-png");
}
await writeFile(
path.join(directory, "evidence-manifest.json"),
JSON.stringify({
files: attempt === 2 ? ["final-state.png"] : [],
leaks: [],
missing: [],
}),
);
}
const staleDuplicate = path.join(root, "attempt-2-stale-duplicate");
await mkdir(staleDuplicate, { recursive: true });
await writeFile(
path.join(staleDuplicate, "result.json"),
JSON.stringify({
...base,
status: "failed",
failureClass: "candidate_failure",
finishedAt: "2026-08-26T00:00:00.500Z",
}),
);
await writeFile(
path.join(staleDuplicate, "evidence-manifest.json"),
JSON.stringify({ files: [], leaks: [], missing: [] }),
);
const output = path.join(root, "merged");
await execFileAsync(
process.execPath,
[
path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"),
path.join(repositoryRoot, "tests/runner-e2e/report.ts"),
],
{
cwd: repositoryRoot,
env: {
...process.env,
PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root,
PAPERCLIP_RUNNER_E2E_REPORT_OUT: output,
PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]),
PAPERCLIP_RUNNER_E2E_SOURCE_SHA:
"0123456789abcdef0123456789abcdef01234567",
PAPERCLIP_RUNNER_E2E_SOURCE_REF:
"refs/heads/fix/runner-paid-source-attribution",
GITHUB_SHA: "trusted-default-workflow-sha",
GITHUB_REF: "refs/heads/master",
GITHUB_SERVER_URL: "https://github.com",
GITHUB_REPOSITORY: "paperclipai/paperclip",
GITHUB_RUN_ID: "123456",
PAPERCLIP_RUNNER_E2E_HISTORY_PUBLIC_BASE_URL:
"https://reports.example.test/",
PAPERCLIP_RUNNER_E2E_HISTORY_PREFIX: "/runner-e2e/",
},
},
);
const normalized = JSON.parse(
await readFile(path.join(output, "normalized-results.json"), "utf8"),
);
expect(normalized).toMatchObject({
schema: "paperclip.runner-e2e.campaign/v2",
selected: 1,
executed: 1,
passed: 1,
failed: 0,
retries: 1,
cleanupPassed: true,
source: {
sha: "0123456789abcdef0123456789abcdef01234567",
ref: "refs/heads/fix/runner-paid-source-attribution",
workflowRunUrl:
"https://github.com/paperclipai/paperclip/actions/runs/123456",
},
});
expect(normalized.billing).toMatchObject({
reportedLlmCostUsd: 0.0125,
llm: {
inputTokens: 1_250,
outputTokens: 75,
runsWithReportedCost: 1,
},
});
expect(normalized.results[0]).toMatchObject({
attempt: 2,
evidenceValid: true,
source: {
sha: "0123456789abcdef0123456789abcdef01234567",
ref: "refs/heads/fix/runner-paid-source-attribution",
workflowRunUrl:
"https://github.com/paperclipai/paperclip/actions/runs/123456",
},
});
const dashboard = await readFile(
path.join(output, "dashboard.html"),
"utf8",
);
expect(dashboard).toContain("Runner Full-Stack E2E");
expect(dashboard).toContain(executionId);
expect(dashboard).toContain("case-passed");
expect(dashboard).not.toContain(
"core-compatibility.runner-acpx-codex.daytona.message-marker",
);
expect(dashboard).not.toContain('class="case case-not-selected"');
expect(dashboard).toContain("<img");
expect(dashboard).toContain('class="brand-lockup"');
expect(dashboard).toContain("data-gallery-dialog");
expect(dashboard).toContain(
`id="execution-core-compatibility.${executionId}"`,
);
expect(dashboard).toContain("data-gallery-previous");
expect(dashboard).toContain("data-gallery-next");
expect(dashboard).toContain("View gallery · 1");
expect(dashboard).toContain(
"Declared PNG screenshots and normalized results are retained with every published campaign",
);
expect(dashboard).toContain(
"Declared screenshots and normalized results published",
);
expect(dashboard).toContain("message_contains");
expect(dashboard).toContain("Test details");
expect(dashboard).toContain("Scheduler");
expect(dashboard).toContain("Run duration");
expect(dashboard).toContain("100ms");
expect(dashboard).toContain("Campaign billing summary");
expect(dashboard).toContain("LLM reported subtotal");
expect(dashboard).toContain("Agent execution time");
expect(dashboard).toContain("Daytona lease time");
expect(dashboard).toContain("1,250 in · 75 out");
expect(dashboard).toContain("$0.0125");
expect(dashboard).toContain("unpriced or unavailable runs are excluded");
expect(dashboard).toContain('class="profile-sticky"');
expect(dashboard).toContain('class="mobile-environment-header"');
expect(dashboard).toContain("data-gallery-profile=");
expect(dashboard).toContain("data-gallery-environment=");
expect(dashboard).toContain("data-gallery-duration=");
expect(dashboard).toContain("data-gallery-tokens=");
expect(dashboard).toContain("data-gallery-matchers=");
expect(dashboard).toContain("data-report-query");
expect(dashboard).toContain("data-report-profile");
expect(dashboard).toContain("data-report-environment");
expect(dashboard).toContain("data-report-status");
expect(dashboard).not.toContain('class="suite-nav"');
expect(dashboard).toContain("data-report-feature");
expect(dashboard).toContain("Feature coverage");
expect(dashboard).not.toContain(".report-filters { position: sticky");
expect(dashboard).toContain("table-layout: fixed");
expect(dashboard).toContain('class="profile-column"');
expect(dashboard).toContain('aria-label="Previous"');
expect(dashboard).toContain('aria-label="Next"');
expect(dashboard).not.toContain("overflow: auto; max-height: calc(100vh");
expect(dashboard).toContain("@media (max-width: 1180px)");
expect(
await readFile(path.join(output, "assets", "favicon-32x32.png")),
).not.toHaveLength(0);
expect(
await readFile(path.join(output, "assets", "InterVariable.woff2")),
).not.toHaveLength(0);
expect(
await readFile(
path.join(
output,
"evidence",
`core-compatibility.${executionId}`,
"attempt-2",
"final-state.png",
),
"utf8",
),
).toBe("fake-png");
expect(await readFile(path.join(output, "index.html"), "utf8")).toBe(
dashboard,
);
const summary = await readFile(path.join(output, "summary.md"), "utf8");
expect(summary).toContain("## View results");
expect(summary).toContain(
"[Open the exact interactive campaign report](https://reports.example.test/runner-e2e/campaigns/gha-123456-1/index.html)",
);
expect(summary).toContain(
"[Open the workflow run and per-cell job logs](https://github.com/paperclipai/paperclip/actions/runs/123456)",
);
expect(summary).toContain(
"[Download the merged report and per-cell evidence](https://github.com/paperclipai/paperclip/actions/runs/123456#artifacts)",
);
expect(summary).toContain(
`[core-compatibility.${executionId}](https://reports.example.test/runner-e2e/campaigns/gha-123456-1/index.html#execution-core-compatibility.${executionId})`,
);
});
it("prefers a valid rerun over a higher attempt number from an older campaign", async () => {
const root = await mkdtemp(
path.join(os.tmpdir(), "runner-e2e-report-rerun-test-"),
);
cleanupDirectories.push(root);
const executionId = "runner-opencode.local.ask-question";
const common: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v1",
executionId,
attempt: 2,
status: "failed",
failureClass: "candidate_failure",
profileId: "runner-opencode",
environmentId: "local",
caseId: "ask-question",
provider: "opencode",
model: "fixture-model",
runtimeMode: "native",
startedAt: "2026-08-26T00:00:00.000Z",
finishedAt: "2026-08-26T00:00:01.000Z",
durationMs: 1_000,
cleanup: "not_started",
};
const failedDirectory = path.join(root, "old-campaign", "attempt-2");
const passedDirectory = path.join(root, "new-campaign", "attempt-1");
await mkdir(failedDirectory, { recursive: true });
await mkdir(passedDirectory, { recursive: true });
await writeFile(
path.join(failedDirectory, "result.json"),
JSON.stringify(common),
);
await writeFile(
path.join(failedDirectory, "evidence-manifest.json"),
JSON.stringify({ files: [], leaks: [], missing: [] }),
);
await writeFile(
path.join(passedDirectory, "result.json"),
JSON.stringify({
...common,
attempt: 1,
status: "passed",
failureClass: undefined,
finishedAt: "2026-08-26T00:00:02.000Z",
cleanup: "passed",
}),
);
await writeFile(
path.join(passedDirectory, "evidence-manifest.json"),
JSON.stringify({
files: ["final-state.png"],
leaks: [],
missing: [],
}),
);
await writeFile(path.join(passedDirectory, "final-state.png"), "fake-png");
const output = path.join(root, "merged");
await execFileAsync(
process.execPath,
[
path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"),
path.join(repositoryRoot, "tests/runner-e2e/report.ts"),
],
{
cwd: repositoryRoot,
env: {
...process.env,
PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root,
PAPERCLIP_RUNNER_E2E_REPORT_OUT: output,
PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]),
},
},
);
const normalized = JSON.parse(
await readFile(path.join(output, "normalized-results.json"), "utf8"),
);
expect(normalized).toMatchObject({ passed: 1, failed: 0 });
expect(normalized.results[0]).toMatchObject({
attempt: 1,
status: "passed",
evidenceValid: true,
});
});
it("materializes declared screenshots from hashed Playwright attachments", async () => {
const root = await mkdtemp(
path.join(os.tmpdir(), "runner-e2e-report-screenshot-alias-")
);
cleanupDirectories.push(root);
const executionId = "daytona-warm-continuity.legacy-codex.daytona.warm-three-turn";
const directory = path.join(root, "attempt-1");
const attachment =
"playwright-output/warm-turn/attachments/warm-turn-1-deadbeef.png";
await mkdir(path.join(directory, path.dirname(attachment)), {
recursive: true,
});
await writeFile(path.join(directory, "final-state.png"), "final-png");
await writeFile(path.join(directory, attachment), "warm-turn-png");
await writeFile(
path.join(directory, "result.json"),
JSON.stringify({
schema: "paperclip.runner-e2e.result/v1",
executionId,
attempt: 1,
status: "passed",
profileId: "legacy-codex",
environmentId: "daytona",
caseId: "warm-three-turn",
provider: "codex",
model: "fixture-model",
runtimeMode: "legacy",
startedAt: "2026-08-26T00:00:00.000Z",
finishedAt: "2026-08-26T00:00:01.000Z",
durationMs: 1_000,
cleanup: "passed",
screenshots: [
{
id: "warm-turn-1",
label: "Warm Daytona turn 1 awaiting review",
file: "warm-turn-1.png",
},
{
id: "final-state",
label: "Final visible task state",
file: "final-state.png",
},
],
} satisfies RunnerE2EResult),
);
await writeFile(
path.join(directory, "evidence-manifest.json"),
JSON.stringify({
files: ["final-state.png", attachment],
leaks: [],
missing: [],
}),
);
const output = path.join(root, "merged");
await execFileAsync(
process.execPath,
[
path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"),
path.join(repositoryRoot, "tests/runner-e2e/report.ts"),
],
{
cwd: repositoryRoot,
env: {
...process.env,
PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root,
PAPERCLIP_RUNNER_E2E_REPORT_OUT: output,
PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]),
},
},
);
expect(
await readFile(
path.join(output, "evidence", executionId, "attempt-1", "warm-turn-1.png"),
"utf8",
),
).toBe("warm-turn-png");
});
it("constructs the public root JUnit from fixed markup and escaped fields", async () => {
const root = await mkdtemp(
path.join(os.tmpdir(), "runner-e2e-report-junit-test-"),
);
cleanupDirectories.push(root);
const executionId = "legacy-codex.local.message-marker";
const directory = path.join(root, "attempt-1");
await mkdir(directory, { recursive: true });
await writeFile(
path.join(directory, "result.json"),
JSON.stringify({
schema: "paperclip.runner-e2e.result/v1",
executionId,
attempt: 1,
status: "failed",
failureClass: "candidate_failure",
error: `provider said \"><script>alert(1)</script>&`,
profileId: "legacy-codex",
environmentId: "local",
caseId: "message-marker",
provider: "codex",
model: "fixture-model",
runtimeMode: "legacy",
startedAt: "2026-08-26T00:00:00.000Z",
finishedAt: "2026-08-26T00:00:01.000Z",
durationMs: 1_000,
cleanup: "passed",
} satisfies RunnerE2EResult),
);
await writeFile(
path.join(directory, "evidence-manifest.json"),
JSON.stringify({ files: [], leaks: [], missing: [] }),
);
const output = path.join(root, "merged");
await expect(
execFileAsync(
process.execPath,
[
path.join(repositoryRoot, "cli/node_modules/tsx/dist/cli.mjs"),
path.join(repositoryRoot, "tests/runner-e2e/report.ts"),
],
{
cwd: repositoryRoot,
env: {
...process.env,
PAPERCLIP_RUNNER_E2E_REPORT_ROOT: root,
PAPERCLIP_RUNNER_E2E_REPORT_OUT: output,
PAPERCLIP_RUNNER_E2E_EXPECTED_IDS: JSON.stringify([executionId]),
},
},
),
).rejects.toBeDefined();
const junit = await readFile(path.join(output, "junit.xml"), "utf8");
expect(junit).toContain(
`message="provider said &quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;&amp;"`,
);
expect(junit).not.toContain("<script>");
expect(junit).not.toContain("<?xml-stylesheet");
});
});