diff --git a/tests/runner-e2e/dashboard-regenerate.ts b/tests/runner-e2e/dashboard-regenerate.ts index 41c9d2225ef..3934577d3f9 100644 --- a/tests/runner-e2e/dashboard-regenerate.ts +++ b/tests/runner-e2e/dashboard-regenerate.ts @@ -175,6 +175,7 @@ export async function regenerateRunnerDashboard(input: { entries, campaign, summaryAvailable: summary !== undefined, + summaryText: summary, history, }); const upgraded = { diff --git a/tests/runner-e2e/dashboard-summary-link.test.ts b/tests/runner-e2e/dashboard-summary-link.test.ts index 5be3b3c3088..ebb54f3d73d 100644 --- a/tests/runner-e2e/dashboard-summary-link.test.ts +++ b/tests/runner-e2e/dashboard-summary-link.test.ts @@ -22,11 +22,32 @@ it("regeneration derives link availability from the actual summary file without 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); + expect(html).toContain('Campaign notes and coverage'); + expect(html).toContain('Six incomplete or blocked journeys.'); 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"'); + const withoutSummary = await readFile(path.join(root, "index.html"), "utf8"); + expect(withoutSummary).not.toContain('href="summary.md"'); + expect(withoutSummary).not.toContain('Campaign notes and coverage'); } finally { await rm(root, { recursive: true, force: true }); } }); + +it("keeps coverage prose separate from measured counts and escapes untrusted summary markup", () => { + const summaryText = '# Current coverage\n0 passes, 1 failed initial, 243 unrun\n\n- tasks.create-backlog\n\n[unsafe](javascript:alert(1))\n & private-looking text'; + const input = { title: "Campaign", generatedAt: "2026-10-27T00:00:00Z", expected: [], catalog: [], entries: [], campaign: campaign(), summaryText }; + const output = renderRunnerE2EDashboard(input); + expect(output).toContain('0 passes, 1 failed initial, 243 unrun'); + expect(output).toContain('tasks.create-backlog'); + expect(output).toContain('<script>alert(1)</script>'); + expect(output).toContain('<img src=x onerror=alert(2)> & private-looking text'); + expect(output).not.toContain(''); + expect(output).not.toContain('Campaign notes and coverage'); +}); diff --git a/tests/runner-e2e/dashboard.ts b/tests/runner-e2e/dashboard.ts index 2a50c8f4340..96a5eefad07 100644 --- a/tests/runner-e2e/dashboard.ts +++ b/tests/runner-e2e/dashboard.ts @@ -46,6 +46,8 @@ export interface RunnerDashboardInput { campaign?: RunnerE2ECampaign; /** The bundle contains its existing relative summary.md document. */ summaryAvailable?: boolean; + /** Existing public summary.md content, rendered only as escaped readable text. */ + summaryText?: string; history?: RunnerE2EHistoryIndex; } @@ -532,6 +534,9 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { h1 { max-width: 760px; margin: 0; font-size: clamp(32px, 4vw, 56px); font-weight: 650; line-height: 1.03; letter-spacing: -.04em; } .lede { max-width: 720px; margin: 16px 0 0; color: var(--muted-foreground); font-size: 16px; } .report-actions { display: flex; align-items: stretch; gap: 8px; } + .campaign-notes { margin: 0 0 24px; } + .campaign-notes > summary { cursor: pointer; font-weight: 600; } + .campaign-notes pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; line-height: 1.6; } .summary { display: flex; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); } .metric { min-width: 108px; padding: 13px 16px; border-right: 1px solid var(--border); } .metric:last-child { border-right: 0; } @@ -849,6 +854,7 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { + ${input.campaign && input.summaryText?.trim() ? `
Campaign notes and coverage
${html(input.summaryText)}
` : ""} ${renderCampaignSuiteSummary(input.campaign?.suites ?? [], catalog, "", false)}
Campaign usage and spend