mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.
Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
93 lines
6.8 KiB
TypeScript
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);
|
|
});
|
|
});
|