Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import * as Stream from "effect/Stream";
import * as Scope from "effect/Scope";
import * as TestClock from "effect/testing/TestClock";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import * as ChildProcess from "effect/process/ChildProcess";
import * as EffectAcpErrors from "effect-acp/errors";
import type * as EffectAcpProtocol from "effect-acp/protocol";
import type * as EffectAcpSchema from "effect-acp/compat";
Expand Down Expand Up @@ -515,6 +516,71 @@ function makeMockRuntime(input: {
});
}

it.live("does not detach an owned Windows ACP process and retains PID tree cleanup", () => {
let finalizerPid = 0;
let spawnedPid = 0;
return Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const path = yield* Path.Path;
const terminated: number[] = [];
const runtime = yield* AcpSessionRuntime.make({
spawn: {
command: process.execPath,
args: [
yield* path.fromFileUrl(new URL("../../../scripts/acp-mock-agent.ts", import.meta.url)),
],
},
cwd: process.cwd(),
clientInfo: { name: "test", version: "1" },
ownDetachedProcessGroup: true,
processGroupPlatform: "win32",
windowsProcessTreeTerminator: (pid) =>
Effect.sync(() => {
terminated.push(pid);
}),
}).pipe(
Effect.provideService(HostProcess.Platform, "win32"),
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
if (!ChildProcess.isStandardCommand(command))
return Effect.die("Expected standard command");
if (command.command === "taskkill") {
finalizerPid = Number(command.args[1]);
// Exercise the Windows finalizer while terminating the mock child on any host.
return spawner.spawn(
ChildProcess.make(process.execPath, ["-e", `process.kill(${spawnedPid})`]),
);
}
assert.equal(command.command, process.execPath);
assert.isFalse(command.options.detached);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return spawner.spawn(command).pipe(
Effect.tap((child) =>
Effect.sync(() => {
spawnedPid = Number(child.pid);
}),
),
);
}),
),
);
if (runtime.terminateProcessGroup === undefined)
return yield* Effect.die("Missing tree cleanup");
yield* runtime.terminateProcessGroup;
assert.deepEqual(terminated, [spawnedPid]);
assert.isAbove(spawnedPid, 0);
}).pipe(
Effect.provide(layerTest),
Effect.scoped,
Effect.tap(() =>
Effect.sync(() => {
assert.isAbove(spawnedPid, 0);
assert.equal(finalizerPid, spawnedPid);
}),
),
);
});

const isProviderAdapterProtocolError = Schema.is(ProviderAdapter.ProviderAdapterProtocolError);

const makePromptInactivityHarness = Effect.fnUntraced(function* (input: {
Expand Down
3 changes: 2 additions & 1 deletion packages/provider-acp/src/server/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,7 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* (
> {
const random = yield* SecureRandom.SecureRandom;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const hostPlatform = yield* HostProcess.Platform;
const runtimeScope = yield* Scope.fork(yield* Scope.Scope);
const eventQueue = yield* Queue.unbounded<AcpSessionRuntimeEvent>();
const modeStateRef = yield* Ref.make<AcpSessionModeState | undefined>(undefined);
Expand Down Expand Up @@ -1455,7 +1456,7 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* (
extendEnv: options.spawn.extendEnv ?? true,
...(options.ownDetachedProcessGroup === undefined
? {}
: { detached: options.ownDetachedProcessGroup }),
: { detached: options.ownDetachedProcessGroup && hostPlatform !== "win32" }),
shell: containedSpawnCommand.shell,
}),
)
Expand Down
Loading