Files
PaperClipAI/tests/runner-e2e/history.test.ts
T
Dotta af3023f1e3 fix(runner): repair paid provider startup paths (#12769)
## Thinking Path

> - Paperclip manages AI agents that perform work.
> - Paperclip Runner connects durable task runs to local provider
processes.
> - The full-stack paid matrix exposed failures after the runner
integrity repair.
> - Verified JavaScript entrypoints lost their relative module graph
when Linux executed them through descriptor paths.
> - Returned provider startup errors also remained pending and became
indeterminate after recovery.
> - Sparse Codex tool lifecycle events lost the `write_document`
identity before task transcript projection.
> - This pull request repairs those three boundaries and makes the
structured-question fixture deterministic.
> - The benefit is repeatable provider startup, exact failure replay,
and correct inline Plan placement.

## Linked Issues or Issue Description

Refs #12721 and #12700.

**What happened?**

The paid runner matrix failed ACPX and OpenCode startup before provider
session creation. The runner journal then replaced the original startup
error with an indeterminate recovery result. Native Codex saved a Plan
but rendered it only as a fallback card. A legacy Claude waiting reply
could also echo the reserved terminal marker before the answer arrived.

**Expected behavior**

Verified JavaScript providers must start from immutable
descriptor-backed artifacts. Returned startup failures must persist as
terminal failed command results. Native tool lifecycle updates must
preserve the `write_document` boundary. Pre-answer fixture output must
not contain the reserved terminal marker.

**Steps to reproduce**

1. Run the local provider cells in the Runner Full-Stack E2E workflow.
2. Observe ACPX and OpenCode fail during `session.open` before provider
execution.
3. Observe recovery report `execution_indeterminate` instead of the
original startup error.
4. Run the native Codex Plan cell and observe the fallback Plan card
after the tool activity row.
5. Run the legacy Claude structured-question resume cell and observe an
early marker echo in waiting prose.

**Paperclip version or commit**

`0f9452101740835ce0b1488a204bf48acd5bafc3`

**Deployment mode**

Local development with the paid GitHub Actions acceptance workflow.

## What Changed

- Bundle the ACPX sidecar and OpenCode proxy as self-contained Node ESM
entrypoints before hashing and verified descriptor launch.
- Anchor ACPX dynamic provider package resolution at a
controller-derived provider-pack root and keep that root out of the
provider child environment.
- Persist executor-returned startup errors as redacted durable failed
command results while retaining indeterminate recovery for true process
death.
- Coalesce sparse native tool items by stable ID so a late
`write_document` name, input, and result reach the transcript boundary
once.
- Forbid the structured-question fixture from spelling or announcing its
reserved terminal marker before the user answers.

## Verification

- Rust and TypeScript regression tests cover durable failed replay, true
crash ambiguity, bundle closure, package-root derivation, environment
filtering, exact Codex tool lifecycle coalescing, and prompt
determinism.
- Local execution is intentionally limited to formatters and static diff
checks. GitHub Actions will run tests, type checks, builds, and security
checks.
- After ordinary CI is green, scoped paid cells will validate one ACPX
launch, one OpenCode launch, native Codex Plan projection, and legacy
Claude structured resume before a complete matrix rerun.
- Prior failing matrix:
https://github.com/paperclipai/paperclip/actions/runs/33682434315

## Risks

- Bundling changes the bytes covered by provider launch hashes.
Provider-pack generation already hashes the final built files.
- ACPX still loads qualified provider packages dynamically. The
controller supplies a normalized package root, while existing version,
digest, path, and descriptor checks remain active.
- Durable `failed` is terminal. Replays return the same redacted result
and do not execute the provider effect twice.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex based on GPT-5 with agentic reasoning, repository
inspection, code editing, Git, parallel subagents, and GitHub Actions
coordination. The exact deployed snapshot and context-window size are
not exposed to this task.

## 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 linked related public work or described the bug in
this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket id
- [ ] I have run tests locally and they pass (intentionally deferred to
GitHub Actions)
- [x] I have added or updated tests where applicable
- [x] No documentation change is required for this runtime repair
- [x] I have considered and documented the 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
2026-09-04 07:58:44 -05:00

397 lines
14 KiB
TypeScript

import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { runnerMatrix } from "./catalog.js";
import { regenerateRunnerDashboard } from "./dashboard-regenerate.js";
import { renderRunnerE2EDashboard } from "./dashboard.js";
import {
buildHistoryPointers,
createBundleManifest,
isHistoricalBundlePathAllowed,
prunePrivateHistoryEvidence,
validateHistoryDestination,
} from "./history-publish.js";
import {
buildRunnerCampaign,
campaignHistoryRecord,
canonicalExecutionId,
emptyRunnerHistory,
mergeRunnerHistory,
} from "./history.js";
import { renderRunnerHistoryIndex } from "./history-index.js";
import type { MatrixExecution, RunnerE2EResult } from "./types.js";
const temporaryDirectories: string[] = [];
afterEach(async () => {
vi.unstubAllEnvs();
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
function result(execution: MatrixExecution, status: "passed" | "failed") {
return {
schema: "paperclip.runner-e2e.result/v2",
executionId: execution.id,
suiteId: execution.suite.id,
suiteDefinitionHash: execution.suiteDefinitionHash,
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,
runIds: ["run-1"],
usage: { inputTokens: 100, outputTokens: 25, costUsd: 0.01 },
startedAt: "2026-08-28T00:00:00.000Z",
finishedAt: "2026-08-28T00:00:01.000Z",
durationMs: 1_000,
cleanup: "passed",
} satisfies RunnerE2EResult;
}
describe("runner E2E campaign history", () => {
it("records the resolved paid target instead of the trusted workflow checkout", () => {
vi.stubEnv("PAPERCLIP_RUNNER_E2E_SOURCE_SHA", "target-sha");
vi.stubEnv("PAPERCLIP_RUNNER_E2E_SOURCE_REF", "refs/heads/target");
vi.stubEnv("GITHUB_SHA", "trusted-master-sha");
vi.stubEnv("GITHUB_REF", "refs/heads/master");
const execution = runnerMatrix[0]!;
const campaign = buildRunnerCampaign({
campaignId: "target-provenance",
generatedAt: "2026-08-28T00:01:00.000Z",
expected: [execution.id],
results: [
{
...result(execution, "passed"),
source: {
sha: "retained-result-sha",
ref: "refs/heads/retained-result",
workflowRunUrl: "https://example.test/actions/runs/1",
},
},
],
});
expect(campaign.source).toMatchObject({
sha: "target-sha",
ref: "refs/heads/target",
workflowRunUrl: "https://example.test/actions/runs/1",
});
});
it("migrates v1 execution IDs and keeps partial suite runs out of overall trends", () => {
expect(canonicalExecutionId("legacy-codex.local.message-marker")).toBe(
"core-compatibility.legacy-codex.local.message-marker",
);
const breadth = runnerMatrix.filter(
(execution) => execution.suite.id === "openrouter-model-breadth",
);
const campaign = buildRunnerCampaign({
campaignId: "breadth-smoke",
generatedAt: "2026-08-28T00:01:00.000Z",
expected: breadth.map((execution) => execution.id),
results: breadth.map((execution) => result(execution, "passed")),
});
expect(campaign).toMatchObject({ complete: false, passed: 10, failed: 0 });
expect(campaign.suites[0]).toMatchObject({
suiteId: "openrouter-model-breadth",
complete: true,
selected: 10,
});
expect(campaign.billing).toMatchObject({
llm: { inputTokens: 1_000, outputTokens: 250 },
});
expect(campaign.billing.reportedLlmCostUsd).toBeCloseTo(0.1, 10);
const history = mergeRunnerHistory(
emptyRunnerHistory(),
campaignHistoryRecord(campaign, "https://history.example/runner-e2e"),
);
expect(history.latestGreenCampaignId).toBeNull();
expect(history.latestGreenBySuite).toEqual({
"openrouter-model-breadth": "breadth-smoke",
});
});
it("retains latest and latest-green pointers independently", () => {
const green = buildRunnerCampaign({
campaignId: "complete-green",
generatedAt: "2026-08-28T00:01:00.000Z",
expected: runnerMatrix.map((execution) => execution.id),
results: runnerMatrix.map((execution) => result(execution, "passed")),
});
const red = buildRunnerCampaign({
campaignId: "complete-red",
generatedAt: "2026-08-28T01:01:00.000Z",
expected: runnerMatrix.map((execution) => execution.id),
results: runnerMatrix.map((execution, index) =>
result(execution, index === 0 ? "failed" : "passed"),
),
});
let history = mergeRunnerHistory(
emptyRunnerHistory(),
campaignHistoryRecord(green, "https://history.example/runner-e2e"),
);
history = mergeRunnerHistory(
history,
campaignHistoryRecord(red, "https://history.example/runner-e2e"),
);
const pointers = buildHistoryPointers(history);
expect(pointers.latest.overall).toMatchObject({
campaignId: "complete-red",
});
expect(pointers.latestGreen.overall).toMatchObject({
campaignId: "complete-green",
});
expect(history.campaigns).toHaveLength(2);
const dashboard = renderRunnerE2EDashboard({
title: "Runner Full-Stack E2E",
generatedAt: red.generatedAt,
expected: red.expected,
catalog: runnerMatrix,
campaign: red,
history,
entries: red.results.map((campaignResult) => ({
result: campaignResult,
valid: campaignResult.status === "passed",
errors: campaignResult.status === "passed" ? [] : ["failed"],
})),
});
expect(dashboard).toContain("Campaign trends");
expect(dashboard).toContain("data-history-from");
expect(dashboard).toContain("data-history-through");
expect(dashboard).toContain(
'data-history-suite-trends="core-compatibility"',
);
expect(dashboard).toContain(
'data-history-suite-trends="openrouter-model-breadth"',
);
expect(dashboard).toContain(
'data-history-suite-trends="local-session-integrity"',
);
expect(dashboard).toContain("Suite pass rate");
expect(dashboard).toContain("lines break at definition changes");
expect(dashboard).toContain("cleanup passed");
const index = renderRunnerHistoryIndex(history);
expect(index).toContain("Runner E2E campaigns");
expect(index).toContain("complete-green");
expect(index).toContain("complete-red");
expect(index).toContain("66/66 passed");
expect(index).toContain("65/66 passed");
expect(index).toContain("Open report →");
expect(index).toContain(
"Visual evidence remains in access-controlled workflow artifacts",
);
expect(index).toContain("Inert structured public evidence");
expect(index).not.toContain("data-gallery-dialog");
expect(index).not.toContain("Configuration matrix");
});
});
describe("historical publication security", () => {
it("keeps visual and active evidence private when building the public dashboard", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "runner-landing-test-"));
const output = path.join(root, "landing");
temporaryDirectories.push(root);
const execution = runnerMatrix[0]!;
const campaignResult = {
...result(execution, "passed"),
screenshots: [
{
id: "final-state",
label: "Final state",
file: "final-state.png",
},
],
} satisfies RunnerE2EResult;
const campaign = buildRunnerCampaign({
campaignId: "campaign-1",
generatedAt: "2026-08-28T00:01:00.000Z",
expected: [execution.id],
results: [campaignResult],
});
const evidenceDirectory = path.join(
root,
"evidence",
execution.id,
"attempt-1",
);
await mkdir(evidenceDirectory, { recursive: true });
await writeFile(
path.join(root, "normalized-results.json"),
JSON.stringify(campaign),
);
await writeFile(path.join(evidenceDirectory, "final-state.png"), "png");
await writeFile(path.join(evidenceDirectory, "failure.webm"), "webm");
await writeFile(path.join(evidenceDirectory, "unsafe.svg"), "<svg />");
await writeFile(
path.join(evidenceDirectory, "junit.xml"),
"<?xml-stylesheet href='https://example.test/private.xsl'?>",
);
await writeFile(path.join(evidenceDirectory, "result.json"), "{}\n");
await mkdir(path.join(evidenceDirectory, "snapshots"));
await writeFile(
path.join(evidenceDirectory, "snapshots", "api-state.json"),
"{}\n",
);
await mkdir(path.join(evidenceDirectory, "html-report"));
await writeFile(
path.join(evidenceDirectory, "html-report", "index.html"),
"<img src='data:image/png;base64,cHJpdmF0ZQ==' />",
);
await mkdir(path.join(evidenceDirectory, "blob-report"));
await writeFile(
path.join(evidenceDirectory, "blob-report", "report.zip"),
"private archive",
);
await prunePrivateHistoryEvidence(root);
await regenerateRunnerDashboard({
bundle: root,
outputDirectory: output,
evidenceHrefPrefix: "campaigns/campaign-1",
});
const dashboard = await readFile(path.join(output, "index.html"), "utf8");
expect(dashboard).not.toContain(
`campaigns/campaign-1/evidence/${execution.id}/attempt-1/final-state.png`,
);
expect(dashboard).toContain("Visual evidence · workflow artifact only");
expect(dashboard).toContain(
"public history contains inert structured evidence only",
);
expect(dashboard).toContain("Public history excludes visual evidence");
await expect(
readFile(path.join(evidenceDirectory, "final-state.png")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "failure.webm")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "unsafe.svg")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "junit.xml")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "html-report", "index.html")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "blob-report", "report.zip")),
).rejects.toThrow();
await expect(
readFile(path.join(evidenceDirectory, "result.json"), "utf8"),
).resolves.toBe("{}\n");
await expect(
readFile(
path.join(evidenceDirectory, "snapshots", "api-state.json"),
"utf8",
),
).resolves.toBe("{}\n");
expect(
JSON.parse(
await readFile(path.join(output, "normalized-results.json"), "utf8"),
).schema,
).toBe("paperclip.runner-e2e.campaign/v2");
await expect(
regenerateRunnerDashboard({
bundle: root,
outputDirectory: output,
evidenceHrefPrefix: "../unsafe",
}),
).rejects.toThrow("safe relative URL path");
});
it("requires a private-origin-compatible destination shape", () => {
expect(
validateHistoryDestination({
bucket: "paperclip-runner-e2e-history",
prefix: "/runner-e2e/",
publicBaseUrl: "https://history.paperclip.ai/",
}),
).toEqual({
prefix: "runner-e2e",
publicBaseUrl: "https://history.paperclip.ai",
});
expect(() =>
validateHistoryDestination({
bucket: "paperclip-runner-e2e-history",
prefix: "../unsafe",
publicBaseUrl: "https://history.paperclip.ai/",
}),
).toThrow("safe non-empty key prefix");
expect(() =>
validateHistoryDestination({
bucket: "paperclip-runner-e2e-history",
prefix: "runner-e2e",
publicBaseUrl: "http://history.paperclip.ai/",
}),
).toThrow("credential-free HTTPS");
});
it("rejects non-allowlisted files and fingerprints an immutable bundle", async () => {
expect(isHistoricalBundlePathAllowed("normalized-results.json")).toBe(true);
expect(isHistoricalBundlePathAllowed("junit.xml")).toBe(true);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/final-state.png",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/failure.webm",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/unsafe.svg",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/junit.xml",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/blob-report/report.zip",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/html-report/index.html",
),
).toBe(false);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/result.json",
),
).toBe(true);
expect(
isHistoricalBundlePathAllowed(
"evidence/core-compatibility.profile.local.case/attempt-1/snapshots/api-state.json",
),
).toBe(true);
expect(isHistoricalBundlePathAllowed("paperclip-home/database")).toBe(
false,
);
const root = await mkdtemp(path.join(os.tmpdir(), "runner-history-test-"));
temporaryDirectories.push(root);
await mkdir(path.join(root, "assets"));
await writeFile(path.join(root, "index.html"), "safe");
await writeFile(path.join(root, "assets", "favicon.svg"), "safe");
const first = await createBundleManifest(root, "campaign-1");
const second = await createBundleManifest(root, "campaign-1");
expect(first.bundleDigest).toBe(second.bundleDigest);
await writeFile(path.join(root, "database.sqlite"), "unsafe");
await expect(createBundleManifest(root, "campaign-1")).rejects.toThrow(
"non-allowlisted",
);
});
});