Skip to content

Commit 1f8149c

Browse files
authored
fix(ssh-tunnel): avoid failed-spawn cleanup delay
1 parent bd4edf3 commit 1f8149c

2 files changed

Lines changed: 25 additions & 19 deletions

File tree

src/infra/ssh-tunnel.test.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe("startSshPortForward", () => {
6565
const openServers: net.Server[] = [];
6666

6767
afterEach(async () => {
68+
vi.useRealTimers();
6869
while (openServers.length > 0) {
6970
const server = openServers.pop();
7071
await new Promise<void>((resolve) => {
@@ -165,37 +166,43 @@ describe("startSshPortForward", () => {
165166
});
166167

167168
it("rejects with the spawn error when ssh binary is missing", async () => {
169+
vi.useFakeTimers();
168170
const spawnError = new Error("ENOENT: no such file or directory, spawn /usr/bin/ssh");
169171
(spawnError as NodeJS.ErrnoException).code = "ENOENT";
172+
const kill = vi.fn(() => false);
170173
mocks.spawn.mockImplementation(() => {
171174
const child = new EventEmitter() as EventEmitter & {
172175
killed: boolean;
173-
pid: number;
176+
pid?: number;
174177
stderr: EventEmitter & { setEncoding: (enc: string) => void };
175178
kill: (signal?: string) => boolean;
176179
};
177180
child.killed = false;
178-
child.pid = 0;
179181
const stderr = new EventEmitter() as EventEmitter & { setEncoding: (enc: string) => void };
180182
stderr.setEncoding = () => {};
181183
child.stderr = stderr;
182-
child.kill = () => {
183-
child.killed = true;
184-
queueMicrotask(() => child.emit("exit", null, "SIGTERM"));
185-
return true;
186-
};
187-
// Emit the error asynchronously, matching real spawn behavior
188-
queueMicrotask(() => child.emit("error", spawnError));
184+
child.kill = kill;
185+
queueMicrotask(() => {
186+
child.emit("error", spawnError);
187+
child.emit("close", -2, null);
188+
});
189189
return child;
190190
});
191191

192-
await expect(
193-
startSshPortForward({
194-
target: "[email protected]:2222",
195-
localPortPreferred: 43210,
196-
remotePort: 18789,
197-
timeoutMs: 500,
198-
}),
199-
).rejects.toThrow("ENOENT");
192+
const forwarding = startSshPortForward({
193+
target: "[email protected]:2222",
194+
localPortPreferred: 43210,
195+
remotePort: 18789,
196+
timeoutMs: 500,
197+
});
198+
const rejection = expect(forwarding).rejects.toMatchObject({
199+
message: expect.stringContaining("ENOENT"),
200+
cause: spawnError,
201+
});
202+
203+
await vi.advanceTimersByTimeAsync(0);
204+
expect(vi.getTimerCount()).toBe(0);
205+
await rejection;
206+
expect(kill).toHaveBeenCalledWith("SIGTERM");
200207
});
201208
});

src/infra/ssh-tunnel.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,9 @@ export async function startSshPortForward(opts: {
172172
});
173173

174174
const stop = async () => {
175-
if (child.killed) {
175+
if (child.killed || !child.kill("SIGTERM")) {
176176
return;
177177
}
178-
child.kill("SIGTERM");
179178
await new Promise<void>((resolve) => {
180179
const t = setTimeout(() => {
181180
try {

0 commit comments

Comments
 (0)