98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
getSystrayChildPid,
|
|
killSystrayUnix,
|
|
} from "../../bin/cli/tray/traySystray.mjs";
|
|
|
|
// Regression guard for the macOS orphan-NSStatusItem bug: systray2.kill(false)
|
|
// only closes the IPC channel and leaves the Go tray binary subprocess running.
|
|
// killSystrayUnix() must SIGKILL the child PID FIRST so a freshly spawned tray
|
|
// (respawn / hide-to-tray) can register a new NSStatusItem.
|
|
|
|
test("getSystrayChildPid reads pid from the _process field", () => {
|
|
const tray = { _process: { pid: 4242 } };
|
|
assert.equal(getSystrayChildPid(tray), 4242);
|
|
});
|
|
|
|
test("getSystrayChildPid falls back to the process() accessor", () => {
|
|
const tray = { process: () => ({ pid: 777 }) };
|
|
assert.equal(getSystrayChildPid(tray), 777);
|
|
});
|
|
|
|
test("getSystrayChildPid returns null when no child process is exposed", () => {
|
|
assert.equal(getSystrayChildPid({}), null);
|
|
assert.equal(getSystrayChildPid(null), null);
|
|
assert.equal(getSystrayChildPid({ _process: {} }), null);
|
|
});
|
|
|
|
test("killSystrayUnix SIGKILLs the child PID BEFORE closing IPC", () => {
|
|
const calls: string[] = [];
|
|
const origKill = process.kill;
|
|
let killedPid: number | undefined;
|
|
let killedSignal: string | number | undefined;
|
|
|
|
// @ts-expect-error - patching process.kill for the test
|
|
process.kill = (pid: number, signal?: string | number) => {
|
|
killedPid = pid;
|
|
killedSignal = signal;
|
|
calls.push("process.kill");
|
|
return true;
|
|
};
|
|
|
|
try {
|
|
const tray = {
|
|
_process: { pid: 9001 },
|
|
kill(closeOnly: boolean) {
|
|
calls.push(`tray.kill(${closeOnly})`);
|
|
},
|
|
};
|
|
|
|
killSystrayUnix(tray);
|
|
|
|
// The child PID is SIGKILLed first, then the IPC channel is closed.
|
|
assert.deepEqual(calls, ["process.kill", "tray.kill(false)"]);
|
|
assert.equal(killedPid, 9001);
|
|
assert.equal(killedSignal, "SIGKILL");
|
|
} finally {
|
|
process.kill = origKill;
|
|
}
|
|
});
|
|
|
|
test("killSystrayUnix still closes IPC when no child PID is available", () => {
|
|
const calls: string[] = [];
|
|
const origKill = process.kill;
|
|
|
|
// @ts-expect-error - patching process.kill for the test
|
|
process.kill = () => {
|
|
calls.push("process.kill");
|
|
return true;
|
|
};
|
|
|
|
try {
|
|
const tray = {
|
|
kill(closeOnly: boolean) {
|
|
calls.push(`tray.kill(${closeOnly})`);
|
|
},
|
|
};
|
|
|
|
killSystrayUnix(tray);
|
|
|
|
// No PID known -> never calls process.kill, but still closes IPC.
|
|
assert.deepEqual(calls, ["tray.kill(false)"]);
|
|
} finally {
|
|
process.kill = origKill;
|
|
}
|
|
});
|
|
|
|
test("killSystrayUnix swallows errors from a broken tray instance", () => {
|
|
const tray = {
|
|
kill() {
|
|
throw new Error("boom");
|
|
},
|
|
};
|
|
// Must not throw.
|
|
assert.doesNotThrow(() => killSystrayUnix(tray));
|
|
});
|