diff --git a/tests/runner-e2e/dashboard-regenerate.ts b/tests/runner-e2e/dashboard-regenerate.ts index 2913bd96459..41c9d2225ef 100644 --- a/tests/runner-e2e/dashboard-regenerate.ts +++ b/tests/runner-e2e/dashboard-regenerate.ts @@ -161,6 +161,12 @@ export async function regenerateRunnerDashboard(input: { }; }), ); + const summary = await readFile(path.join(bundle, "summary.md"), "utf8").then(text => text.replace( + /^(Feedback across retained attempts:.*?); [0-9]+ distinct items awaiting review;/m, "$1;", + )).catch((error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return undefined; + throw error; + }); const dashboard = renderRunnerE2EDashboard({ title: "Runner Full-Stack E2E", generatedAt: normalized.generatedAt, @@ -168,6 +174,7 @@ export async function regenerateRunnerDashboard(input: { catalog: runnerMatrix, entries, campaign, + summaryAvailable: summary !== undefined, history, }); const upgraded = { @@ -180,12 +187,6 @@ export async function regenerateRunnerDashboard(input: { evidenceErrors: normalized.results[index]?.evidenceErrors ?? [], })), }; - const summary = await readFile(path.join(bundle, "summary.md"), "utf8").then(text => text.replace( - /^(Feedback across retained attempts:.*?); [0-9]+ distinct items awaiting review;/m, "$1;", - )).catch((error: NodeJS.ErrnoException) => { - if (error.code === "ENOENT") return undefined; - throw error; - }); const reviewStatus = /^(?:[0-9]+ distinct feedback items await review\.|Feedback reviewed by (?:human|agent) at ).*$/m; const refreshedSummary = summary === undefined ? undefined : reviewStatus.test(summary) ? summary.replace(reviewStatus, () => campaignFeedbackReviewText(campaign)) diff --git a/tests/runner-e2e/dashboard-summary-link.test.ts b/tests/runner-e2e/dashboard-summary-link.test.ts new file mode 100644 index 00000000000..5be3b3c3088 --- /dev/null +++ b/tests/runner-e2e/dashboard-summary-link.test.ts @@ -0,0 +1,32 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { expect, it } from "vitest"; +import { renderRunnerE2EDashboard } from "./dashboard.js"; +import { regenerateRunnerDashboard } from "./dashboard-regenerate.js"; +import { buildRunnerCampaign } from "./history.js"; + +const campaign = () => buildRunnerCampaign({ campaignId: "summary-link-fixture", generatedAt: "2026-10-27T00:00:00Z", expected: [], results: [] }); +it("offers only the fixed relative summary link when a campaign summary is available", () => { + const input = { title: "Campaign", generatedAt: "2026-10-27T00:00:00Z", expected: [], catalog: [], entries: [], campaign: campaign() }; + expect(renderRunnerE2EDashboard({ ...input, summaryAvailable: true })).toContain('Campaign summary ↗'); + expect(renderRunnerE2EDashboard(input)).not.toContain('href="summary.md"'); + expect(renderRunnerE2EDashboard({ ...input, campaign: undefined, summaryAvailable: true })).not.toContain('href="summary.md"'); +}); +it("regeneration derives link availability from the actual summary file without changing result identities", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "runner-summary-link-")); + try { + const before = campaign(); before.source.workflowRunUrl = "https://github.com/paperclipai/paperclip/actions/runs/123"; + await writeFile(path.join(root, "normalized-results.json"), JSON.stringify(before)); + await writeFile(path.join(root, "summary.md"), "# Historical partial fixture\n\nSix incomplete or blocked journeys.\n"); + await regenerateRunnerDashboard({ bundle: root, historyFile: null }); + const html = await readFile(path.join(root, "index.html"), "utf8"); + expect(html).toContain('href="summary.md"'); expect(html).toContain(before.source.workflowRunUrl); + const after = JSON.parse(await readFile(path.join(root, "normalized-results.json"), "utf8")); + expect(after.results).toEqual(before.results); expect(after.source).toEqual(before.source); + expect(after.selected).toBe(before.selected); + await rm(path.join(root, "summary.md")); + await regenerateRunnerDashboard({ bundle: root, historyFile: null }); + expect(await readFile(path.join(root, "index.html"), "utf8")).not.toContain('href="summary.md"'); + } finally { await rm(root, { recursive: true, force: true }); } +}); diff --git a/tests/runner-e2e/dashboard.ts b/tests/runner-e2e/dashboard.ts index b8cb4ac92b0..2a50c8f4340 100644 --- a/tests/runner-e2e/dashboard.ts +++ b/tests/runner-e2e/dashboard.ts @@ -44,6 +44,8 @@ export interface RunnerDashboardInput { catalog: readonly ReportExecution[]; entries: readonly RunnerDashboardEntry[]; campaign?: RunnerE2ECampaign; + /** The bundle contains its existing relative summary.md document. */ + summaryAvailable?: boolean; history?: RunnerE2EHistoryIndex; } @@ -835,7 +837,7 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) {

Full-stack acceptance campaign

${html(input.title)}

Results across runner profiles and execution environments. Declared PNG screenshots and normalized results are retained with every published campaign; additional diagnostic evidence remains in the access-controlled workflow artifact.

- ${input.campaign ? `

${html(input.generatedAt)}${input.campaign.source.sha ? `${html(input.campaign.source.sha.slice(0,8))}` : ""}${input.campaign.source.workflowRunUrl ? `Source run ↗` : ""}

` : ""} + ${input.campaign ? `

${html(input.generatedAt)}${input.campaign.source.sha ? `${html(input.campaign.source.sha.slice(0,8))}` : ""}${input.campaign.source.workflowRunUrl ? `Source run ↗` : ""}${input.summaryAvailable ? `Campaign summary ↗` : ""}

` : ""}