Files
PaperClipAI/tests/e2e/runner-e2e-dashboard.spec.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

341 lines
16 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { runnerExecutionById, runnerMatrix } from "../runner-e2e/catalog.js";
import {
renderRunnerE2EDashboard,
type RunnerDashboardEntry,
} from "../runner-e2e/dashboard.js";
import { buildRunnerCampaign } from "../runner-e2e/history.js";
import type { MatrixExecution, RunnerE2EResult } from "../runner-e2e/types.js";
const executionIds = [
"core-compatibility.legacy-codex.local.message-marker",
"core-compatibility.legacy-codex.daytona.message-marker",
"core-compatibility.legacy-claude.local.message-marker",
"local-session-integrity.runner-acpx-codex.local.structured-question-restart-resume",
] as const;
function resultFor(
execution: MatrixExecution,
status: RunnerE2EResult["status"],
): RunnerDashboardEntry {
const result: RunnerE2EResult = {
schema: "paperclip.runner-e2e.result/v1",
executionId: execution.id,
suiteId: execution.suite.id,
attempt: 1,
status,
profileId: execution.profile.id,
environmentId: execution.environment.id,
caseId: execution.task.id,
provider: execution.profile.provider,
model: execution.profile.model,
runtimeMode: execution.profile.expectedRuntimeMode,
startedAt: "2026-09-05T12:00:00.000Z",
finishedAt: "2026-09-05T12:00:12.000Z",
durationMs: 12_000,
runIds: [`run-${execution.id}`],
usage: {
inputTokens: 1_250,
outputTokens: 75,
cachedInputTokens: 500,
},
matcherResults: [
{
matcher: { kind: "message_contains", expected: "PAPERCLIP_E2E_OK" },
passed: status === "passed",
detail: status === "passed" ? "matched" : "marker missing",
},
],
screenshots: [
{
id: "final-state",
label: "Final visible task state",
file: "final-state.png",
},
],
cleanup: "passed",
};
return {
result,
valid: status === "passed",
errors: status === "passed" ? [] : ["fixture failure"],
evidenceBaseHref: `evidence/${execution.id}/attempt-1`,
evidenceFiles: ["final-state.png"],
};
}
test("filters matrix results and pages through the filtered screenshot gallery", async ({
page,
}) => {
const catalog = executionIds.map(runnerExecutionById);
const entries = catalog.map((execution, index) =>
resultFor(execution, index === 1 ? "failed" : "passed"),
);
await page.setContent(
renderRunnerE2EDashboard({
title: "Runner E2E interaction fixture",
generatedAt: "2026-09-05T12:01:00.000Z",
expected: executionIds,
catalog: runnerMatrix,
entries,
}),
);
const filters = page
.locator("[data-report-query]")
.locator("..")
.locator("..");
await expect(page.locator(".suite-nav")).toHaveCount(0);
await expect(page.locator(".suite-section")).toHaveCount(2);
await expect(page.locator('select[data-report-suite] option')).toHaveCount(3);
await expect(page.locator('select[data-report-profile] option')).toHaveCount(4);
await expect(page.locator("[data-report-result]")).toHaveText(
"4 of 4 tests shown",
);
await expect(page.locator("[data-gallery-item]")).toHaveCount(4);
const passedCard = page.locator('[data-execution-id="core-compatibility.legacy-codex.local.message-marker"]');
const failedCard = page.locator('[data-execution-id="core-compatibility.legacy-codex.daytona.message-marker"]');
await expect(passedCard.locator(".status")).toHaveText("passed");
await expect(passedCard.locator(".case-outcome")).toBeHidden();
await expect(passedCard.locator(".billing-strip")).toBeHidden();
await expect(passedCard.locator(".execution-id")).toBeHidden();
await expect(passedCard.locator(".case-diagnostics > summary")).toContainText("1/1 checks · 12s");
await expect(failedCard.locator(":scope > .case-outcome")).toBeVisible();
expect(
await filters.evaluate((element) => getComputedStyle(element).position),
).toBe("static");
await page.locator('.feature-coverage > summary').click();
await page.locator('.feature-coverage').getByRole('button', { name: 'Questions and approvals', exact: true }).click();
await expect(page.locator('select[data-report-feature]')).toHaveValue('questions-and-approvals');
await expect(page.locator('.feature-coverage')).not.toHaveAttribute('open');
await expect(page.locator('[data-report-result]')).toHaveText('1 of 4 tests shown');
await expect(page.locator('.suite-section:visible')).toHaveCount(1);
await expect(page.locator('[data-gallery-open]')).toHaveText('View gallery · 1');
await page.setViewportSize({ width: 900, height: 900 });
await expect(page.locator('[data-report-profile-row]:visible')).toHaveCount(1);
await page.setViewportSize({ width: 1280, height: 720 });
await page.locator('[data-report-reset]').click();
const firstTable = page.locator(".matrix").first();
const widthsBefore = await firstTable
.locator("thead th")
.evaluateAll((cells) =>
cells.map((cell) => cell.getBoundingClientRect().width),
);
await firstTable.locator("details summary").first().click();
const widthsAfter = await firstTable
.locator("thead th")
.evaluateAll((cells) =>
cells.map((cell) => cell.getBoundingClientRect().width),
);
expect(widthsAfter).toEqual(widthsBefore);
await expect(passedCard.locator(".billing-strip")).toBeVisible();
await page.locator("[data-report-query]").fill("Legacy Claude");
await expect(page.locator("[data-report-result]")).toHaveText(
"1 of 4 tests shown",
);
await expect(page.locator("[data-gallery-open]")).toHaveText(
"View gallery · 1",
);
await page.locator("[data-report-reset]").click();
await expect(page.locator("[data-report-query]")).toBeFocused();
await page
.locator("select[data-report-profile]")
.selectOption("legacy-codex");
await expect(page.locator("[data-report-result]")).toHaveText(
"2 of 4 tests shown",
);
await expect(page.locator("[data-gallery-open]")).toHaveText(
"View gallery · 2",
);
await page.locator("[data-gallery-open]").click();
const dialog = page.locator("[data-gallery-dialog]");
await expect(dialog).toBeVisible();
await expect(dialog.locator("[data-gallery-profile]")).toHaveText(
"Legacy Codex",
);
await expect(dialog.locator("[data-gallery-environment]")).toHaveText(
"Isolated local",
);
await expect(dialog.locator("[data-gallery-position]")).toHaveText(
"Image 1 of 2",
);
await expect(dialog.locator("[data-gallery-original]")).toHaveAttribute("href", /legacy-codex\.local\.message-marker\/attempt-1\/final-state\.png$/);
await page.keyboard.press("ArrowRight");
await expect(dialog.locator("[data-gallery-environment]")).toHaveText(
"Daytona sandbox",
);
await expect(dialog.locator("[data-gallery-status]")).toHaveText("failed");
await expect(dialog.locator("[data-gallery-position]")).toHaveText(
"Image 2 of 2",
);
await expect(dialog.locator("[data-gallery-original]")).toHaveAttribute("href", /legacy-codex\.daytona\.message-marker\/attempt-1\/final-state\.png$/);
await page.keyboard.press("Escape");
await expect(dialog).not.toBeVisible();
await page.locator("[data-report-reset]").click();
await page.keyboard.press("/");
await expect(page.locator("[data-report-query]")).toBeFocused();
});
function navigationReport(ids: readonly string[], suppliedEntries?: readonly RunnerDashboardEntry[]) {
const catalog = ids.map(runnerExecutionById);
const entries = suppliedEntries ?? catalog.map((execution) => resultFor(execution, "passed"));
const generatedAt = "2026-09-05T12:01:00.000Z";
return renderRunnerE2EDashboard({
title: "Runner E2E navigation fixture",
generatedAt,
expected: ids,
catalog: runnerMatrix,
entries,
campaign: buildRunnerCampaign({
campaignId: "navigation-fixture",
generatedAt,
expected: ids,
catalog,
results: entries.map((entry) => entry.result),
}),
});
}
test("suite links reveal filtered sections and scroll on repeated clicks", async ({ page }) => {
const report = navigationReport(executionIds);
const reportUrl = "http://runner-e2e.test/campaign/";
await page.route(reportUrl, (route) => route.fulfill({ contentType: "text/html", body: report }));
await page.goto(reportUrl);
const core = page.locator("#suite-core-compatibility");
const coreHeading = core.getByRole("heading", { name: "Core Runner Compatibility", exact: true });
const coreLink = page.getByRole("link", { name: "Core Runner Compatibility", exact: true });
const backToTop = page.getByRole("link", { name: "Back to top", exact: true });
await expect(backToTop).toBeHidden();
const feature = page.locator("select[data-report-feature]");
for (let click = 0; click < 2; click += 1) {
await feature.selectOption("questions-and-approvals");
await expect(core).toBeHidden();
await coreLink.click();
await expect(core).toBeVisible();
await expect(coreHeading).toBeInViewport();
await expect(feature).toHaveValue("");
await expect(page).toHaveURL(`${reportUrl}#suite-core-compatibility`);
await expect(page.locator("[data-report-result]")).toHaveText("4 of 4 tests shown");
}
await page.locator("select[data-report-profile]").selectOption("legacy-codex");
await coreLink.click();
await expect(coreHeading).toBeInViewport();
await expect(page.locator("select[data-report-profile]")).toHaveValue("legacy-codex");
await expect(backToTop).toBeVisible();
expect(await backToTop.evaluate((element) => getComputedStyle(element).position)).toBe("fixed");
await backToTop.focus();
await page.keyboard.press("Enter");
await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0);
await expect(backToTop).toBeHidden();
await expect(page.locator(".report-header h1")).toBeFocused();
await expect(page.locator("select[data-report-profile]")).toHaveValue("legacy-codex");
await coreLink.click();
await page.getByRole("link", { name: "Local Session Integrity", exact: true }).click();
await expect(page.locator("#suite-local-session-integrity")).toBeVisible();
await feature.selectOption("questions-and-approvals");
await page.goBack();
await expect(coreHeading).toBeInViewport();
await expect(feature).toHaveValue("");
});
test("folds the suite index and links each case to a matching configuration", async ({ page }) => {
const ids = [...executionIds, "core-compatibility.legacy-codex.local.ask-question"];
const reportUrl = "http://runner-e2e.test/campaign/";
await page.route(reportUrl, (route) => route.fulfill({ contentType: "text/html", body: navigationReport(ids) }));
await page.goto(reportUrl);
const coreIndex = page.locator(".campaign-suite-details").filter({
has: page.getByRole("link", { name: "Core Runner Compatibility", exact: true }),
});
await expect(coreIndex).not.toHaveAttribute("open");
await coreIndex.getByRole("link", { name: "Core Runner Compatibility", exact: true }).click();
await expect(coreIndex).not.toHaveAttribute("open");
await coreIndex.locator("summary").focus();
await page.keyboard.press("Enter");
await expect(coreIndex).toHaveAttribute("open");
await expect(coreIndex.locator(".suite-case-list a")).toHaveCount(2);
const sectionToc = page.locator("#suite-core-compatibility .suite-case-toc");
await expect(sectionToc.locator(".suite-case-list a")).toHaveCount(2);
const basicLink = coreIndex.getByRole("link", { name: "Basic response", exact: true });
const profile = page.locator("select[data-report-profile]");
const feature = page.locator("select[data-report-feature]");
await profile.selectOption("legacy-claude");
const claudeResult = page.locator('[data-execution-id="core-compatibility.legacy-claude.local.message-marker"]');
await basicLink.click();
await expect(claudeResult.locator(".case-heading")).toBeInViewport();
await expect(profile).toHaveValue("legacy-claude");
await expect(page).toHaveURL(`${reportUrl}#execution-core-compatibility.legacy-claude.local.message-marker`);
for (let click = 0; click < 2; click += 1) {
await feature.selectOption("questions-and-approvals");
await basicLink.click();
await expect(feature).toHaveValue("");
await expect(profile).toHaveValue("");
await expect(page.locator('[data-execution-id="core-compatibility.legacy-codex.local.message-marker"] .case-heading')).toBeInViewport();
}
await sectionToc.getByRole("link", { name: "Ask mode question", exact: true }).click();
await expect(page.locator('[data-execution-id="core-compatibility.legacy-codex.local.ask-question"] .case-heading')).toBeInViewport();
await sectionToc.getByRole("link", { name: "Back to index", exact: false }).click();
await expect(coreIndex.locator("summary")).toBeInViewport();
await page.setViewportSize({ width: 390, height: 844 });
await expect(basicLink).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await basicLink.click();
const backToTop = page.getByRole("link", { name: "Back to top", exact: true });
await expect(backToTop).toBeVisible();
expect(await backToTop.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= innerWidth && bounds.bottom <= innerHeight;
})).toBe(true);
await backToTop.click();
await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0);
});
test("case status dots summarize matching results and open failures", async ({ page }) => {
const ids = [...executionIds, "core-compatibility.legacy-codex.local.ask-question"];
const entries = executionIds.map((id, index) => resultFor(runnerExecutionById(id), index === 1 ? "failed" : "passed"));
const reportUrl = "http://runner-e2e.test/campaign/";
await page.route(reportUrl, (route) => route.fulfill({ contentType: "text/html", body: navigationReport(ids, entries) }));
await page.goto(reportUrl);
const toc = page.locator("#suite-core-compatibility .suite-case-toc");
const basic = toc.getByRole("link", { name: "Basic response", exact: true });
const missing = toc.getByRole("link", { name: "Ask mode question", exact: true });
const topLink = page.locator('.campaign-case-index [data-report-case-link="core-compatibility.message-marker"]');
await expect(basic).toHaveAttribute("data-case-status", "failed");
await expect(topLink).toHaveAttribute("data-case-status", "failed");
await expect(basic).toHaveAccessibleDescription("Failed · 2/3 passed · 1 failed — All configurations");
await expect(missing).toHaveAttribute("data-case-status", "missing");
await expect(missing).toHaveAttribute("title", "Missing result · 0/1 passed · 1 missing — All configurations");
const red = await basic.locator(".case-status-dot").evaluate((element) => getComputedStyle(element).backgroundColor);
const profile = page.locator("select[data-report-profile]");
await profile.selectOption("legacy-claude");
await expect(basic).toHaveAttribute("data-case-status", "passed");
await expect(topLink).toHaveAttribute("data-case-status", "passed");
await expect(basic).toHaveAccessibleDescription("Passed · 1/1 passed — Matching configurations");
const green = await basic.locator(".case-status-dot").evaluate((element) => getComputedStyle(element).backgroundColor);
expect(green).not.toBe(red);
await expect(missing).toHaveAttribute("title", /none match current filters/);
await profile.selectOption("legacy-codex");
await expect(basic).toHaveAttribute("data-case-status", "failed");
await page.locator("select[data-report-environment]").selectOption("local");
await expect(basic).toHaveAttribute("data-case-status", "passed");
await page.locator("[data-report-reset]").click();
await basic.click();
await expect(page).toHaveURL(`${reportUrl}#execution-core-compatibility.legacy-codex.daytona.message-marker`);
await expect(page.locator('[data-execution-id="core-compatibility.legacy-codex.daytona.message-marker"] .case-heading')).toBeInViewport();
});