mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-31 20:47:00 +00:00
* fix(dev): honor Gateway drain budgets in source launchers Use the shared Gateway service stop budget for foreground source runs and allow the outer watcher to finish the same drain during shutdown or replacement. Preserve the short generic-command and build deadlines and existing Mantis lifecycle authorization. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(dev): preserve active replies when stopping the Gateway Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 971c0fff-ebe1-4ae9-b9a0-519321c380e1 --------- Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import { runWatch, watchReady } from "./watch-node.test-support.js";
|
|
|
|
class FakeProcess extends EventEmitter {
|
|
execPath = process.execPath;
|
|
pid = 12345;
|
|
}
|
|
|
|
class FakeChild extends EventEmitter {
|
|
signals: string[] = [];
|
|
constructor(public pid?: number) {
|
|
super();
|
|
}
|
|
|
|
kill(signal: string): boolean {
|
|
this.signals.push(signal);
|
|
if (signal === "SIGKILL") {
|
|
this.emit("exit", null, "SIGKILL");
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
function start(
|
|
children: FakeChild[],
|
|
signalProcess?: (pid: number, signal: NodeJS.Signals | 0) => void,
|
|
env?: NodeJS.ProcessEnv,
|
|
) {
|
|
vi.useFakeTimers();
|
|
const fakeProcess = new FakeProcess();
|
|
const run = runWatch({
|
|
args: ["gateway"],
|
|
createWatcher: () => ({ close: async () => {} }),
|
|
env,
|
|
process: fakeProcess as unknown as NodeJS.Process,
|
|
signalProcess,
|
|
spawn: () => children.shift() as never,
|
|
});
|
|
return { run, fakeProcess };
|
|
}
|
|
|
|
async function expectEscalation(
|
|
{ run, fakeProcess }: ReturnType<typeof start>,
|
|
child: FakeChild,
|
|
graceMs: number,
|
|
) {
|
|
let resolvedCode: Awaited<typeof run> | undefined;
|
|
const settled = run.then((code) => (resolvedCode = code));
|
|
fakeProcess.emit("SIGTERM");
|
|
await vi.advanceTimersByTimeAsync(graceMs - 1);
|
|
expect(resolvedCode).toBeUndefined();
|
|
expect(child.signals).toEqual(["SIGTERM"]);
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await expect(settled).resolves.toBe(process.platform === "win32" ? 143 : "SIGKILL");
|
|
expect(child.signals).toEqual(["SIGTERM", "SIGKILL"]);
|
|
}
|
|
|
|
it.each([false, true])(
|
|
"waits and escalates when the child ignores SIGTERM (repair=%s)",
|
|
async (repair) => {
|
|
const runner = new FakeChild();
|
|
const child = repair ? new FakeChild() : runner;
|
|
const children = repair ? [runner, child] : [child];
|
|
const watch = start(children, undefined, repair ? {} : undefined);
|
|
await watchReady();
|
|
if (repair) {
|
|
runner.emit("exit", 1, null);
|
|
expect(children).toHaveLength(0);
|
|
await watchReady();
|
|
}
|
|
await expectEscalation(watch, child, repair ? 5_000 : 335_000);
|
|
},
|
|
);
|
|
|
|
it("force-cleans the child process group when the leader exits after shutdown", async () => {
|
|
const child = new FakeChild(4_242);
|
|
const groupSignals: Array<[number, string | number]> = [];
|
|
const { run, fakeProcess } = start([child], (pid, signal) => {
|
|
groupSignals.push([pid, signal]);
|
|
});
|
|
|
|
await watchReady();
|
|
fakeProcess.emit("SIGTERM");
|
|
expect(groupSignals).toEqual([[-4_242, "SIGTERM"]]);
|
|
child.emit("exit", 0, null);
|
|
|
|
await expect(run).resolves.toBe(143);
|
|
expect(groupSignals).toEqual([
|
|
[-4_242, "SIGTERM"],
|
|
[-4_242, "SIGKILL"],
|
|
]);
|
|
});
|