Files
PaperClipAI/tests/runner-e2e/select-rerun-artifacts.test.ts
T
Dotta b84964e5a2 fix(runner): stabilize local paid E2E recovery (#12836)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paid runner E2E tests verify the complete runner, control-plane, and
UI path.
> - A server restart could load a fresh task page while Playwright still
waited on an unsettled Vite navigation lifecycle.
> - The current server also ignored the isolated Vite cache path and
skipped Vite's per-request HTML transform from the known-green runner
snapshot.
> - A one-cell paid run then exposed that download-artifact v8 removes
the artifact-name directory for one pattern match.
> - This pull request restores the Vite contract, proves a fresh
document after restart, and accepts only the exact singleton artifact
layout.
> - The benefit is reliable local runner qualification without weaker
UI, source, or artifact checks.

## Linked Issues or Issue Description

Refs #12769
Refs #12828
Refs #12829
Refs #12833

**What happened?**

The structured-question restart test could time out after the
replacement server returned the task route and rendered the durable
pending interaction. A focused one-cell rerun passed the paid test but
failed aggregation because download-artifact v8 flattened its single
artifact.

**Expected behavior**

The test must prove that a new document loaded after the server restart
and that the same pending interaction survived. The aggregate must
accept the exact documented singleton download layout while it continues
to reject ambiguous or foreign artifacts.

**Steps to reproduce**

1. Run the local ACPX-Codex structured-question restart-resume cell.
2. Restart the isolated server while the question waits for an answer.
3. Observe that the route and task UI can reload before Playwright
settles the navigation promise.
4. Run a paid campaign with one selected cell.
5. Observe download-artifact v8 extract the sole campaign directory
directly into the requested path.

**Paperclip version or commit**

The local campaign reproduced the navigation failure at
`3586956a1b794b3cb4a9c5f57ffb7355e2b0c46d`. The one-cell aggregate
reproduced the singleton layout at
`f487660c0a06ba06ca140b57386f21ed39f13120`. This fix is
`de4ccceff453a4b39436bf9a2eb8f03924151af7`.

**Deployment mode**

Local development and paid GitHub Actions.

**Installation method**

Built from source.

**Agent adapter(s) involved**

ACPX-Codex. The Vite and aggregate fixes are provider-neutral.

## What Changed

- Prove a new post-restart browser document with an in-memory sentinel.
- Tolerate only Playwright's navigation timeout before the exact UI and
API checks run.
- Honor `PAPERCLIP_VITE_CACHE_DIR` in the embedded Vite server.
- Limit dependency optimization to the real UI entry.
- Run `vite.transformIndexHtml` for each request while caching only the
branded source template.
- Accept download-artifact v8's flattened layout only for one expected
cell with one unique recognized campaign.
- Keep source SHA, source ref, workflow URL, execution ID, attempt, and
unexpected-entry validation.
- Add focused positive and negative regressions for Vite rendering and
singleton artifact selection.

## Verification

- Exact 45-cell local campaign
https://github.com/paperclipai/paperclip/actions/runs/33888939013 passed
44/45. Its only failure was the post-restart navigation false negative
fixed here.
- Exact focused rerun
https://github.com/paperclipai/paperclip/actions/runs/33891207957 passed
the ACPX-Codex restart cell first attempt with the same session, two
durable runs, the terminal marker once, and cleanup complete.
- The focused Vite renderer suite passed 2/2 tests.
- The focused rerun-artifact selector suite passed 12/12 tests.
- Prettier and `git diff --check` passed.
- An exact-head 45-cell confirmation is pending.

## Risks

Low to medium risk. The Vite change restores known-green per-request
transforms and isolated cache behavior. It can affect all development UI
loads. The paid matrix and ordinary CI will verify that behavior. The
singleton selector remains fail-closed for ambiguous layouts and
validates every result source.

## Model Used

OpenAI Codex, `gpt-5.6-sol`, extended reasoning, tool use, code
execution, and parallel focused agents.

## 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
- [ ] 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
2026-09-04 11:16:20 -05:00

444 lines
12 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { selectRerunArtifacts } from "./select-rerun-artifacts.js";
import type { RunnerE2EResult } from "./types.js";
const RUN_ID = "33843305626";
const SOURCE_SHA = "0123456789abcdef0123456789abcdef01234567";
const SOURCE_REF = "refs/heads/fix/runner-paid-matrix-integrity-v2";
const WORKFLOW_RUN_URL =
"https://github.com/paperclipai/paperclip/actions/runs/33843305626";
const RETAINED = "core-compatibility.legacy-codex.local.message-marker";
const RERUN = "core-compatibility.runner-codex.local.plan-revise-accept";
const cleanupDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
cleanupDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
function result(executionId: string, status: "passed" | "failed") {
const [suiteId, profileId, environmentId, caseId] = executionId.split(".");
return {
schema: "paperclip.runner-e2e.result/v2",
executionId,
suiteId,
source: {
sha: SOURCE_SHA,
ref: SOURCE_REF,
workflowRunUrl: WORKFLOW_RUN_URL,
},
attempt: 1,
status,
...(status === "failed"
? { failureClass: "candidate_failure" as const, error: "failed" }
: {}),
profileId: profileId!,
environmentId: environmentId as RunnerE2EResult["environmentId"],
caseId: caseId!,
provider: "codex",
model: "fixture-model",
runtimeMode: "native",
startedAt: "2026-09-04T00:00:00.000Z",
finishedAt: "2026-09-04T00:00:01.000Z",
durationMs: 1_000,
cleanup: status === "passed" ? "passed" : "not_started",
} satisfies RunnerE2EResult;
}
async function addArtifact(input: {
root: string;
executionId: string;
workflowAttempt: number;
status: "passed" | "failed";
sourceSha?: string;
resultExecutionId?: string;
campaignName?: string;
flattened?: boolean;
}) {
const artifactName = `runner-e2e-${RUN_ID}-${input.workflowAttempt}-${input.executionId}`;
const campaignName =
input.campaignName ??
`gha-${RUN_ID}-${input.workflowAttempt}-${input.executionId}`;
const directory = path.join(
input.root,
...(input.flattened ? [] : [artifactName]),
campaignName,
"results",
"attempt-1",
);
await mkdir(directory, { recursive: true });
const value = result(
input.resultExecutionId ?? input.executionId,
input.status,
);
await writeFile(
path.join(directory, "result.json"),
JSON.stringify({
...value,
source: { ...value.source, sha: input.sourceSha ?? value.source.sha },
}),
);
return { artifactName, campaignName };
}
async function fixture() {
const root = await mkdtemp(
path.join(os.tmpdir(), "runner-e2e-rerun-artifacts-"),
);
cleanupDirectories.push(root);
return {
root,
artifactRoot: path.join(root, "downloaded"),
selectedRoot: path.join(root, "selected"),
};
}
function selectionInput(paths: Awaited<ReturnType<typeof fixture>>) {
return {
...paths,
jobs: {
jobs: [
{
name: RETAINED,
run_attempt: 1,
started_at: "2026-09-04T00:00:01.000Z",
},
{
name: RERUN,
run_attempt: 1,
started_at: "2026-09-04T00:00:02.000Z",
},
// filter=all synthesizes this attempt-2 row even though GitHub retained
// the successful attempt-1 job. Its original start time exposes it.
{
name: RETAINED,
run_attempt: 2,
started_at: "2026-09-04T00:00:01.000Z",
},
{
name: RERUN,
run_attempt: 2,
started_at: "2026-09-04T01:00:01.000Z",
},
],
attempts: [
{
run_attempt: 1,
run_started_at: "2026-09-04T00:00:00.000Z",
},
{
run_attempt: 2,
run_started_at: "2026-09-04T01:00:00.000Z",
},
],
},
expectedExecutionIds: [RETAINED, RERUN],
workflowRunId: RUN_ID,
workflowRunAttempt: 2,
sourceSha: SOURCE_SHA,
sourceRef: SOURCE_REF,
workflowRunUrl: WORKFLOW_RUN_URL,
};
}
function singletonSelectionInput(paths: Awaited<ReturnType<typeof fixture>>) {
const input = selectionInput(paths);
return {
...input,
jobs: {
...input.jobs,
jobs: input.jobs.jobs.filter((job) => job.name === RERUN),
},
expectedExecutionIds: [RERUN],
};
}
describe("runner E2E workflow rerun artifact selection", () => {
it("accepts the v8 flattened layout for one expected artifact", async () => {
const paths = await fixture();
const latest = await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
flattened: true,
});
const selected = await selectRerunArtifacts(singletonSelectionInput(paths));
expect(selected).toEqual([
{
executionId: RERUN,
workflowAttempt: 2,
artifactName: latest.artifactName,
},
]);
const selectedResult = JSON.parse(
await readFile(
path.join(
paths.selectedRoot,
latest.artifactName,
latest.campaignName,
"results",
"attempt-1",
"result.json",
),
"utf8",
),
);
expect(selectedResult.status).toBe("passed");
});
it("does not let an older flattened campaign mask a latest missing artifact", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 1,
status: "passed",
flattened: true,
});
const selected = await selectRerunArtifacts(singletonSelectionInput(paths));
expect(selected).toEqual([]);
});
it("rejects a flattened campaign when multiple artifacts are expected", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
flattened: true,
});
await expect(selectRerunArtifacts(selectionInput(paths))).rejects.toThrow(
/downloaded unexpected runner artifact/u,
);
});
it("rejects a flattened campaign beside another root entry", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
flattened: true,
});
await writeFile(path.join(paths.artifactRoot, "unexpected.txt"), "no");
await expect(
selectRerunArtifacts(singletonSelectionInput(paths)),
).rejects.toThrow(/downloaded unexpected runner artifact/u);
});
it("rejects an unrecognized flattened campaign", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
campaignName: `gha-another-run-2-${RERUN}`,
flattened: true,
});
await expect(
selectRerunArtifacts(singletonSelectionInput(paths)),
).rejects.toThrow(/downloaded unexpected runner artifact/u);
});
it("applies source validation to a flattened campaign", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
sourceSha: "ffffffffffffffffffffffffffffffffffffffff",
flattened: true,
});
await expect(
selectRerunArtifacts(singletonSelectionInput(paths)),
).rejects.toThrow("contains result from another source");
});
it("applies execution validation to a flattened campaign", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
resultExecutionId: RETAINED,
workflowAttempt: 2,
status: "passed",
flattened: true,
});
await expect(
selectRerunArtifacts(singletonSelectionInput(paths)),
).rejects.toThrow(`contains result for ${RETAINED}`);
});
it("combines retained successes with the latest rerun artifact", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RETAINED,
workflowAttempt: 1,
status: "passed",
});
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 1,
status: "failed",
});
const latest = await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
});
const selected = await selectRerunArtifacts(selectionInput(paths));
expect(selected).toEqual([
{
executionId: RETAINED,
workflowAttempt: 1,
artifactName: `runner-e2e-${RUN_ID}-1-${RETAINED}`,
},
{
executionId: RERUN,
workflowAttempt: 2,
artifactName: latest.artifactName,
},
]);
const selectedResult = JSON.parse(
await readFile(
path.join(
paths.selectedRoot,
latest.artifactName,
latest.campaignName,
"results",
"attempt-1",
"result.json",
),
"utf8",
),
);
expect(selectedResult.status).toBe("passed");
});
it("never falls back when the latest workflow attempt has no artifact", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RETAINED,
workflowAttempt: 1,
status: "passed",
});
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 1,
status: "passed",
});
const selected = await selectRerunArtifacts(selectionInput(paths));
expect(selected.map((entry) => entry.executionId)).toEqual([RETAINED]);
});
it("does not let an older pass mask a latest failed artifact", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RETAINED,
workflowAttempt: 1,
status: "passed",
});
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 1,
status: "passed",
});
const latest = await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "failed",
});
await selectRerunArtifacts(selectionInput(paths));
const selectedResult = JSON.parse(
await readFile(
path.join(
paths.selectedRoot,
latest.artifactName,
latest.campaignName,
"results",
"attempt-1",
"result.json",
),
"utf8",
),
);
expect(selectedResult.status).toBe("failed");
});
it("rejects artifacts from another source", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RETAINED,
workflowAttempt: 1,
status: "passed",
sourceSha: "ffffffffffffffffffffffffffffffffffffffff",
});
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
});
await expect(selectRerunArtifacts(selectionInput(paths))).rejects.toThrow(
"contains result from another source",
);
});
it("rejects an artifact carrying another campaign", async () => {
const paths = await fixture();
await addArtifact({
root: paths.artifactRoot,
executionId: RETAINED,
workflowAttempt: 1,
status: "passed",
});
await addArtifact({
root: paths.artifactRoot,
executionId: RERUN,
workflowAttempt: 2,
status: "passed",
campaignName: `gha-another-run-2-${RERUN}`,
});
await expect(selectRerunArtifacts(selectionInput(paths))).rejects.toThrow(
/must contain only its exact campaign/u,
);
});
});