mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 01:50:13 +01:00
Release and resume retained plugin Runner sessions in Core (#19611)
This commit is contained in:
1 parent
9efee7ee16
commit
b21884bdcf
7 files changed
+965
-80
No files matched your search
@@ -6,6 +6,7 @@ import * as harnessStorage from "./native-harness-backup-storage.js";
|
||||
import * as stagedAttachmentRecovery from "./native-staged-attachment-recovery.js";
|
||||
import * as controllerRecovery from "./native-controller-recovery.js";
|
||||
import { isSettledRemoteWarmControlState } from "./remote-warm-session-recovery.js";
|
||||
import * as remoteWarmRecoveryRuntime from "./remote-warm-session-recovery.js";
|
||||
import { DurablePrpControlPlane } from "../../vendor/paperclip-runner/index.js";
|
||||
import * as environmentTaskRuntime from "../environment-task-runtime.js";
|
||||
import * as pluginTaskRuntime from "./plugin-runner-task.js";
|
||||
@@ -171,7 +172,7 @@ type BackendFactoryOptions = {
|
||||
type RunnerTransportOptions = {
|
||||
authorityRunId?: string;
|
||||
onAccountingEvent?: (event: PrpEvent) => Promise<void>;
|
||||
onRecoveryAccountingEvent?: unknown;
|
||||
onRecoveryAccountingEvent?: (event: PrpEvent, identity: import("../../vendor/paperclip-runner/index.js").DurableRecoveryIdentity) => Promise<void>;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
acpxRuntimeDirectory?: string;
|
||||
adoptExistingRunner?: { pid: number; isAlive: () => Promise<boolean> | boolean };
|
||||
@@ -8381,6 +8382,13 @@ describe("native terminal-turn accounting", () => {
|
||||
expect(firstCapture).toHaveBeenCalledTimes(2);
|
||||
expect(secondCapture).toHaveBeenCalledTimes(1);
|
||||
await expect(retainedHook({ ...firstReport, runId: "unrelated-run" })).rejects.toThrow("native_usage_run_binding_mismatch");
|
||||
const recoveryHook = state.createTransport.mock.calls.at(-1)![0].onRecoveryAccountingEvent!;
|
||||
const source = { runnerInstanceId: "warm-producer", environmentLeaseId: "warm-usage-workspace",
|
||||
runId: first.binding.runId, normalizedSessionId: first.session.normalizedSessionId!, turnId: "source-turn", itemId: "source-item" };
|
||||
await recoveryHook(firstReport, source);
|
||||
expect(firstCapture).toHaveBeenCalledTimes(3);
|
||||
expect(secondCapture).toHaveBeenCalledTimes(1);
|
||||
await expect(recoveryHook({ ...firstReport, runId: "foreign-source" }, source)).rejects.toThrow("native_recovery_usage_source_unproven");
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.PAPERCLIP_RUNNER_STATE_DIR; else process.env.PAPERCLIP_RUNNER_STATE_DIR = previous;
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
@@ -15857,6 +15865,318 @@ describe("runnerd provider runtime wiring", () => {
|
||||
} finally {owner.mockRestore();await rm(home,{recursive:true,force:true});}
|
||||
});
|
||||
|
||||
it.each((["codex", "claude"] as const).flatMap(provider => [[false, false, false, false], [true, false, false, false], [false, true, false, false], [false, false, true, false], ...(provider === "codex" ? [[false, false, false, true]] : [])].map(([unknown, shutdown, journalFailure, deadline]) => [provider, unknown, shutdown, journalFailure, deadline] as const)))(
|
||||
"releases settled %s only under its current owner and detaches its cache locally (unknown=%s, shutdown=%s, journalFailure=%s, deadline=%s)", async (provider, unknown, shutdown, journalFailure, deadline) => {
|
||||
await closeIdleWarmNativeSessionsForRestart();
|
||||
expect(readNativeSessionIdleWork()).toBe("none");
|
||||
const previousHome = process.env.PAPERCLIP_HOME;
|
||||
process.env.PAPERCLIP_HOME = isolatedStateDirectory;
|
||||
const current = scopedNativeExecution(`released-${provider}-${unknown}`);
|
||||
current.session.lifecyclePolicy = { mode: "warm", idleTimeoutMs: unknown ? 750 : 250 };
|
||||
if (provider === "claude") current.provider = { kind: "acpx", agent: "claude", model: "fixture", permissionMode: "deny-all" } as never;
|
||||
const source = { runnerInstanceId: "released-runner", environmentLeaseId: "released-lease", runId: current.binding.runId,
|
||||
normalizedSessionId: current.session.normalizedSessionId!, turnId: "settled-turn", itemId: "settled-item" };
|
||||
const root = await initializeScopedRunnerRoot(current, source.runnerInstanceId);
|
||||
await writeRunnerAuthority(root, source, "ready");
|
||||
const home = join(isolatedStateDirectory, "selected-account");
|
||||
await mkdir(home, { recursive: true });
|
||||
await writeFile(join(home, "auth.json"), JSON.stringify({ OPENAI_API_KEY: "synthetic-fixture" }), { mode: 0o600 });
|
||||
const task = vi.fn(async (_operation: unknown) => ({ kind: "description", taskId: "retained-task",
|
||||
launchPreparationVersion: 1, projectAuthCheckVersion: 1, warmTaskVersion: 1, releaseResumeVersion: 1,
|
||||
runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } }));
|
||||
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd: ".", environmentId: "retained-environment",
|
||||
leaseId: source.environmentLeaseId, runnerTask: task, sandboxLeaseAcquisition: { outcome: "resumed" as const, providerLeaseId: "retained-task" } };
|
||||
const owners = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner");
|
||||
let allowRelease!: () => void;
|
||||
const receipt = new Promise<void>(resolve => { allowRelease = resolve; });
|
||||
const updates: Array<{ table: unknown; values: Record<string, unknown> }> = [];
|
||||
let attempts = 0;
|
||||
const release = vi.fn(async () => {
|
||||
expect(owners.mock.calls.at(-1)![0].authorityRunId).toBe(current.binding.runId);
|
||||
expect(updates.some(update => update.values.leaseOwner === null && update.values.recoveryState === null)).toBe(true);
|
||||
if (unknown && attempts++ === 0) throw new Error("fixture lost release reply");
|
||||
await receipt;
|
||||
return structuredClone(source);
|
||||
});
|
||||
let allowPhysicalStop!: () => void;
|
||||
const physicalStop = new Promise<void>(resolve => { allowPhysicalStop = resolve; });
|
||||
const close = vi.fn(async () => { if (shutdown || deadline || journalFailure) await physicalStop; }), detachControllerForRestart = vi.fn(async () => {});
|
||||
const session = { release, close, detachControllerForRestart };
|
||||
const validated = validatePrpStructuredRunResult({ schema: "paperclip.run_result.v1", reportedWorkDisposition: "done",
|
||||
summary: "Completed", completionClaim: { contractRevision: current.completionContract.contract.revision,
|
||||
objectiveSatisfied: true, criteria: [], remainingWork: [] }, evidence: [], verification: [], artifacts: [], attentionRequests: [] });
|
||||
if (!validated.ok) throw new Error("Invalid completion fixture");
|
||||
state.execute.mockReset().mockImplementationOnce(async options => {
|
||||
await options.onCheckpoint?.({ backendKind: "runner", driverKind: current.session.driverKind, sessionId: "driver-session",
|
||||
identity: { ...current.binding, sessionId: source.normalizedSessionId }, providerSessionId: "provider-session",
|
||||
activeTurnId: null, pendingRuntimeRequests: [] });
|
||||
await options.onSession?.(session);
|
||||
return { result: validated.result, terminal: { runTerminalState: "succeeded" }, turnId: source.turnId,
|
||||
normalizedSessionId: source.normalizedSessionId, providerSessionId: "provider-session", driverKind: "test", driverVersion: "1",
|
||||
nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
|
||||
});
|
||||
const execute = executePaperclipNativeSession({ db: leaseDb(current, {}, {}, updates), execution: current,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true, runnerExecutionTarget: target,
|
||||
managedAiCredentialHome: home, managedAiCredentialIdentity: "selected-revision", managedAiAccountIdentity: "selected-account",
|
||||
runnerEnvironment: provider === "claude" ? { ANTHROPIC_API_KEY: "synthetic-fixture" } : {} });
|
||||
void execute.catch(() => undefined);
|
||||
try {
|
||||
await vi.waitFor(() => expect(release, inspect(updates.map(update => update.values.failureDetail))).toHaveBeenCalledTimes(unknown ? 2 : 1));
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
const next = { ...current, binding: { ...current.binding, runId: `${current.binding.runId}-next` } };
|
||||
const nextTask = vi.fn();
|
||||
await expect(executePaperclipNativeSession({ db: leaseDb(next), execution: next,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
runnerExecutionTarget: { ...target, runnerTask: nextTask } })).rejects.toThrow("native_session_supervisor_busy");
|
||||
expect(nextTask).not.toHaveBeenCalled();
|
||||
if (provider === "codex" && !unknown && !shutdown && !journalFailure && !deadline) {
|
||||
await expect(executePaperclipNativeSession({ db: leaseDb(next), execution: next,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
runnerExecutionTarget: { ...target, environmentId: "competing-environment", runnerTask: nextTask,
|
||||
sandboxLeaseAcquisition: { ...target.sandboxLeaseAcquisition, providerLeaseId: "competing-task" } } })).rejects.toThrow("native_session_supervisor_busy");
|
||||
expect(nextTask).not.toHaveBeenCalled();
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
}
|
||||
if (shutdown || deadline) {
|
||||
if (shutdown) expect(await closeIdleWarmNativeSessionsForRestart()).toMatchObject({ busy: 1 });
|
||||
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
expect(detachControllerForRestart).not.toHaveBeenCalled();
|
||||
// A late host receipt cannot publish a released owner during retirement.
|
||||
allowRelease();
|
||||
await receipt;
|
||||
await Promise.resolve();
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
await expect(executePaperclipNativeSession({ db: leaseDb(next), execution: next,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
runnerExecutionTarget: { ...target, runnerTask: nextTask } })).rejects.toThrow("native_session_supervisor_busy");
|
||||
expect(nextTask).not.toHaveBeenCalled();
|
||||
allowPhysicalStop();
|
||||
await expect(execute).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(readNativeSessionIdleWork()).toBe("none");
|
||||
const directory = join(resolvePaperclipInstanceRoot(), "runtime/paperclip-runner/sessions");
|
||||
expect(JSON.parse(await readFile(join(directory, (await readdir(directory))[0]!), "utf8")).releasedRunner).toBeUndefined();
|
||||
return;
|
||||
}
|
||||
if (journalFailure) {
|
||||
const directory = join(resolvePaperclipInstanceRoot(), "runtime/paperclip-runner/sessions");
|
||||
const path = join(directory, (await readdir(directory))[0]!);
|
||||
await rename(path, `${path}.saved`);
|
||||
await mkdir(path);
|
||||
let finished = false;
|
||||
void execute.finally(() => { finished = true; }).catch(() => undefined);
|
||||
allowRelease();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(finished).toBe(false);
|
||||
await expect(executePaperclipNativeSession({ db: leaseDb(next), execution: next,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
managedAiCredentialHome: home, managedAiCredentialIdentity: "selected-revision", managedAiAccountIdentity: "selected-account",
|
||||
runnerExecutionTarget: { ...target, remoteCwd: "different-workspace", runnerTask: nextTask } })).rejects.toThrow("native_session_supervisor_busy");
|
||||
expect(nextTask).not.toHaveBeenCalled();
|
||||
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
expect(finished).toBe(false);
|
||||
await rm(path, { recursive: true });
|
||||
await rename(`${path}.saved`, path);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toBeUndefined();
|
||||
allowPhysicalStop();
|
||||
await expect(execute).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(readNativeSessionIdleWork()).toBe("none");
|
||||
expect(detachControllerForRestart).not.toHaveBeenCalled();
|
||||
return;
|
||||
}
|
||||
allowRelease();
|
||||
await expect(execute).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(readNativeSessionIdleWork()).toBe("none");
|
||||
const directory = join(resolvePaperclipInstanceRoot(), "runtime/paperclip-runner/sessions");
|
||||
const files = await readdir(directory);
|
||||
const path = join(directory, files[0]!);
|
||||
const envelope = JSON.parse(await readFile(path, "utf8"));
|
||||
expect(envelope.releasedRunner).toMatchObject({ source, taskId: "retained-task", scopeDigest: expect.any(String) });
|
||||
expect(JSON.stringify(envelope)).not.toContain("synthetic-fixture");
|
||||
if (!unknown) {
|
||||
const calls = owners.mock.calls.length;
|
||||
for (const changed of [{ remoteCwd: "different-workspace" }, { runnerTaskProjectIds: ["different-project"] }]) {
|
||||
await expect(executePaperclipNativeSession({ db: leaseDb(next), execution: next,
|
||||
runnerInstanceId: source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
managedAiCredentialHome: home, managedAiCredentialIdentity: "selected-revision", managedAiAccountIdentity: "selected-account",
|
||||
runnerExecutionTarget: { ...target, ...changed, runnerTask: nextTask },
|
||||
runnerEnvironment: provider === "claude" ? { ANTHROPIC_API_KEY: "synthetic-fixture" } : {} })).rejects.toThrow("released_owner_scope_mismatch");
|
||||
}
|
||||
expect(owners).toHaveBeenCalledTimes(calls);
|
||||
expect(nextTask).not.toHaveBeenCalled();
|
||||
expect(state.execute).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toEqual(envelope.releasedRunner);
|
||||
}
|
||||
await vi.waitFor(() => expect(detachControllerForRestart).toHaveBeenCalledOnce());
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
expect(task.mock.calls.map(([operation]) => (operation as { kind: string }).kind)).toEqual(["describe"]);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toEqual(envelope.releasedRunner);
|
||||
if (provider === "codex" && !unknown) {
|
||||
const inspected = vi.spyOn(remoteWarmRecoveryRuntime, "inspectRemoteWarmSessionRecovery").mockImplementation(async request => {
|
||||
request.loadCheckpoint(current);
|
||||
throw new Error("fixture completed-source proof checked");
|
||||
});
|
||||
try {
|
||||
const calls = owners.mock.calls.length;
|
||||
const base = { db: leaseDb(next), execution: next, runnerInstanceId: source.runnerInstanceId,
|
||||
useRunnerd: true, runnerIngressAuthorized: true, runnerExecutionTarget: target,
|
||||
managedAiCredentialHome: home, managedAiCredentialIdentity: "selected-revision", managedAiAccountIdentity: "selected-account" };
|
||||
await expect(executePaperclipNativeSession(base)).rejects.toThrow("fixture completed-source proof checked");
|
||||
for (const changed of [
|
||||
{ managedAiAccountIdentity: "different-account" }, { managedAiCredentialIdentity: "different-revision" },
|
||||
{ runnerExecutionTarget: { ...target, runnerTaskProjectIds: ["different-project"] } },
|
||||
{ runnerExecutionTarget: { ...target, remoteCwd: "different-workspace" } },
|
||||
{ runnerExecutionTarget: { ...target, sandboxLeaseAcquisition: { ...target.sandboxLeaseAcquisition, providerLeaseId: "different-task" } } },
|
||||
]) await expect(executePaperclipNativeSession({ ...base, ...changed })).rejects.toThrow("released_owner_scope_mismatch");
|
||||
expect(owners).toHaveBeenCalledTimes(calls);
|
||||
expect(state.execute).toHaveBeenCalledTimes(1);
|
||||
} finally { inspected.mockRestore(); }
|
||||
}
|
||||
await owners.mock.calls.at(-1)![0].onResumed!(source);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toBeUndefined();
|
||||
const replacement = { ...envelope, snapshot: { ...envelope.snapshot, identity: { ...envelope.snapshot.identity, runId: "new-owner" } },
|
||||
releasedRunner: { ...envelope.releasedRunner, source: { ...source, runId: "new-owner" } } };
|
||||
await writeFile(path, JSON.stringify(replacement));
|
||||
await expect(owners.mock.calls.at(-1)![0].onResumed!(source)).rejects.toThrow("released_source_mismatch");
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toEqual(replacement.releasedRunner);
|
||||
} finally {
|
||||
allowRelease(); allowPhysicalStop(); await execute.catch(() => undefined); await closeIdleWarmNativeSessionsForRestart(); owners.mockRestore();
|
||||
if (previousHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = previousHome;
|
||||
}
|
||||
});
|
||||
|
||||
it.each((["codex", "claude"] as const).flatMap(provider => (["current", "stale", ...(provider === "codex" ? ["foreign runner", "foreign lease"] : [])] as const)
|
||||
.map(receipt => [provider, receipt] as const)))(
|
||||
"records the confirmed %s source after external checkpoint registration and attachment (receipt=%s)", async (provider, receipt) => {
|
||||
const invalidReceipt = receipt !== "current";
|
||||
await closeIdleWarmNativeSessionsForRestart();
|
||||
const previousHome = process.env.PAPERCLIP_HOME;
|
||||
process.env.PAPERCLIP_HOME = isolatedStateDirectory;
|
||||
const { permissionPolicy: _permissionPolicy, ...claudeProfile } = QUALIFIED_ACPX_PROFILES.claude;
|
||||
let first: NativeExecutionInput = scopedNativeExecution(`released-external-${provider}-${receipt.replaceAll(" ", "-")}`);
|
||||
first.session.lifecyclePolicy = { mode: "warm", idleTimeoutMs: 5_000 };
|
||||
if (provider === "claude") first = { ...first, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null,
|
||||
runtimeContext: nativeRuntimeContextFixture(), session: { ...first.session, driverKind: "acpx_runtime" },
|
||||
provider: { kind: "acpx", agent: "claude", model: "claude-sonnet-4-6", permissionMode: "approve-all", profile: claudeProfile } };
|
||||
parseNativeExecutionInput(first);
|
||||
const next = { ...first, binding: { ...first.binding, runId: `${first.binding.runId}-next` } };
|
||||
const source = { runnerInstanceId: "external-runner", environmentLeaseId: "external-lease", runId: first.binding.runId,
|
||||
normalizedSessionId: first.session.normalizedSessionId!, turnId: `turn-${first.binding.runId}`, itemId: `item-${first.binding.runId}` };
|
||||
const successor = { ...source, runId: next.binding.runId, turnId: "attached-turn", itemId: "attached-item" };
|
||||
const root = await initializeScopedRunnerRoot(first, source.runnerInstanceId);
|
||||
await writeRunnerAuthority(root, source, "ready");
|
||||
const home = join(isolatedStateDirectory, "external-selected-account");
|
||||
await mkdir(home, { recursive: true });
|
||||
await writeFile(join(home, "auth.json"), JSON.stringify({ OPENAI_API_KEY: "synthetic-fixture" }), { mode: 0o600 });
|
||||
const task = vi.fn(async () => ({ kind: "description", taskId: "external-task", launchPreparationVersion: 1,
|
||||
projectAuthCheckVersion: 1, warmTaskVersion: 1, releaseResumeVersion: 1,
|
||||
runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } }));
|
||||
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd: ".", environmentId: "external-environment",
|
||||
leaseId: source.environmentLeaseId, runnerTask: task, sandboxLeaseAcquisition: { outcome: "resumed" as const, providerLeaseId: "external-task" } };
|
||||
const createOwner = pluginTaskRuntime.createPluginRunnerTaskOwner;
|
||||
const owners = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner").mockImplementation(input => createOwner({ ...input, adoptIdentity: source }));
|
||||
const saved = new Map<string, import("../../vendor/paperclip-runner/index.js").DurablePrpCheckpoint>();
|
||||
const restore = vi.spyOn(runnerCheckpoint, "restoreNativeRunnerCheckpoint").mockImplementation(async (_db, binding, directory) => {
|
||||
const checkpoint = saved.get(binding.runId);
|
||||
if (!checkpoint) throw new Error("Missing fixture controller checkpoint");
|
||||
checkpoint.restoreSessionDirectory(directory);
|
||||
return checkpoint;
|
||||
});
|
||||
let authority: DurablePrpControlPlane | undefined;
|
||||
let register: import("../../vendor/paperclip-runner/index.js").RunnerExecutionDriver["controlPlaneRegistration"];
|
||||
let releasePhysical!: () => void;
|
||||
const physical = new Promise<void>(resolve => { releasePhysical = resolve; });
|
||||
const close = vi.fn(async () => { if (invalidReceipt) await physical; });
|
||||
const release = vi.fn(async () => {
|
||||
await authority!.flushCheckpoint();
|
||||
const identity = authority!.store.state.identity;
|
||||
if (identity.runId !== successor.runId || receipt === "current") return structuredClone(identity);
|
||||
if (receipt === "stale") return structuredClone(source);
|
||||
return { ...identity, ...(receipt === "foreign runner" ? { runnerInstanceId: "successor-candidate-runner" }
|
||||
: { environmentLeaseId: "successor-controller-lease" }) };
|
||||
});
|
||||
const session = { close, release, detachControllerForRestart: vi.fn(async () => {}) };
|
||||
const validated = validatePrpStructuredRunResult({ schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: "Completed",
|
||||
completionClaim: { contractRevision: first.completionContract.contract.revision, objectiveSatisfied: true, criteria: [], remainingWork: [] },
|
||||
evidence: [], verification: [], artifacts: [], attentionRequests: [] });
|
||||
if (!validated.ok) throw new Error("Invalid completion fixture");
|
||||
state.execute.mockReset().mockImplementation(async options => {
|
||||
const current = options.input.binding.runId === first.binding.runId ? first : next;
|
||||
if (!authority) {
|
||||
state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!();
|
||||
const transport = state.createTransport.mock.calls.at(-1)![0] as RunnerTransportOptions & {
|
||||
controlPlaneRegistration: NonNullable<import("../../vendor/paperclip-runner/index.js").RunnerExecutionDriver["controlPlaneRegistration"]>;
|
||||
};
|
||||
await rm(join(root, "control-plane"), { recursive: true });
|
||||
authority = new DurablePrpControlPlane({ stateDirectory: join(root, "control-plane"), identity: source,
|
||||
expectedRunnerVersion: "fixture", expectedRunnerDigest: `sha256:${"a".repeat(64)}` });
|
||||
const persist = authority.setCheckpointPersistence.bind(authority);
|
||||
// Only the backing store is modeled; registration and the fail-closed marker are real.
|
||||
vi.spyOn(authority, "setCheckpointPersistence").mockImplementation(() => persist(async checkpoint => { saved.set(checkpoint.runId, checkpoint); }));
|
||||
register = transport.controlPlaneRegistration;
|
||||
await register(authority, source);
|
||||
expect(JSON.parse(await readFile(join(root, "control-plane/control-plane-state.json"), "utf8")).schema)
|
||||
.toBe("paperclip.runner.external-control-plane-state.v1");
|
||||
} else {
|
||||
expect(options.existingSession).toBe(session);
|
||||
await owners.mock.calls[0]![0].onResumed!(source);
|
||||
authority.rotateRunIdentity(successor);
|
||||
await register!(authority, successor);
|
||||
await writeFile(join(root, "runner/runner-state.json"), JSON.stringify(durableRunnerState(successor, "ready")));
|
||||
expect(JSON.parse(await readFile(join(root, "control-plane/control-plane-state.json"), "utf8")).identity.runId).toBe(source.runId);
|
||||
}
|
||||
await options.onCheckpoint?.({ backendKind: "runner", driverKind: current.session.driverKind, sessionId: "driver-session",
|
||||
identity: { ...current.binding, sessionId: source.normalizedSessionId }, providerSessionId: "provider-session", activeTurnId: null, pendingRuntimeRequests: [] });
|
||||
await options.onSession?.(session);
|
||||
return { result: validated.result, terminal: { runTerminalState: "succeeded" }, turnId: "provider-turn",
|
||||
normalizedSessionId: source.normalizedSessionId, providerSessionId: "provider-session", driverKind: "test", driverVersion: "1",
|
||||
nativeEventCount: 1, highestContiguousSourceSeq: 1, usage: null };
|
||||
});
|
||||
const run = (execution: NativeExecutionInput) => {
|
||||
const db = leaseDb(execution, { leaseOwner: "external-controller", controllerGeneration: 1 });
|
||||
if (execution.binding.runId === next.binding.runId) {
|
||||
const select = db.select.bind(db);
|
||||
db.select = ((fields?: Record<string, unknown>) => fields?.status === heartbeatRuns.status && fields?.runnerProfileJson === heartbeatRuns.runnerProfileJson
|
||||
? priorNativeRunDb(first).select(fields as never) : select(fields as never)) as Db["select"];
|
||||
}
|
||||
return executePaperclipNativeSession({ db, leaseOwner: "external-controller", execution,
|
||||
runnerInstanceId: execution === next ? "successor-candidate-runner" : source.runnerInstanceId, useRunnerd: true, runnerIngressAuthorized: true,
|
||||
runnerExecutionTarget: execution === next ? { ...target, leaseId: "successor-controller-lease" } : target,
|
||||
managedAiCredentialHome: home, managedAiCredentialIdentity: "selected-revision",
|
||||
managedAiAccountIdentity: "selected-account", runnerEnvironment: provider === "claude" ? { ANTHROPIC_API_KEY: "synthetic-fixture" } : {} });
|
||||
};
|
||||
let following: ReturnType<typeof run> | undefined;
|
||||
try {
|
||||
await expect(run(first)).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
const directory = join(resolvePaperclipInstanceRoot(), "runtime/paperclip-runner/sessions");
|
||||
const path = join(directory, (await readdir(directory))[0]!);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner.source).toEqual(source);
|
||||
following = run(next);
|
||||
void following.catch(() => undefined);
|
||||
if (invalidReceipt) {
|
||||
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
||||
expect(readNativeSessionIdleWork()).toBe("present");
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner).toBeUndefined();
|
||||
releasePhysical();
|
||||
await expect(following).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(readNativeSessionIdleWork()).toBe("none");
|
||||
} else {
|
||||
await expect(following).resolves.toMatchObject({ sessionId: source.normalizedSessionId });
|
||||
expect(release).toHaveBeenCalledTimes(2);
|
||||
expect(JSON.parse(await readFile(path, "utf8")).releasedRunner.source).toEqual(successor);
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
}
|
||||
} finally {
|
||||
releasePhysical(); await following?.catch(() => undefined); await closeIdleWarmNativeSessionsForRestart();
|
||||
await authority?.stop(); owners.mockRestore(); restore.mockRestore();
|
||||
if (previousHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = previousHome;
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["codex", "opencode"] as const)("keeps assigned MCP credentials on the control plane for %s", async (providerKind) => {
|
||||
const selectedExecution: NativeExecutionInput = providerKind === "opencode"
|
||||
? { ...execution, provider: { kind: "opencode", model: "openrouter/deepseek/deepseek-v4.1-flash-1017", permissionMode: "allow" } }
|
||||
|
||||
Reference in new issue
Block a user