Files
PaperClipAI/tests/runner-e2e/task-workspaces.test.ts
T
DottaandPaperclip 58bcb04bfd fix: preserve workspace ownership through project changes and recovery
Keep project edits organizational, validate immutable native task roots before materialization, align authorization fixtures, and correct live E2E evidence collection.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-09 21:19:26 -05:00

93 lines
6.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { runnerMatrix } from "./catalog.js";
import { gradeTaskWorkspaces, isRepositoryPreparationReceipt, type TaskWorkspaceObservation } from "./task-workspaces-scoring.js";
import { TASK_WORKSPACES_SUITE, taskWorkspaceFiles, taskWorkspacePrompt } from "./task-workspaces-cases.js";
function valid(): TaskWorkspaceObservation {
const nonce = "fixture", cwd = "/isolated/task", pin = "a".repeat(40), head = "b".repeat(40);
return { nonce, native: true, daytona: true, initial: { workspaceId: "workspace", cwd, repositories: 0 },
checkpoints: [1, 2, 3].map(turn => ({ turn,
issue: { id: "issue", projectId: null, executionWorkspaceId: "workspace", status: "done" },
workspace: { id: "workspace", projectId: null, cwd }, confinedTaskRoot: true,
note: `task-files-turn-${turn}-${nonce}\n`,
repositories: [{ id: "repo", state: turn === 1 ? "pending" : "ready", relativePath: `.paperclip-repositories/task-repo-${"a".repeat(24)}`,
pinnedCommit: turn === 1 ? null : pin, requestedByIssueId: "issue", requestKeys: [`repository-${nonce}`] }],
run: { id: `run-${turn}`, status: "succeeded", runtimeMode: "native", nativePhase: "committed", nativeBinding: "workspace",
nativeCwd: "/workspace/task", transport: "provider_ingress", authenticated: true, nativeCompleted: true },
...(turn > 1 ? { repository: { head, baseIsAncestor: true, dirty: true, content: `repository-dirty-${nonce}\n` } } : {}),
prepareCalls: turn === 1 ? 2 : 0,
})), projectCount: 0, runCount: 3, restarted: true, pendingInteractions: 0, activeOperations: 0, activeRecovery: false,
artifact: { content: `repository-dirty-${nonce}\n`, contentVerified: true, mimeType: "text/plain" },
remoteLeaseRunIds: ["run-1", "run-2", "run-3"] };
}
describe("task workspace product E2E", () => {
it("declares exactly the three authorized cells with no project/cwd and one attempt", () => {
const cells = runnerMatrix.filter(cell => cell.suite.id === TASK_WORKSPACES_SUITE);
expect(cells.map(cell => cell.id).sort()).toEqual([
"task-workspaces.legacy-opencode.local.task-directory-repository-resume",
"task-workspaces.runner-opencode.daytona.task-directory-repository-resume",
"task-workspaces.runner-opencode.local.task-directory-repository-resume",
]);
for (const cell of cells) {
expect(cell.suite.manualOnly).toBe(true);
expect(cell.task.automaticRetryPolicy).toBe("single_attempt");
expect(cell.task.expectedRunCount).toBe(3);
const agent = cell.profile.buildAgent({ environmentId: "environment", environmentFixtureId: cell.environment.id,
workspacePath: "/must-not-use", executionId: "fixture", secretRefs: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "secret", version: "latest" } } });
expect(agent.budgetMonthlyCents).toBe(1000);
expect((agent.adapterConfig as Record<string, unknown>).cwd).toBeUndefined();
}
});
it("keeps native tools and legacy authenticated API prompts distinct", () => {
expect(taskWorkspacePrompt("fixture", 1, true)).toContain("Call prepare_repository twice");
expect(taskWorkspacePrompt("fixture", 1, false)).toContain("POST /api/issues/$PAPERCLIP_TASK_ID/workspace/repositories twice");
expect(taskWorkspacePrompt("fixture", 3, true)).not.toContain(taskWorkspaceFiles("fixture").repositoryBytes.trim());
});
it("recognizes the normalized provider receipt and rejects claims from another source/run", () => {
const receipt = { eventType: "tool.execution.completed", payload: { prpEvent: {
sourceKind: "runner", runId: "run-1", payload: { name: "paperclip_prepare_repository", status: "completed" },
} } };
expect(isRepositoryPreparationReceipt(receipt, "run-1")).toBe(true);
expect(isRepositoryPreparationReceipt(receipt, "another-run")).toBe(false);
receipt.payload.prpEvent.sourceKind = "control_plane";
expect(isRepositoryPreparationReceipt(receipt, "run-1")).toBe(false);
receipt.payload.prpEvent.sourceKind = "runner";
receipt.payload.prpEvent.payload.status = "failed";
expect(isRepositoryPreparationReceipt(receipt, "run-1")).toBe(false);
});
it("accepts independently consistent persisted observations", () => {
expect(gradeTaskWorkspaces(valid()).filter(check => !check.passed)).toEqual([]);
const legacy = valid(); legacy.native = false; legacy.daytona = false;
legacy.checkpoints.forEach(point => { point.run.runtimeMode = "legacy"; delete point.prepareCalls; });
expect(gradeTaskWorkspaces(legacy).filter(check => !check.passed)).toEqual([]);
});
it("accepts a pending receipt queued before the first browser observation", () => {
const observation = valid();
observation.initial!.repositories = 1;
expect(gradeTaskWorkspaces(observation).filter(check => !check.passed)).toEqual([]);
observation.checkpoints[0]!.repositories[0]!.state = "ready";
expect(gradeTaskWorkspaces(observation).find(check => check.id === "deferred-idempotent-acquisition")?.passed).toBe(false);
});
it.each([
["missing evidence", (v: TaskWorkspaceObservation) => { v.checkpoints = []; }],
["agent home", (v: TaskWorkspaceObservation) => { v.checkpoints[0]!.confinedTaskRoot = false; }],
["new project", (v: TaskWorkspaceObservation) => { v.projectCount = 1; }],
["root switched", (v: TaskWorkspaceObservation) => { v.checkpoints[1]!.workspace.cwd = "/different"; }],
["early clone", (v: TaskWorkspaceObservation) => { v.checkpoints[0]!.repositories[0]!.state = "ready"; }],
["duplicate receipt", (v: TaskWorkspaceObservation) => { v.checkpoints[1]!.repositories.push(v.checkpoints[1]!.repositories[0]!); }],
["different pin", (v: TaskWorkspaceObservation) => { v.checkpoints[2]!.repositories[0]!.pinnedCommit = "c".repeat(40); }],
["lost Git commit", (v: TaskWorkspaceObservation) => { v.checkpoints[2]!.repository!.head = "a".repeat(40); }],
["lost dirty bytes", (v: TaskWorkspaceObservation) => { v.checkpoints[2]!.repository!.content = "reset"; }],
["missing download", (v: TaskWorkspaceObservation) => { v.artifact = null; }],
["unverified download", (v: TaskWorkspaceObservation) => { v.artifact!.contentVerified = false; }],
["extra wake", (v: TaskWorkspaceObservation) => { v.runCount = 4; }],
["unfinished persistence", (v: TaskWorkspaceObservation) => { v.activeOperations = 1; }],
["wrong native binding", (v: TaskWorkspaceObservation) => { v.checkpoints[2]!.run.nativeBinding = "another"; }],
["tool bypass", (v: TaskWorkspaceObservation) => { v.checkpoints[0]!.prepareCalls = 0; }],
["missing remote lease", (v: TaskWorkspaceObservation) => { v.remoteLeaseRunIds = ["run-1"]; }],
] as const)("rejects %s instead of accepting model claims", (_label, mutate) => {
const observation = valid(); mutate(observation);
expect(gradeTaskWorkspaces(observation).some(check => !check.passed)).toBe(true);
});
});