Merge adapter startup accounting proof and test isolation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-10-03 23:21:17 -07:00
commit 1dd6c03d2c
3 files changed
+75 -14

No files matched your search

@@ -176,3 +176,32 @@ describe("incremental protocol accounting", () => {
expect(second.usage).toMatchObject(Object.fromEntries(Object.entries(fixture.tokens!).map(([key, count]) => [key, count * multiplier])));
});
});
describe("process adapter bootstrap accounting proof", () => {
const context = (config: Record<string, unknown>): AdapterExecutionContext => ({
runId: `bootstrap-${crypto.randomUUID()}`,
agent: { id: "agent", companyId: "company", name: "Process", adapterType: "process", adapterConfig: {} },
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
config, context: {}, onLog: async () => {},
});
it.each([{}, { command: "/nonexistent-paperclip-accounting-command" },
{ command: process.execPath, cwd: "/nonexistent-paperclip-accounting-directory" }])(
"proves no provider work for a failure before spawn: %j", async (config) => {
const { execute } = await import("../adapters/process/execute.js");
const onSpawn = vi.fn();
const result = await execute({ ...context(config), onSpawn });
expect(result).toMatchObject({ exitCode: 1, executionRecovery: { kind: "bootstrap", providerWorkStarted: false } });
expect(result.errorMessage).toBeTruthy();
expect(onSpawn).not.toHaveBeenCalled();
},
);
it("does not invent bootstrap proof when a spawned process fails", async () => {
const { execute } = await import("../adapters/process/execute.js");
const onSpawn = vi.fn(async () => { throw new Error("Metadata persistence failed"); });
const result = await execute({ ...context({ command: process.execPath, args: ["-e", "process.exit(7)"] }), onSpawn });
expect(onSpawn).toHaveBeenCalledOnce();
expect(result.exitCode).toBe(7);
expect(result.executionRecovery).toBeUndefined();
});
});
+28 -14
View File
@@ -43,6 +43,7 @@ import {
releaseRuntimeServicesForRun,
UnresolvedWorkspaceBaseRefError,
resetRuntimeServicesForTests,
resetRuntimeServicePortReservationsForTests,
MANAGED_RUNTIME_PUBLIC_URL_ENV,
resolveManagedPaperclipRuntimePublicOrigin,
resolveRuntimeProvisionCommand,
@@ -454,6 +455,9 @@ afterEach(async () => {
delete process.env.PAPERCLIP_WORKTREES_DIR;
delete process.env.DATABASE_URL;
await resetRuntimeServicesForTests();
// Registry reset does not clear the process-local allocation claims. A
// failed-start fixture must not reserve another test's ephemeral port.
resetRuntimeServicePortReservationsForTests();
});
describe("sanitizeRuntimeServiceBaseEnv", () => {
@@ -4099,6 +4103,7 @@ describe("realizeExecutionWorkspace", () => {
"utf8",
);
process.env.PAPERCLIP_WORKTREES_DIR = worktreesDir;
const canonicalInstanceRoot = await fs.realpath(instanceRoot);
await cleanupExecutionWorkspaceArtifacts({
workspace: {
@@ -4133,7 +4138,7 @@ describe("realizeExecutionWorkspace", () => {
expect(operations[0]?.command).toBe("printf 'cleanup ok\\n'");
expect(operations[1]?.metadata).toMatchObject({
cleanupAction: "remove_worktree_instance",
instanceRoot,
instanceRoot: canonicalInstanceRoot,
});
expect(operations[2]?.metadata).toMatchObject({
cleanupAction: "worktree_remove",
@@ -7643,16 +7648,25 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => {
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = `runtime-desired-reconcile-${randomUUID()}`;
const reservePort = async () => {
const probe = net.createServer();
await new Promise<void>((resolve) => probe.listen(0, "127.0.0.1", resolve));
const address = probe.address();
const port = typeof address === "object" && address ? address.port : null;
await new Promise<void>((resolve, reject) => {
probe.close((error) => error ? reject(error) : resolve());
});
if (!port) throw new Error("Failed to reserve runtime reconciliation test port");
return port;
const reservePorts = async () => {
const probes = [net.createServer(), net.createServer()];
try {
// Keep both sockets open until both allocations finish. Closing the
// first probe early lets the kernel return that same port again.
for (const probe of probes) await new Promise<void>((resolve, reject) => {
probe.once("error", reject);
probe.listen(0, "127.0.0.1", resolve);
});
return probes.map(probe => {
const address = probe.address();
if (!address || typeof address !== "object") throw new Error("Failed to reserve runtime reconciliation test port");
return address.port;
});
} finally {
await Promise.all(probes.filter(probe => probe.listening).map(probe => new Promise<void>((resolve, reject) => {
probe.close(error => error ? reject(error) : resolve());
})));
}
};
const isLoopbackPortFree = async (port: number) => {
const probe = net.createServer();
@@ -7673,8 +7687,8 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => {
}
throw new Error(`Port ${port} did not become free in time`);
};
const stoppedPort = await reservePort();
const livePort = await reservePort();
const [stoppedPort, livePort] = await reservePorts();
expect(stoppedPort).not.toBe(livePort);
const companyId = randomUUID();
const projectId = randomUUID();
const projectWorkspaceId = randomUUID();
@@ -9728,7 +9742,7 @@ describe("realizeExecutionWorkspace with an exact existing branch", () => {
const workspace = await realizeExistingBranch(repoRoot, "feature/legacy-checkout");
expect(workspace.cwd).toBe(path.resolve(legacyPath));
expect(workspace.cwd).toBe(await fs.realpath(legacyPath));
expect(workspace.branchName).toBe("feature/legacy-checkout");
expect(workspace.created).toBe(false);
expect(await readGit(workspace.cwd, ["rev-parse", "HEAD"])).toBe(branchTip);
+18
View File
@@ -15,6 +15,24 @@ import {
} from "../utils.js";
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
let spawned = false;
try {
return await executeProcess({ ...ctx, onSpawn: async (meta) => {
// Set this before metadata persistence: a failed callback cannot prove
// that the already-created process did no billable work.
spawned = true;
await ctx.onSpawn?.(meta);
} });
} catch (error) {
if (spawned) throw error;
return { exitCode: 1, signal: null, timedOut: false,
errorMessage: error instanceof Error ? error.message : String(error),
executionRecovery: { kind: "bootstrap", providerWorkStarted: false },
};
}
}
async function executeProcess(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const { runId, agent, config, onLog, onMeta, authToken } = ctx;
const command = asString(config.command, "");
if (!command) throw new Error("Process adapter missing command");