Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const { spawnMock, ptyKillMock, killProcessTreeMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
ptyKillMock: vi.fn(),
killProcessTreeMock: vi.fn(),
}));
vi.mock("@lydell/node-pty", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("../../kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTreeMock(...args),
}));
function createStubPty(pid = 1234) {
let exitListener: ((event: { exitCode: number; signal?: number }) => void) | null = null;
return {
pid,
write: vi.fn(),
onData: vi.fn(() => ({ dispose: vi.fn() })),
onExit: vi.fn((listener: (event: { exitCode: number; signal?: number }) => void) => {
exitListener = listener;
return { dispose: vi.fn() };
}),
kill: (signal?: string) => ptyKillMock(signal),
emitExit: (event: { exitCode: number; signal?: number }) => {
exitListener?.(event);
},
};
}
function expectSpawnEnv() {
const spawnOptions = spawnMock.mock.calls[0]?.[2] as { env?: Record<string, string> };
return spawnOptions?.env;
}
describe("createPtyAdapter", () => {
let createPtyAdapter: typeof import("./pty.js").createPtyAdapter;
beforeAll(async () => {
({ createPtyAdapter } = await import("./pty.js"));
});
beforeEach(() => {
spawnMock.mockClear();
ptyKillMock.mockClear();
killProcessTreeMock.mockClear();
vi.useRealTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});
it("forwards explicit signals to node-pty kill on non-Windows", async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
try {
spawnMock.mockReturnValue(createStubPty());
const adapter = await createPtyAdapter({
shell: "bash",
args: ["-lc", "sleep 10"],
});
adapter.kill("SIGTERM");
expect(ptyKillMock).toHaveBeenCalledWith("SIGTERM");
expect(killProcessTreeMock).not.toHaveBeenCalled();
} finally {
if (originalPlatform) {
Object.defineProperty(process, "platform", originalPlatform);
}
}
});
it("uses process-tree kill for SIGKILL by default", async () => {
spawnMock.mockReturnValue(createStubPty());
const adapter = await createPtyAdapter({
shell: "bash",
args: ["-lc", "sleep 10"],
});
adapter.kill();
expect(killProcessTreeMock).toHaveBeenCalledWith(1234);
expect(ptyKillMock).not.toHaveBeenCalled();
});
it("wait does not settle immediately on SIGKILL", async () => {
vi.useFakeTimers();
spawnMock.mockReturnValue(createStubPty());
const adapter = await createPtyAdapter({
shell: "bash",
args: ["-lc", "sleep 10"],
});
const waitPromise = adapter.wait();
const settled = vi.fn();
void waitPromise.then(() => settled());
adapter.kill();
await Promise.resolve();
expect(settled).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(3999);
expect(settled).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
await expect(waitPromise).resolves.toEqual({ code: null, signal: "SIGKILL" });
});
it("prefers real PTY exit over SIGKILL fallback settle", async () => {
vi.useFakeTimers();
const stub = createStubPty();
spawnMock.mockReturnValue(stub);
const adapter = await createPtyAdapter({
shell: "bash",
args: ["-lc", "sleep 10"],
});
const waitPromise = adapter.wait();
adapter.kill();
stub.emitExit({ exitCode: 0, signal: 9 });
await expect(waitPromise).resolves.toEqual({ code: 0, signal: 9 });
await vi.advanceTimersByTimeAsync(4_001);
await expect(adapter.wait()).resolves.toEqual({ code: 0, signal: 9 });
});
it("resolves wait when exit fires before wait is called", async () => {
const stub = createStubPty();
spawnMock.mockReturnValue(stub);
const adapter = await createPtyAdapter({
shell: "bash",
args: ["-lc", "exit 3"],
});
expect(stub.onExit).toHaveBeenCalledTimes(1);
stub.emitExit({ exitCode: 3, signal: 0 });
await expect(adapter.wait()).resolves.toEqual({ code: 3, signal: null });
});
it("keeps inherited env when no override env is provided", async () => {
const stub = createStubPty();
spawnMock.mockReturnValue(stub);
await createPtyAdapter({
shell: "bash",
args: ["-lc", "env"],
});
expect(expectSpawnEnv()).toBeUndefined();
});
it("passes explicit env overrides as strings", async () => {
const stub = createStubPty();
spawnMock.mockReturnValue(stub);
await createPtyAdapter({
shell: "bash",
args: ["-lc", "env"],
env: { FOO: "bar", COUNT: "12", DROP_ME: undefined },
});
expect(expectSpawnEnv()).toEqual({ FOO: "bar", COUNT: "12" });
});
it("does not pass a signal to node-pty on Windows", async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
try {
spawnMock.mockReturnValue(createStubPty());
const adapter = await createPtyAdapter({
shell: "powershell.exe",
args: ["-NoLogo"],
});
adapter.kill("SIGTERM");
expect(ptyKillMock).toHaveBeenCalledWith(undefined);
expect(killProcessTreeMock).not.toHaveBeenCalled();
} finally {
if (originalPlatform) {
Object.defineProperty(process, "platform", originalPlatform);
}
}
});
it("uses process-tree kill for SIGKILL on Windows", async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
try {
spawnMock.mockReturnValue(createStubPty(4567));
const adapter = await createPtyAdapter({
shell: "powershell.exe",
args: ["-NoLogo"],
});
adapter.kill("SIGKILL");
expect(killProcessTreeMock).toHaveBeenCalledWith(4567);
expect(ptyKillMock).not.toHaveBeenCalled();
} finally {
if (originalPlatform) {
Object.defineProperty(process, "platform", originalPlatform);
}
}
});
});