Files
PaperClipAI/tests/runner-e2e/first-task-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

316 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { renderCaseOutcome } from "./case-outcome.js";
import { renderRunnerE2EDashboard } from "./dashboard.js";
import {
firstTaskTranscript,
firstTaskUserRequest,
renderFirstTaskTranscript,
} from "./first-task-transcript.js";
import type {
FirstTaskEvidence,
FirstTaskCheckpoint,
} from "./first-task-scoring.js";
import type { RunnerE2EResult } from "./types.js";
function recording(): FirstTaskEvidence {
const opening: FirstTaskCheckpoint = {
id: "opening",
phase: "opening",
at: "2026-09-15T00:00:00Z",
issueId: "task",
tasks: [],
agents: [{ id: "agent", name: "Alex" }],
runs: [],
documents: [],
comments: [{ id: "greeting", authorAgentId: "agent", body: "Welcome" }],
interactions: [
{
id: "question",
kind: "ask_user_questions",
status: "pending",
payload: {
questions: [
{
id: "q",
prompt: "What would help?",
options: [{ id: "plan", label: "Make a plan" }],
},
],
},
},
],
};
const response = structuredClone(opening);
Object.assign(response, {
id: "response",
phase: "response",
at: "2026-09-15T00:01:00Z",
});
Object.assign(response.interactions[0], {
status: "answered",
resolvedAt: "2026-09-15T00:00:20Z",
result: {
answers: [
{
questionId: "q",
optionIds: ["plan"],
otherText: "Use our garden club facts",
},
],
},
});
response.comments.push({
id: "reply",
authorAgentId: "agent",
body: "Shall I proceed? <script>unsafe()</script>",
createdAt: "2026-09-15T00:00:50Z",
});
response.documents.push({
id: "doc",
key: "plan",
title: "Proposal",
body: "Original scope",
latestRevisionId: "v1",
latestRevisionNumber: 1,
});
const finished = structuredClone(response);
Object.assign(finished, {
id: "finished",
phase: "finished",
at: "2026-09-15T00:02:00Z",
});
Object.assign(finished.documents[0], {
body: "Revised scope",
latestRevisionId: "v2",
latestRevisionNumber: 2,
});
return {
caseId: "clear-task-first-response",
nonce: "test",
onboardingIssueId: "task",
agentId: "agent",
initialTaskIds: ["task"],
instructions: [],
configuredModel: null,
observedModels: [],
checkpoints: [opening, response, finished],
checks: [
{
id: "no-premature-work",
passed: true,
detail: "No work before approval",
evidence: ["response"],
},
],
};
}
function result(): RunnerE2EResult {
return {
schema: "paperclip.runner-e2e.result/v2",
suiteId: "first-task",
executionId: "first-task.legacy-codex.local.clear-task-first-response",
attempt: 1,
status: "failed",
profileId: "legacy-codex",
environmentId: "local",
caseId: "clear-task-first-response",
provider: "openai",
model: "unknown",
runtimeMode: "legacy",
startedAt: "2026-09-15T00:00:00Z",
finishedAt: "2026-09-15T00:02:00Z",
durationMs: 120000,
runIds: [],
cleanup: "passed",
firstTask: recording(),
failureClass: "secret_leak",
error:
"Credential detected in persisted Paperclip home: sessions/example.json",
};
}
describe("first-task conversation report", () => {
it("grounds requirements in submitted user comments and resolved form answers", () => {
const e = recording();
for (const checkpoint of e.checkpoints) {
for (const row of [...checkpoint.comments, ...checkpoint.interactions]) row.issueId = "task";
Object.assign(checkpoint.interactions[0], { resolvedByUserId: "board" });
}
e.checkpoints.at(-1)!.comments.push({ id: "user-scope", issueId: "task", authorUserId: "board", body: "Invite beginners explicitly." });
const request = firstTaskUserRequest(e);
expect(request).toContain("[comment:task:user-scope] Invite beginners explicitly.");
expect(request).toContain("[answer:question] Make a plan\nUse our garden club facts");
expect(request.match(/Use our garden club facts/g)).toHaveLength(1);
expect(request).not.toMatch(/Shall I proceed|Welcome|GARDENtest|two-sentence/);
});
it("does not infer requirements from scenario defaults, agent answers, or unrelated tasks", () => {
const e = recording();
for (const checkpoint of e.checkpoints) {
for (const row of checkpoint.interactions) Object.assign(row, { issueId: "task", resolvedByAgentId: "agent" });
checkpoint.comments.push({ id: "foreign", issueId: "other-task", authorUserId: "board", body: "Foreign requirement" });
}
expect(() => firstTaskUserRequest(e)).toThrow("Missing recorded user request");
});
it("deduplicates observations, keeps first-observed time, and includes answers and each document revision", () => {
const entries = firstTaskTranscript(recording());
expect(entries.filter((e) => e.kind === "comment")).toHaveLength(2);
expect(entries.filter((e) => e.kind === "interaction")).toHaveLength(1);
expect(entries.filter((e) => e.kind === "answer")).toHaveLength(1);
expect(
entries.filter((e) => e.kind === "document").map((e) => e.row.body),
).toEqual(["Original scope", "Revised scope"]);
expect(entries.find((e) => e.row.id === "greeting")?.at).toBe(
"2026-09-15T00:00:00Z",
);
expect(entries.find((e) => e.kind === "interaction")?.row.status).toBe(
"answered",
);
expect(entries.findIndex((e) => e.kind === "answer")).toBeLessThan(
entries.findIndex((e) => e.row.id === "reply"),
);
});
it("renders full text, option labels, source references, and escaped untrusted chat", () => {
const e = recording();
const longText = "Full retained body ".repeat(1000);
e.checkpoints.at(-1)!.comments.push({ id: "long", body: longText });
const rendered = renderFirstTaskTranscript(e, (id) => `#${id}`);
expect(rendered).toContain(longText);
expect(rendered).toContain("Agent · Alex");
expect(rendered).toContain("transcript-comment transcript-agent");
expect(rendered).toContain("transcript-comment transcript-human");
expect(rendered).toContain("transcript-answer transcript-human");
expect(rendered).toContain("transcript-interaction transcript-card");
expect(rendered).toContain("User card response");
expect(rendered).toContain("Make a plan\nUse our garden club facts");
expect(rendered).toContain('href="#finished"');
expect(rendered).toContain("&lt;script&gt;unsafe()&lt;/script&gt;");
expect(rendered).not.toContain("<script>");
});
it("makes the non-matcher failure explicit when all behavior checks pass", () => {
const rendered = renderCaseOutcome(result(), false, [result().error!]);
expect(rendered).toContain("Overall failed · 1/1 behavioral checks passed");
expect(rendered).toContain("No behavioral matcher failed");
expect(rendered).toContain("Credential-persistence check failed");
expect(rendered.match(/sessions\/example.json/g)).toHaveLength(1);
});
it("names failed matchers and retains independent validation failures", () => {
const r = result();
r.firstTask!.checks[0] = {
id: "no-premature-work",
passed: false,
detail: "Created <final> document before acceptance",
evidence: ["response"],
};
const rendered = renderCaseOutcome(r, false, ["Evidence missing"]);
expect(rendered).toContain("0/1 behavioral checks passed");
expect(rendered).toContain("<strong>no-premature-work</strong>");
expect(rendered).toContain(
"Created &lt;final&gt; document before acceptance",
);
expect(rendered).toContain("Evidence missing");
expect(rendered).not.toContain("No behavioral matcher failed");
});
it("separates unexercised checks from behavioral failures and passes", () => {
const r = result();
r.firstTask!.checks.push({
id: "rejection-respected", passed: false, evidence: [],
detail: "Rejected work never executes",
notReached: "The rejection checkpoint was not reached <yet>.",
});
const rendered = renderCaseOutcome(r, false, []);
expect(rendered).toContain("1/1 behavioral checks passed · 1 not reached");
expect(rendered).toContain("Not reached:");
expect(rendered).toContain("The journey is incomplete");
expect(rendered).toContain("not reached &lt;yet&gt;");
expect(rendered).not.toContain("<p>Failed checks:</p>");
expect(rendered).not.toContain("separate run, cleanup, or evidence check");
expect(rendered).toContain("<td>Not reached</td>");
expect(rendered).toContain("Overall failed");
});
it("does not invent matcher results when a run fails before assertions", () => {
const r = result();
r.firstTask = undefined;
expect(renderCaseOutcome(r, false, [])).toContain(
"No behavioral checks recorded",
);
r.status = "passed";
r.failureClass = undefined;
r.error = undefined;
expect(renderCaseOutcome(r, true, [])).toContain("Overall passed");
});
it("keeps large timeout payloads expandable without burying the checks and conversation", () => {
const r = result();
r.error = `Timed out waiting for outcome: ${"recorded state ".repeat(200)}<unsafe>`;
const rendered = renderCaseOutcome(r, false, []);
expect(rendered).toContain("<details><summary>Full failure details");
expect(rendered).toContain("&lt;unsafe&gt;");
expect(rendered).not.toContain("<unsafe>");
expect(rendered).toContain("Overall failed");
});
it("shows an interrupted regraded journey consistently in the dashboard and gallery", () => {
const r = result();
r.failureClass = "candidate_failure";
r.error = "Recording stopped before acceptance.";
r.firstTask!.checks.push({
id: "acceptance-recorded", passed: false, evidence: [],
detail: "Acceptance recorded", notReached: "No acceptance checkpoint.",
});
r.matcherResults = [{ matcher: { kind: "json_path", path: "old-check", expected: true }, passed: false, detail: "Stale result" }];
r.screenshots = [{ id: "failure", label: "Task state at failure", file: "failure.png", publication: "public-runner-fixture" }];
const render = (errors: string[] = []) => renderRunnerE2EDashboard({
title: "Regraded campaign", generatedAt: r.finishedAt,
expected: [r.executionId], catalog: [],
entries: [{ result: r, valid: false, errors, evidenceBaseHref: "evidence/codex", evidenceFiles: ["failure.png", "result.json", "regraded-result.json"] }],
});
const page = render();
expect(page).toContain('data-gallery-status="incomplete"');
expect(page).toContain('data-gallery-matchers="1/1 matchers passed · 1 not reached"');
expect(page).toContain('data-gallery-label="Task state when recording stopped"');
expect(page).toContain("Incomplete journey");
expect(page).toContain('<strong>1</strong><span>Incomplete</span>');
expect(page).toContain('<strong>0</strong><span>Failed</span>');
expect(page).toContain("Regraded result JSON");
expect(page).toContain("Original result JSON");
expect(page).not.toContain("<td>Stale result</td>");
expect(render(["Evidence missing"])).toContain('data-gallery-status="failed"');
});
it("integrates visible outcome, transcript, and only available evidence links in the dashboard", () => {
const r = result();
const rendered = renderRunnerE2EDashboard({
title: "Smoke review",
generatedAt: r.finishedAt,
expected: [r.executionId],
catalog: [],
entries: [
{
result: r,
valid: false,
errors: [],
evidenceBaseHref: "evidence/codex",
evidenceFiles: [
"snapshots/first-task.json",
"snapshots/first-task-run-evidence.json",
"result.json",
],
},
],
});
expect(rendered).toContain("Read full conversation</summary>");
expect(rendered).toContain(
'href="evidence/codex/snapshots/first-task-run-evidence.json"',
);
expect(rendered).not.toContain(
'href="evidence/codex/html-report/index.html"',
);
const diagnostics = rendered.indexOf('class="case-context case-diagnostics"');
expect(diagnostics).toBeGreaterThan(0);
expect(rendered.indexOf("Credential-persistence check failed")).toBeLessThan(diagnostics);
expect(rendered.indexOf("Full run / tool-event JSON")).toBeGreaterThan(diagnostics);
expect(rendered).toContain(
'id="first-task-first-task.legacy-codex.local.clear-task-first-response-response"',
);
});
});