Skip to content

Commit d77763e

Browse files
test(server): stop every OpenCode server the live test spawns
The live test's instance scope was a bare Scope.make() closed near the end of the test, so a failed assertion before that point left the T3-spawned `opencode serve` running, reparented to systemd. - The instance scope is now acquired with acquireRelease, so it closes on any exit, and the early close stays. - The external server the test starts itself is stopped by the PID captured at spawn: SIGTERM, then SIGKILL if it is still alive after five seconds, instead of waiting on an exit code with a timeout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent bcba877 commit d77763e

1 file changed

Lines changed: 31 additions & 12 deletions

File tree

‎apps/server/src/provider/opencode2/OpenCode2Server.live.test.ts‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,31 @@ const isAlive = (pid: number) => {
4040
}
4141
};
4242

43-
/** Starts `opencode serve` the way a user would run it themselves; stopped by its PID. */
43+
/**
44+
* Stops a server the test spawned itself: SIGTERM, then SIGKILL if it is still
45+
* alive after five seconds. Keyed on the PID captured at spawn, never on a
46+
* pattern. Servers T3 spawns are stopped by T3, through the instance scope.
47+
*/
48+
const stopByPid = (pid: number) =>
49+
Effect.gen(function* () {
50+
const signal = (name: NodeJS.Signals) => {
51+
try {
52+
process.kill(pid, name);
53+
} catch {
54+
// Already exited.
55+
}
56+
};
57+
signal("SIGTERM");
58+
for (let attempt = 0; attempt < 50 && isAlive(pid); attempt++) {
59+
yield* Effect.sleep("100 millis");
60+
}
61+
if (isAlive(pid)) signal("SIGKILL");
62+
});
63+
64+
/**
65+
* Starts `opencode serve` the way a user would run it themselves. It is stopped
66+
* by its PID when the calling scope closes, whether or not the test passed.
67+
*/
4468
const startExternalServer = Effect.fn("OpenCode2ServerLive.startExternalServer")(function* (input: {
4569
readonly environment: NodeJS.ProcessEnv;
4670
readonly directory: string;
@@ -53,15 +77,7 @@ const startExternalServer = Effect.fn("OpenCode2ServerLive.startExternalServer")
5377
}),
5478
);
5579
const pid = Number(child.pid);
56-
yield* Effect.addFinalizer(() =>
57-
Effect.sync(() => {
58-
try {
59-
process.kill(pid, "SIGTERM");
60-
} catch {
61-
// Already exited.
62-
}
63-
}).pipe(Effect.andThen(child.exitCode), Effect.timeout("10 seconds"), Effect.ignore),
64-
);
80+
yield* Effect.addFinalizer(() => stopByPid(pid));
6581
const url = yield* child.stdout.pipe(
6682
Stream.decodeText(),
6783
Stream.splitLines,
@@ -101,8 +117,11 @@ describe.runIf(binaryPath !== undefined)("OpenCode2Server live", () => {
101117
};
102118

103119
// The instance owns its server: building the layer spawns nothing, and
104-
// closing its scope stops whatever it spawned.
105-
const instanceScope = yield* Scope.make();
120+
// closing its scope stops whatever it spawned. The scope is released
121+
// even when an assertion below fails; it is closed early on purpose.
122+
const instanceScope = yield* Effect.acquireRelease(Scope.make(), (scope) =>
123+
Scope.close(scope, Exit.void),
124+
);
106125
const server = Context.get(
107126
yield* Layer.buildWithScope(
108127
OpenCode2Server.layer({

0 commit comments

Comments
 (0)