Files
qiming/qimingclaw/crates/agent-electron-client/src/main/processManager.test.ts

168 lines
4.7 KiB
TypeScript

/**
* 单元测试: ManagedProcess — stop() 监听器清理 & kill() 升级超时
*
* 覆盖内容:
* - stop() 调用 removeAllListeners 防止句柄泄漏
* - stop() 在无进程时返回 Not running
* - kill() SIGTERM→SIGKILL 升级使用 PROCESS_KILL_ESCALATION_TIMEOUT
* - kill() 在无进程时不报错
* - kill() 升级定时器在进程正常退出后被清除
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "events";
vi.mock("electron-log", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
vi.mock("child_process", () => ({
spawn: vi.fn(),
}));
import { ManagedProcess } from "./processManager";
import { PROCESS_KILL_ESCALATION_TIMEOUT } from "@shared/constants";
/** Create a fake ChildProcess-like object */
function createFakeProc() {
const proc = new EventEmitter() as any;
proc.pid = 12345;
proc.killed = false;
proc.stdout = new EventEmitter();
proc.stderr = new EventEmitter();
proc.stdin = new EventEmitter();
proc.kill = vi.fn();
return proc;
}
/** Inject a fake process into a ManagedProcess instance */
function injectProcess(mp: ManagedProcess, proc: any) {
(mp as any).process = proc;
}
describe("ManagedProcess", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
describe("stop()", () => {
it("returns Not running when no process exists", () => {
const mp = new ManagedProcess("test");
const result = mp.stop();
expect(result).toEqual({ success: true, message: "Not running" });
});
it("removes all listeners before killing", () => {
const mp = new ManagedProcess("test");
const proc = createFakeProc();
injectProcess(mp, proc);
const stdoutRemove = vi.spyOn(proc.stdout, "removeAllListeners");
const stderrRemove = vi.spyOn(proc.stderr, "removeAllListeners");
const stdinRemove = vi.spyOn(proc.stdin, "removeAllListeners");
const procRemove = vi.spyOn(proc, "removeAllListeners");
const result = mp.stop();
expect(result).toEqual({ success: true });
expect(stdoutRemove).toHaveBeenCalled();
expect(stderrRemove).toHaveBeenCalled();
expect(stdinRemove).toHaveBeenCalled();
expect(procRemove).toHaveBeenCalled();
expect(proc.kill).toHaveBeenCalled();
expect(mp.running).toBe(false);
});
it("sets process to null before calling kill", () => {
const mp = new ManagedProcess("test");
const proc = createFakeProc();
injectProcess(mp, proc);
let runningDuringKill: boolean | undefined;
proc.kill = vi.fn(() => {
runningDuringKill = mp.running;
});
mp.stop();
expect(runningDuringKill).toBe(false);
});
});
describe("kill()", () => {
it("does not throw when no process exists", () => {
const mp = new ManagedProcess("test");
expect(() => mp.kill()).not.toThrow();
});
it("sends SIGTERM and schedules SIGKILL escalation", () => {
vi.useFakeTimers();
const mp = new ManagedProcess("test");
const proc = createFakeProc();
injectProcess(mp, proc);
const processKillSpy = vi
.spyOn(process, "kill")
.mockImplementation(() => true);
mp.kill();
expect(proc.kill).toHaveBeenCalled();
expect(mp.running).toBe(false);
// Escalation should not have fired yet
expect(processKillSpy).not.toHaveBeenCalled();
// Advance past PROCESS_KILL_ESCALATION_TIMEOUT
vi.advanceTimersByTime(PROCESS_KILL_ESCALATION_TIMEOUT + 1);
expect(processKillSpy).toHaveBeenCalledWith(proc.pid, "SIGKILL");
processKillSpy.mockRestore();
});
it("clears escalation timer if process exits promptly", () => {
vi.useFakeTimers();
const mp = new ManagedProcess("test");
const proc = createFakeProc();
injectProcess(mp, proc);
const processKillSpy = vi
.spyOn(process, "kill")
.mockImplementation(() => true);
mp.kill();
// Simulate prompt exit
proc.emit("exit", 0, null);
// Advance past escalation timeout — SIGKILL should NOT fire
vi.advanceTimersByTime(PROCESS_KILL_ESCALATION_TIMEOUT + 1);
expect(processKillSpy).not.toHaveBeenCalled();
processKillSpy.mockRestore();
});
it("removes all listeners on the process", () => {
const mp = new ManagedProcess("test");
const proc = createFakeProc();
injectProcess(mp, proc);
const removeAll = vi.spyOn(proc, "removeAllListeners");
const stdoutRemove = vi.spyOn(proc.stdout, "removeAllListeners");
mp.kill();
expect(removeAll).toHaveBeenCalled();
expect(stdoutRemove).toHaveBeenCalled();
});
});
});