fix: retain warm sandbox bindings independently of worktree settings

Validate live engine coverage and physical sandbox reuse in deployed acceptance.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-07 14:56:30 -05:00
1 parent a5d7df55f6
commit e8a6fb02bf
7 files changed
+116 -8

No files matched your search

+11 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isStagingOrigin, assertDeployedAdapterExclusions } from "./deployed-stack.js";
import { isStagingOrigin, assertDeployedAdapterExclusions, deployedAgentEngine } from "./deployed-stack.js";
describe("deployed stack target", () => {
it("requires an explicit HTTPS staging tenant and rejects credential-bearing URLs", () => {
@@ -11,6 +11,16 @@ describe("deployed stack target", () => {
});
});
it("derives engine coverage from live adapter configuration", () => {
expect(deployedAgentEngine({ adapterType: "codex_local", adapterConfig: {} })).toBe("cli");
expect(deployedAgentEngine({ adapterType: "claude_local", adapterConfig: { engine: "acp" } })).toBe("acp");
expect(deployedAgentEngine({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } })).toBe("codex");
expect(deployedAgentEngine({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "pi" } })).toBe("acpx:pi");
expect(deployedAgentEngine({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude" } })).not.toBe("acpx:pi");
expect(() => deployedAgentEngine({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx" } })).toThrow("Missing");
expect(() => deployedAgentEngine({ adapterType: "codex_local", adapterConfig: { engine: "unknown" } })).toThrow("Unknown");
});
it("allows only the four explicitly deferred adapters to be excluded", () => {
expect(() => assertDeployedAdapterExclusions([{ adapterType: "cursor", reason: "Explicitly deferred by the user" }])).not.toThrow();
for (const adapterType of ["codex_local", "claude_local", "opencode_local", "pi_local", "paperclip_runner"]) {
+13
View File
@@ -16,6 +16,19 @@ export interface DeployedStack {
profiles: Array<{ id: string; adapterType: string; engine: string; model: string; qualification: string; agentId: string }>;
}
/** Derive coverage from the live configuration, never a manifest's label. */
export function deployedAgentEngine(agent: { adapterType: string; adapterConfig: Record<string, unknown> }): string {
const config = agent.adapterConfig;
if (agent.adapterType === "paperclip_runner") {
assert(config.provider === "codex" || config.provider === "opencode" || config.provider === "acpx", "Unknown native provider");
if (config.provider !== "acpx") return config.provider;
assert(typeof config.acpxAgent === "string" && config.acpxAgent.length > 0, "Missing native ACPX engine");
return `acpx:${config.acpxAgent}`;
}
assert(config.engine === undefined || config.engine === "cli" || config.engine === "acp", "Unknown legacy engine");
return config.engine === "acp" ? "acp" : "cli";
}
export function loadDeployedStack(): DeployedStack {
const filename = process.env.PAPERCLIP_DEPLOYED_STACK_MANIFEST;
if (!filename) throw new Error("PAPERCLIP_DEPLOYED_STACK_MANIFEST is required");
+18 -5
View File
@@ -1,10 +1,10 @@
import { randomUUID } from "node:crypto";
import { test, expect } from "@playwright/test";
import type { EnvironmentCapabilities } from "../../packages/shared/src/environment-support.js";
import type { WorkFolderListing, WorkFolderSyncStatus } from "../../packages/shared/src/work-folders.js";
import type { SandboxWorkFolderManifest, WorkFolderListing, WorkFolderSyncStatus } from "../../packages/shared/src/work-folders.js";
import { QUALIFIED_ACPX_PROFILES } from "../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
import { pollUntil } from "./api.js";
import { DeployedStackApi, loadDeployedStack } from "./deployed-stack.js";
import { DeployedStackApi, deployedAgentEngine, loadDeployedStack } from "./deployed-stack.js";
const stack = loadDeployedStack();
const api = new DeployedStackApi(stack);
@@ -32,13 +32,16 @@ test("deployed candidate and complete supported adapter inventory", async ({}, i
if (adapter.capabilities.supportsAcp) required.push(`${adapter.type}:acp`);
}
}
const configured = new Set(stack.profiles.map((profile) => `${profile.adapterType}:${profile.engine}`));
expect(required.filter((key) => !configured.has(key)), "Every exposed sandbox adapter/engine requires a qualified fixture").toEqual([]);
const configured = new Set<string>();
for (const profile of stack.profiles) {
const agent = await api.json<{ adapterType: string; adapterConfig: Record<string, unknown> }>(`/api/agents/${profile.agentId}`);
expect(agent.adapterType).toBe(profile.adapterType);
expect(agent.adapterConfig.model).toBe(profile.model);
const engine = deployedAgentEngine(agent);
expect(engine, `${profile.id} must exercise its declared live engine`).toBe(profile.engine);
configured.add(`${agent.adapterType}:${engine}`);
}
expect(required.filter((key) => !configured.has(key)), "Every exposed sandbox adapter/engine requires a qualified fixture").toEqual([]);
await info.attach("deployed-candidate-and-inventory", { contentType: "application/json", body: Buffer.from(JSON.stringify({ stack, required }, null, 2)) });
});
@@ -83,6 +86,7 @@ for (const profile of stack.profiles) {
"In each repo write 'staged' without newline to .acceptance-state, git add ONLY that file, then replace its working-tree content with 'unstaged' without newline. Write 'untracked' without newline to .acceptance-untracked and leave it untracked.",
"If .acceptance-setup-count exists, assert it has exactly one line. Never run setup yourself.",
`Write exactly '${nonce}' without a newline into $HOME/task/acceptance.txt and $HOME/agent/acceptance-${nonce}.txt.`,
`Also write exactly '${nonce}' to $HOME/.cache/warm-${nonce}; this disposable cache marker must survive an actual warm reuse.`,
"Then complete this task successfully. Do not print credentials or modify unrelated files.",
].join("\n"),
});
@@ -99,10 +103,15 @@ for (const profile of stack.profiles) {
const content = await api.request(`${base}/content?path=acceptance.txt`);
expect(content.status).toBe(200); expect(await content.text()).toBe(nonce);
const coldRunIds = new Set(cold.saves.map((save) => save.runId));
const coldSave = cold.saves.find((save) => !save.active && save.state === "saved")!;
const coldRun = await api.json<{ contextSnapshot: { paperclipWorkFolders: SandboxWorkFolderManifest } }>(`/api/heartbeat-runs/${coldSave.runId}`);
const coldManifest = coldRun.contextSnapshot.paperclipWorkFolders;
expect(coldManifest.sandboxKey).toBeTruthy();
await api.json(`/api/issues/${issue.id}`, "PATCH", { status: "todo", description: [
"Continue this sandbox acceptance task. This is a warm run; inspect the existing work without repairing it.",
"Verify cwd equals HOME and all seven directories still exist.",
`Assert $HOME/task/acceptance.txt and every repo's committed HEAD:.acceptance-owner equal '${nonce}'.`,
`Assert $HOME/.cache/warm-${nonce} still contains exactly '${nonce}'. A replacement is not a warm pass; do not recreate the marker.`,
"For each repo assert HEAD equals the saved task/head-<repo-directory-name>.txt, index :.acceptance-state equals 'staged', working .acceptance-state equals 'unstaged', and .acceptance-untracked equals 'untracked' and remains untracked.",
"If .acceptance-setup-count exists, assert exactly one line. Fail with the actual discrepancy; do not recreate missing state or rerun setup.",
`Write exactly '${nonce}' without a newline to $HOME/task/warm.txt, then complete the task.`,
@@ -117,7 +126,11 @@ for (const profile of stack.profiles) {
});
const warmContent = await api.request(`${base}/content?path=warm.txt`);
expect(warmContent.status).toBe(200); expect(await warmContent.text()).toBe(nonce);
await info.attach("cold-and-warm-checkpoints", { contentType: "application/json", body: Buffer.from(JSON.stringify({ cold: cold.saves, warm: warm.saves })) });
const warmSave = warm.saves.find((save) => !save.active && save.state === "saved")!;
const warmRun = await api.json<{ contextSnapshot: { paperclipWorkFolders: SandboxWorkFolderManifest } }>(`/api/heartbeat-runs/${warmSave.runId}`);
const warmManifest = warmRun.contextSnapshot.paperclipWorkFolders;
expect(warmManifest.sandboxKey, "Warm acceptance requires the same physical sandbox").toBe(coldManifest.sandboxKey);
await info.attach("cold-and-warm-checkpoints", { contentType: "application/json", body: Buffer.from(JSON.stringify({ cold: cold.saves, warm: warm.saves, coldManifest, warmManifest })) });
});
}