import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import { captureFullEnv } from "../test-utils/env.js" ;
const spawnSyncMock = vi.hoisted(() => vi.fn());
const resolveLsofCommandSyncMock = vi.hoisted(() => vi.fn());
const resolveGatewayPortMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process" , async () => {
const { mockNodeChildProcessSpawnSync } =
await import ("../../test/helpers/node-builtin-mocks.js" );
return mockNodeChildProcessSpawnSync(spawnSyncMock);
});
vi.mock("./ports-lsof.js" , () => ({
resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args),
}));
vi.mock("../config/paths.js" , async () => {
const actual = await vi.importActual<typeof import ("../config/paths.js" )>("../config/paths.js" );
return {
...actual,
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
};
});
let __testing: typeof import ("./restart-stale-pids.js" ).__testing;
let cleanStaleGatewayProcessesSync: typeof import ("./restart-stale-pids.js" ).cleanStaleGatewayProcessesSync;
let findGatewayPidsOnPortSync: typeof import ("./restart-stale-pids.js" ).findGatewayPidsOnPortSync;
let triggerOpenClawRestart: typeof import ("./restart.js" ).triggerOpenClawRestart;
let currentTimeMs = 0 ;
const envSnapshot = captureFullEnv();
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform" );
beforeAll(async () => {
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import ("./restart-stale-pids.js" ));
({ triggerOpenClawRestart } = await import ("./restart.js" ));
});
beforeEach(() => {
spawnSyncMock.mockReset();
resolveLsofCommandSyncMock.mockReset();
resolveGatewayPortMock.mockReset();
currentTimeMs = 0 ;
resolveLsofCommandSyncMock.mockReturnValue("/usr/sbin/lsof" );
resolveGatewayPortMock.mockReturnValue(18789 );
__testing.setSleepSyncOverride((ms) => {
currentTimeMs += ms;
});
__testing.setDateNowOverride(() => currentTimeMs);
});
afterEach(() => {
envSnapshot.restore();
__testing.setSleepSyncOverride(null );
__testing.setDateNowOverride(null );
if (originalPlatformDescriptor) {
Object.defineProperty(process, "platform" , originalPlatformDescriptor);
}
vi.restoreAllMocks();
});
function setPlatform(platform: NodeJS.Platform): void {
if (!originalPlatformDescriptor) {
return ;
}
Object.defineProperty(process, "platform" , {
...originalPlatformDescriptor,
value: platform,
});
}
describe.runIf(process.platform !== "win32" )("findGatewayPidsOnPortSync" , () => {
it("parses lsof output and filters non-openclaw/current processes" , () => {
const gatewayPidA = process.pid + 1000 ;
const gatewayPidB = process.pid + 2000 ;
const foreignPid = process.pid + 3000 ;
spawnSyncMock.mockReturnValue({
error: undefined,
status: 0 ,
stdout: [
`p${process.pid}`,
"copenclaw" ,
`p${gatewayPidA}`,
"copenclaw-gateway" ,
`p${foreignPid}`,
"cnode" ,
`p${gatewayPidB}`,
"cOpenClaw" ,
].join("\n" ),
});
const pids = findGatewayPidsOnPortSync(18789 );
expect(pids).toEqual([gatewayPidA, gatewayPidB]);
expect(spawnSyncMock).toHaveBeenCalledWith(
"/usr/sbin/lsof" ,
["-nP" , "-iTCP:18789" , "-sTCP:LISTEN" , "-Fpc" ],
expect.objectContaining({ encoding: "utf8" , timeout: 2000 }),
);
});
it("returns empty when lsof fails" , () => {
spawnSyncMock.mockReturnValue({
error: undefined,
status: 1 ,
stdout: "" ,
stderr: "lsof failed" ,
});
expect(findGatewayPidsOnPortSync(18789 )).toEqual([]);
});
});
describe.runIf(process.platform !== "win32" )("cleanStaleGatewayProcessesSync" , () => {
it("kills stale gateway pids discovered on the gateway port" , () => {
const stalePidA = process.pid + 1000 ;
const stalePidB = process.pid + 2000 ;
spawnSyncMock
.mockReturnValueOnce({
error: undefined,
status: 0 ,
stdout: [`p${stalePidA}`, "copenclaw" , `p${stalePidB}`, "copenclaw-gateway" ].join("\n" ),
})
.mockReturnValue({
error: undefined,
status: 1 ,
stdout: "" ,
});
const killSpy = vi.spyOn(process, "kill" ).mockImplementation(() => true );
const killed = cleanStaleGatewayProcessesSync();
expect(killed).toEqual([stalePidA, stalePidB]);
expect(resolveGatewayPortMock).toHaveBeenCalledWith(undefined, process.env);
expect(killSpy).toHaveBeenCalledWith(stalePidA, "SIGTERM" );
expect(killSpy).toHaveBeenCalledWith(stalePidB, "SIGTERM" );
expect(killSpy).toHaveBeenCalledWith(stalePidA, "SIGKILL" );
expect(killSpy).toHaveBeenCalledWith(stalePidB, "SIGKILL" );
});
it("uses explicit port override when provided" , () => {
const stalePid = process.pid + 1000 ;
spawnSyncMock
.mockReturnValueOnce({
error: undefined,
status: 0 ,
stdout: [`p${stalePid}`, "copenclaw" ].join("\n" ),
})
.mockReturnValue({
error: undefined,
status: 1 ,
stdout: "" ,
});
const killSpy = vi.spyOn(process, "kill" ).mockImplementation(() => true );
const killed = cleanStaleGatewayProcessesSync(19999 );
expect(killed).toEqual([stalePid]);
expect(resolveGatewayPortMock).not.toHaveBeenCalled();
expect(spawnSyncMock).toHaveBeenCalledWith(
"/usr/sbin/lsof" ,
["-nP" , "-iTCP:19999" , "-sTCP:LISTEN" , "-Fpc" ],
expect.objectContaining({ encoding: "utf8" , timeout: 2000 }),
);
expect(killSpy).toHaveBeenCalledWith(stalePid, "SIGTERM" );
expect(killSpy).toHaveBeenCalledWith(stalePid, "SIGKILL" );
});
it("returns empty when no stale listeners are found" , () => {
spawnSyncMock.mockReturnValue({
error: undefined,
status: 0 ,
stdout: "" ,
});
const killSpy = vi.spyOn(process, "kill" ).mockImplementation(() => true );
const killed = cleanStaleGatewayProcessesSync();
expect(killed).toEqual([]);
expect(killSpy).not.toHaveBeenCalled();
});
});
describe("triggerOpenClawRestart" , () => {
it("continues when launchctl bootstrap reports the service is already loaded" , () => {
setPlatform("darwin" );
delete process.env.VITEST;
delete process.env.NODE_ENV;
process.env.HOME = "/Users/test" ;
process.env.OPENCLAW_PROFILE = "default" ;
const uid = typeof process.getuid === "function" ? process.getuid() : 501 ;
spawnSyncMock.mockImplementation((command: string, args: string[]) => {
if (command === "/usr/sbin/lsof" ) {
return { error: undefined, status: 1 , stdout: "" };
}
if (command === "launchctl" && args[0 ] === "kickstart" && args[1 ] === "-k" ) {
return { error: undefined, status: 113 , stderr: "service not loaded" };
}
if (command === "launchctl" && args[0 ] === "bootstrap" ) {
return { error: undefined, status: 37 , stderr: "Operation already in progress" };
}
if (command === "launchctl" && args[0 ] === "kickstart" ) {
return { error: undefined, status: 0 , stdout: "" };
}
return { error: undefined, status: 1 , stdout: "" };
});
const result = triggerOpenClawRestart();
expect(result).toEqual({
ok: true ,
method: "launchctl" ,
tried: [
`launchctl kickstart -k gui/${uid}/ai.openclaw.gateway`,
`launchctl bootstrap gui/${uid} /Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist`,
`launchctl kickstart gui/${uid}/ai.openclaw.gateway`,
],
});
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland