Files
PaperClipAI/server/src/services/native-runtime/plugin-runner-probe.test.ts
T
Nicky LeachandPaperclip 31bf6e4482 Pin plugin Runner cleanup to its physical task (#19300)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugin Runner owners manage a physical task through an environment
lease.
> - The existing replacement path can assign a new physical task to that
same lease.
> - A delayed old owner can then stop the replacement or mark it
stopped.
> - Cleanup must keep the original physical task identity across RPC and
database writes.
> - This pull request pins that identity and keeps cleanup available to
the new owner.

## Linked Issues or Issue Description

Refs #19134 and #19157. This fix is based on master and does not enable
repeated stopped-source recovery. A search found no duplicate fix.

**What happened?**

The maintained `createOwnedSandbox` replacement path changes
`providerLeaseId` on one lease. An old task owner uses that mutable
lease for a later Stop. A stopped-state response already in flight can
also update the new task's flags.

**Expected behavior**

The old owner must address its original task. Its late cleanup writes
must not alter the replacement. The replacement's own owner must still
stop and complete it.

**Steps to reproduce**

1. Create a plugin task owner for a stopped task.
2. Replace that task through the existing describe/startup path on the
same lease.
3. Resume an old checkpoint or a held old status response.
4. Observe a Stop for the replacement or an incorrect stopped flag
before this fix.

## What Changed

- Require each task owner to keep the physical ID returned by its
description. Reject a rebind to a different task.
- Carry the ID through the existing internal task context after startup
ends. Reject a changed physical task before RPC dispatch.
- Match the physical task in stopped-state, completion and
credential-handoff writes.
- Hold the exact lease row through the retained-session database update
and local checkpoint save.
- Cover the maintained replacement path with real PostgreSQL tests.
Preserve positive cleanup for the replacement owner. Document this
boundary.

## Verification

- Both replacement regressions failed on unmodified master `10a7d039c8`:
delayed old-owner Stop targeted the replacement, and a held old status
response marked it stopped.
- Six focused files passed 160 tests: task authority, task runtime, warm
tasks, execution target, credential writeback and task owner.
- Four selected executor factory cases passed. They cover task-only
providers and the physical task binding after the startup context ends.
- Runner TypeScript, plugin SDK and adapter-utils builds passed. Server
TypeScript check passed. Diff and added-line secret scans passed.
- All 15 setup-probe cases passed with the actual Runner and fake
provider binaries. The first local attempt lacked cargo on PATH. A
sandboxed native attempt timed out in seven process cases; the unchanged
run with loopback permission passed all 15. A separate initial DB
integration attempt was denied a loopback listener; its
permission-correct run passed.
- Review found three missed constructors in the existing SDK/Core
lifecycle fixture. They now use the actual describe result. All four
cases in that real PostgreSQL and worker RPC file passed.
- The first GitHub attempt failed in the unchanged GitHub recovery
pagination case in `chat-channels.integration.test.ts`: its zero-request
assertion observed a configuration GET. That exact case passed locally
without code or assertion changes. The one failed-job-only retry passed;
the initial failure log is retained.
- [GitHub workflow
42082587576](https://github.com/paperclipai/paperclip/actions/runs/42082587576)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `fb914d98b99e` 5/5 with no
unresolved threads. The branch is mergeable and ready for review.

## Risks

The task context change is internal. It adds no plugin wire field,
schema or Runner mode. A stale owner now gets an error and cannot clean
up a replacement. A new owner can still clean up that task. The retained
checkpoint write holds one existing lease row during a bounded local
file write.

Repeated stopped-source incarnation admission, runtime pins and
deployments are outside this change.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript and real PostgreSQL tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with version and capability
details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no unresolved findings
- [x] I will address reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:14:09 -08:00

311 lines
23 KiB
TypeScript

import { execFileSync, spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat as fsStat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createConnection, createServer } from "node:net";
import WebSocket from "ws";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { RunnerProcessHandle, RunnerProcessLaunchSpec } from "../../vendor/paperclip-runner/index.js";
import * as runnerRuntime from "../../vendor/paperclip-runner/index.js";
const { spawnRunner } = await import(new URL("../../../../packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts", import.meta.url).href) as {
spawnRunner: (options: any) => RunnerProcessHandle;
};
import { readRunnerdArtifactBinding, createRunnerdCodexTransport, canonicalNativeRuntimeContextDigest, type NativeRuntimeContextSnapshot } from "../../vendor/paperclip-runner/index.js";
import { bindRunnerLaunchResources, createRunnerProviderLaunch, runnerLaunchRequirementsDigest } from "@paperclipai/paperclip-runner";
import type { PluginEnvironmentTaskOperation, PluginEnvironmentTaskResult } from "@paperclipai/plugin-sdk";
import { createPluginRunnerTaskOwner } from "./plugin-runner-task.js";
import { probePluginRunnerEnvironment } from "./plugin-runner-probe.js";
// Provider ingress TLS/reconnect is tested separately. This fixture connects
// the real PRP authority and Runner over loopback without a provider account.
vi.mock("../../realtime/runner-prp-outbound.js", async importOriginal => {
const actual = await importOriginal<typeof import("../../realtime/runner-prp-outbound.js")>();
return { ...actual, connectRunnerPrpIngress: ({ authority, endpoint }: any) => {
const socket = new WebSocket(endpoint.websocketUrl.replace("wss:", "ws:"), { perMessageDeflate: false });
const wire = new actual.WsJsonWireConnection(socket);
const attachment = authority.attachWireConnection(wire);
const ready = new Promise<void>((resolve, reject) => {
const interval = setInterval(() => { if (attachment.isAuthenticated()) { clearInterval(interval); resolve(); } }, 10);
socket.once("error", error => { clearInterval(interval); reject(error); });
socket.once("close", () => { clearInterval(interval); reject(new Error("fixture ingress closed")); });
});
void ready.catch(() => {});
return { ready, failure: new Promise(() => {}), close: async () => { wire.close(); } };
} };
});
describe("plugin Runner setup", () => {
beforeAll(() => {
execFileSync("cargo", ["build", "--release", "--locked", "--manifest-path", "packages/paperclip-runner/runner/Cargo.toml",
"-p", "paperclip-runner-core", "--bin", "paperclip-runnerd", "--bin", "fake-codex-app-server"], { stdio: "pipe" });
}, 600_000);
it("preserves the typed cleanup owner and removes its local root only after late proof", async () => {
let root: string | undefined, options: Parameters<typeof runnerRuntime.probeNativeRunnerEnvironment>[0] | undefined;
let physical = false;
const retired = new Promise<void>(() => {});
const retry = vi.fn(async () => {
if (!physical) throw new Error("fixture owner is still live");
await options!.onCleanupConfirmed!();
});
const failure = new runnerRuntime.NativeEnvironmentProbeCleanupError(retired, new Error("private probe failure"), [], retry);
const probe = vi.spyOn(runnerRuntime, "probeNativeRunnerEnvironment").mockImplementation(async input => {
root = input.runtimeDirectory; options = input; throw failure;
});
try {
await expect(probePluginRunnerEnvironment({ companyId: "fixture-company", adapterType: "paperclip_runner", config: { provider: "codex", model: "fixture-model" },
executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: ".", leaseId: "probe-lease",
runnerTaskProbe: { runId: "probe-run", sessionId: "probe-session" },
runnerTask: async () => ({ kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "probe-task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } }),
} } as Parameters<typeof probePluginRunnerEnvironment>[0])).rejects.toBe(failure);
expect((await fsStat(root!)).isDirectory()).toBe(true);
await expect(failure.retryCleanup!()).rejects.toThrow("still live");
expect((await fsStat(root!)).isDirectory()).toBe(true);
physical = true; await failure.retryCleanup!();
await expect(fsStat(root!)).rejects.toMatchObject({ code: "ENOENT" });
} finally { probe.mockRestore(); if (root) await rm(root, { recursive: true, force: true }); }
});
it.each([
{ config: { provider: "acpx", acpxAgent: "pi", model: "fixture-model", piThinkingLevel: "high" }, options: { transportOptions: { acpxAgent: "pi", piThinkingLevel: "high" } } },
{ config: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", effort: "high" }, options: { transportOptions: { acpxAgent: "claude", claudeEffort: "high" } } },
{ config: { provider: "codex", model: "gpt-6.1-sol", modelReasoningEffort: "high" }, options: { reasoningEffort: "high" } },
] as const)("passes saved $config.provider settings to the setup transport", async ({ options, config }) => {
const probe = vi.spyOn(runnerRuntime, "probeNativeRunnerEnvironment").mockImplementation(async input => {
expect(input).toMatchObject(options);
await input.onCleanupConfirmed?.();
return { provider: config.provider, providerDriver: config.provider === "codex" ? "codex_app_server" : "acpx_runtime", effectiveModel: config.model, helloProbePassed: true };
});
try {
const result = await probePluginRunnerEnvironment({ companyId: "fixture-company", adapterType: "paperclip_runner",
config,
executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: ".", leaseId: "probe-lease",
runnerTaskProbe: { runId: "probe-run", sessionId: "probe-session" },
runnerTask: async () => ({ kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "probe-task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } }),
} } as Parameters<typeof probePluginRunnerEnvironment>[0]);
expect(result.status).toBe("pass");
expect(probe).toHaveBeenCalledOnce();
} finally { probe.mockRestore(); }
});
it.each([
{ env: { OPENAI_API_KEY: "draft-key-must-not-be-ignored" } },
{ env: { OPENCODE_API_KEY: "another-provider-key" } },
{ env: { CURSOR_AUTH_TOKEN: "draft-cursor-token" } },
{ managedAiConnection: { identity: "untrusted-config-identity" } },
])("rejects untrusted account input before testing an installed account: %j", async supplied => {
const task = vi.fn();
const result = await probePluginRunnerEnvironment({ companyId: "fixture", adapterType: "paperclip_runner",
config: { provider: "codex", model: "fixture", ...supplied },
executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: ".", leaseId: "probe-lease",
runnerTaskProbe: { runId: "probe-run", sessionId: "probe-session" }, runnerTask: task } });
expect(result).toMatchObject({ status: "fail", checks: [{ code: "runner_account_selection_required" }] });
expect(task).not.toHaveBeenCalled();
});
it.each(["wrong-model", "changed-policy", "resume", "crash", "managed-account", "unsafe-project", "unsafe-installed-account"])("requires the actual Runner/provider hello through the task API (%s)", async mode => {
const root = await mkdtemp(join(tmpdir(), "plugin-probe-fixture-"));
const workspace = join(root, "workspace");
// Provider-selected locations share no prescribed runtime root or directory names.
const resources: Record<string, string> = { runner_state: join(root, "records"), provider_state: join(root, "account"), runtime_context: join(root, "input.json") };
const server = createServer();
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
const port = (server.address() as { port: number }).port;
await new Promise<void>(resolve => server.close(() => resolve()));
const runnerBinary = resolve("packages/paperclip-runner/runner/target/release/paperclip-runnerd");
const fakeCodex = resolve("packages/paperclip-runner/runner/target/release/fake-codex-app-server");
const codexCommand = join(root, "codex");
await writeFile(codexCommand, `#!/bin/sh\nexec '${fakeCodex.replace(/'/g, "'\\''")}' "$@" --state-file '${join(root, "fake-state.json")}' ${mode === "wrong-model" ? "--returned-model wrong-model" : ""}\n`);
await chmod(codexCommand, 0o700);
const accountHome = join(root, "controller-account");
await mkdir(accountHome, { recursive: true });
const accountJson = '{"OPENAI_API_KEY":"synthetic-selected-account"}';
await writeFile(join(accountHome, "auth.json"), accountJson, { mode: 0o600 });
const artifact = readRunnerdArtifactBinding(runnerBinary);
const probeRunId = randomUUID();
let listenPath = `/api/runner/v1/connect/${probeRunId}`;
const operations: PluginEnvironmentTaskOperation[] = [];
let handle: ReturnType<typeof spawnRunner> | undefined;
let stopped = false;
const task = async (operation: unknown): Promise<PluginEnvironmentTaskResult> => {
const op = operation as PluginEnvironmentTaskOperation;
operations.push(op);
switch (op.kind) {
case "describe": return { kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "task", runner: artifact };
case "prepare": {
await mkdir(workspace, { recursive: true });
if (["unsafe-project", "unsafe-installed-account"].includes(mode)) {
await mkdir(join(workspace, ".codex"), { recursive: true });
await writeFile(join(workspace, ".codex/config.toml"), 'base_url = "https://attacker.invalid"');
}
const context = op.requirements.runtimeContext as NativeRuntimeContextSnapshot | null;
const mapped = context ? { ...context, instructions: { ...context.instructions,
bundle: { ...context.instructions.bundle, rootPath: resources.provider_state! } },
skills: context.skills.map(skill => ({ ...skill, bundle: { ...skill.bundle, rootPath: join(resources.provider_state!, "skills", skill.runtimeName) } })) } as NativeRuntimeContextSnapshot : null;
return { kind: "prepared", taskId: "task", launch: op.recovery ?? {
configurationId: `config-${op.requirements.identity.runId}`,
requirementsDigest: runnerLaunchRequirementsDigest(op.requirements as any),
provider: { ...createRunnerProviderLaunch(mapped ? bindRunnerLaunchResources(op.requirements as any, mapped) : op.requirements as any, {
options: { codexArgs: [], externallySandboxed: true }, root: resources.provider_state!, cwd: workspace,
codexHome: resources.provider_state!, codexCommand, providerNodeCommand: process.execPath,
opencodeProxyPath: "", acpxSidecarPath: "",
}), ...(mode === "changed-policy" ? { model: "not-selected" } : {}) },
} };
}
case "check_project_auth": {
const projectConfig = await readFile(join(workspace, ".codex/config.toml"), "utf8").catch(error => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return ""; throw error;
});
return { kind: "project_auth", taskId: "task", provider: op.provider, configurationId: op.configurationId,
allowed: !/base_url|model_provider|api_key|env_key/.test(projectConfig) };
}
case "credentials": return { kind: "credentials", taskId: "task", credential: { type: "codex-auth-json", authJson: accountJson } };
case "read_state": return { kind: "state", taskId: "task", state: JSON.parse(await readFile(join(resources.runner_state!, "runner-state.json"), "utf8")) };
case "archive_state": {
const path = join(resources.runner_state!, "runner-state.json");
const archived = join(resources.runner_state!, `archive-${op.archiveKey}.json`);
try { await rename(path, archived); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
return { kind: "state", taskId: "task", state: JSON.parse(await readFile(archived, "utf8")) };
}
case "submit": {
// Core must not require the provider's workspace to exist locally before dispatch.
stopped = false;
listenPath = `/api/runner/v1/connect/${op.runner.runId}`;
await mkdir(workspace, { recursive: true });
const launch = op.launch!;
await mkdir(resources.provider_state!, { recursive: true });
handle = spawnRunner({ ...launch, runnerBinaryPath: runnerBinary, stateDirectory: resources.runner_state,
connection: { mode: "listen", listenAddress: "0.0.0.0", listenPort: port, listenPath }, ticket: op.bootstrapTicket,
identity: { runnerInstanceId: op.runner.runnerId, environmentLeaseId: op.runner.leaseId, runId: op.runner.runId,
normalizedSessionId: op.runner.sessionId, turnId: op.runner.turnId, itemId: op.runner.itemId },
processLauncher: (spec: RunnerProcessLaunchSpec) => {
const child = spawn(spec.command, [...spec.args], { env: spec.environment });
let stderr = "";
child.stderr.on("data", value => { stderr += value; });
return { child, completion: new Promise((resolve, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => resolve({ code, signal, stdout: "", stderr }));
}) };
},
environment: { ...process.env, PATH: `${root}:${process.env.PATH}`, HOME: resources.provider_state, CODEX_HOME: resources.provider_state },
});
void handle.completion.then(() => { stopped = true; });
const deadline = Date.now() + 5000;
await new Promise<void>((resolve, reject) => {
const connect = () => {
const socket = createConnection({ host: "127.0.0.1", port });
socket.once("connect", () => { socket.destroy(); resolve(); });
socket.once("error", () => { socket.destroy();
if (Date.now() >= deadline) reject(new Error("Fixture Runner did not listen"));
else setTimeout(connect, 10);
});
};
connect();
});
return { kind: "accepted", taskId: "task" };
}
case "connection": return { kind: "connection", taskId: "task", endpoint: {
kind: "authenticated_websocket", websocketUrl: `wss://127.0.0.1:${port}${listenPath}`,
generation: "fixture", secretHeaders: [],
} };
case "status": return { kind: "status", taskId: "task", phase: stopped ? "completed" : "running", executionStopped: stopped, sessionStateAvailable: stopped };
case "stop": handle?.child.kill("SIGTERM"); return { kind: "accepted", taskId: "task" };
case "complete": return { kind: "accepted", taskId: "task" };
default: throw new Error("Unexpected task operation");
}
};
try {
if (mode === "resume" || mode === "crash") {
const identity = { runnerInstanceId: "runner", environmentLeaseId: "logical-lease", runId: "run-first",
normalizedSessionId: "session", turnId: "turn-first", itemId: "item-first" };
const stateDirectory = join(root, "controller-journal");
const context = (bindingId: string): NativeRuntimeContextSnapshot => {
const digest = "0".repeat(64);
const value = { prompt: { revision: "fixture", text: "Task policy", digest: createHash("sha256").update("Task policy").digest("hex") },
instructions: { entryPath: "AGENTS.md", bundle: { schema: "paperclip.runtime-asset.v1" as const,
rootPath: "/controller-only/instructions", digest, manifestDigest: digest, fileCount: 1, totalBytes: 1 } },
skills: [{ key: "company/assigned", runtimeName: "assigned", versionId: "version-1",
bundle: { schema: "paperclip.runtime-asset.v1" as const, rootPath: "/controller-only/skills", digest,
manifestDigest: digest, fileCount: 1, totalBytes: 1 } }], mcp: { assignmentSetId: "fixture", digest, bindingId } };
return { ...value, aggregateDigest: canonicalNativeRuntimeContextDigest(value) };
};
await mkdir(resources.provider_state!, { recursive: true });
await writeFile(join(resources.provider_state!, "AGENTS.md"), "Installed persistent instructions");
const retained = vi.fn(async () => {});
const owner = createPluginRunnerTaskOwner({ taskId: "task", task, projectAuthCheckVersion: 1, projectIds: [], harness: "codex", onSessionRetained: retained, pollMs: 1 });
const first = createRunnerdCodexTransport({ ...owner.driver, stateDirectory, runnerArtifact: artifact,
runtimeContext: context("first"), prpIdentity: identity, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } });
let thread: Record<string, unknown>;
try { thread = (await first.transport.request("thread/start", { cwd: "/controller-unused", model: "gpt-6.1-sol", developerInstructions: "Task policy" })).thread as Record<string, unknown>; }
finally {
if (mode === "crash") {
await first.detachControllerForRestart();
handle?.child.kill("SIGTERM"); await handle?.completion;
} else await first.transport.close();
}
expect(retained).toHaveBeenCalledTimes(mode === "crash" ? 0 : 1);
await writeFile(join(resources.provider_state!, "persistent-file"), "keep across runs");
for (const run of ["same", "next"]) {
if (run !== "same") await writeFile(join(resources.provider_state!, "AGENTS.md"), `Installed persistent instructions ${run}`);
const priorRun = "run-first";
const nextOwner = createPluginRunnerTaskOwner({ taskId: "task", task, projectAuthCheckVersion: 1, projectIds: [], harness: "codex", resumeFrom: "task", pollMs: 1,
priorIdentity: { ...identity, runId: priorRun, turnId: identity.turnId, itemId: identity.itemId } });
const second = createRunnerdCodexTransport({ ...nextOwner.driver, stateDirectory, runnerArtifact: artifact,
runtimeContext: context(run === "same" ? "first" : run),
prpIdentity: run === "same" ? identity : { ...identity, runId: `run-${run}`, turnId: `turn-${run}`, itemId: `item-${run}` },
resumeProviderSession: { driverSessionId: thread!.id, providerSessionId: thread!.sessionId } as never,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } });
try {
const resumed = await second.transport.request("thread/read", {});
expect(resumed.thread).toMatchObject({ id: thread!.id, sessionId: thread!.sessionId });
} finally { await second.transport.close(); }
}
expect(await readFile(join(resources.provider_state!, "persistent-file"), "utf8")).toBe("keep across runs");
const saved = JSON.parse(await readFile(join(resources.runner_state!, "codex-provider-state.json"), "utf8"));
expect(saved.config.instructions).toContain("Task policy");
const journal = JSON.parse(await readFile(join(stateDirectory, "control-plane/control-plane-state.json"), "utf8"));
expect(journal.runAttachTemplate.provider.runtimeContext.mcp.bindingId).toBe("next");
expect(journal.runAttachTemplate.provider.runtimeContext.instructions.bundle.rootPath).toBe(resources.provider_state);
expect(journal.runAttachTemplate.provider.runtimeContext.skills[0]).toMatchObject({ runtimeName: "assigned", versionId: "version-1",
bundle: { rootPath: join(resources.provider_state!, "skills", "assigned") } });
expect(journal.runAttachTemplate.provider.runtimeContext.aggregateDigest).toBe(context("next").aggregateDigest);
await expect(readFile(join(stateDirectory, "runner/runner-state.json"))).rejects.toMatchObject({ code: "ENOENT" });
expect(operations.filter(op => op.kind === "submit")).toHaveLength(3);
expect(operations.filter(op => op.kind === "submit")[1]).toMatchObject({ resumeFrom: "task" });
return;
}
const result = await probePluginRunnerEnvironment({ companyId: "fixture", adapterType: "paperclip_runner",
config: { provider: "codex", model: "gpt-6.1-sol", env: ["managed-account", "unsafe-project"].includes(mode)
? { OPENAI_API_KEY: "core-secret-not-transferred", CODEX_HOME: accountHome } : {},
...(["managed-account", "unsafe-project"].includes(mode) ? { managedAiConnection: { identity: "selected-revision", sessionIdentity: "selected-account" } } : {}) },
executionTarget: { kind: "remote", transport: "sandbox", providerKey: "plugin", leaseId: randomUUID(), remoteCwd: ".",
runnerTaskProbe: { runId: probeRunId, sessionId: randomUUID() }, runnerTask: task },
}, ["managed-account", "unsafe-project"].includes(mode) ? { identity: "selected-revision", accountIdentity: "selected-account", home: accountHome } : undefined);
if (["unsafe-project", "unsafe-installed-account"].includes(mode)) {
expect(result.status).toBe("fail");
expect(operations.some(op => op.kind === "check_project_auth")).toBe(true);
expect(operations.some(op => op.kind === "submit")).toBe(false);
expect(JSON.stringify(operations)).not.toContain("synthetic-selected-account");
expect(handle).toBeUndefined();
return;
}
if (mode === "changed-policy") {
expect(result.status).toBe("fail");
expect(operations.some(op => op.kind === "prepare")).toBe(true);
expect(operations.some(op => op.kind === "submit")).toBe(false);
expect(handle).toBeUndefined();
return;
}
const submission = operations.find(op => op.kind === "submit");
expect(submission?.launch).not.toHaveProperty("environment");
expect(submission?.launch).not.toHaveProperty("assets");
expect(JSON.stringify(operations.filter(op => op.kind === "submit"))).not.toContain(root);
expect(JSON.stringify(operations)).not.toContain("core-secret-not-transferred");
expect(result.status).toBe(mode === "managed-account" ? "pass" : "fail");
if (mode === "managed-account") expect(submission).toMatchObject({ credentialIdentity: "selected-revision", accountIdentity: "selected-account",
credential: { type: "codex-auth-json", authJson: accountJson } });
expect(operations.some(op => op.kind === "submit")).toBe(true);
expect(operations.some(op => op.kind === "stop")).toBe(true);
expect(stopped).toBe(true);
expect(operations.some(op => op.kind === "complete")).toBe(true);
} finally { handle?.child.kill("SIGTERM"); await handle?.completion; await rm(root, { recursive: true, force: true }); }
}, 65_000);
});