





















@@ -1,14 +1,19 @@
1-import { beforeEach, describe, expect, it, vi } from "vitest";
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+import type { InternalHookEvent } from "../hooks/internal-hooks.js";
3+4+type TriggerInternalHookMock = (event: InternalHookEvent) => Promise<void>;
2536const mocks = {
47logWarn: vi.fn(),
58disposeAgentHarnesses: vi.fn(async () => undefined),
69disposeAllSessionMcpRuntimes: vi.fn(async () => undefined),
10+triggerInternalHook: vi.fn<TriggerInternalHookMock>(async (_event) => undefined),
711};
812const WEBSOCKET_CLOSE_GRACE_MS = 1_000;
913const WEBSOCKET_CLOSE_FORCE_CONTINUE_MS = 250;
1014const HTTP_CLOSE_GRACE_MS = 1_000;
1115const HTTP_CLOSE_FORCE_WAIT_MS = 5_000;
16+const GATEWAY_LIFECYCLE_HOOK_TIMEOUT_MS = 1_000;
12171318vi.mock("../channels/plugins/index.js", async () => ({
1419 ...(await vi.importActual<typeof import("../channels/plugins/index.js")>(
@@ -21,6 +26,16 @@ vi.mock("../hooks/gmail-watcher.js", () => ({
2126stopGmailWatcher: vi.fn(async () => undefined),
2227}));
232829+vi.mock("../hooks/internal-hooks.js", async () => {
30+const actual = await vi.importActual<typeof import("../hooks/internal-hooks.js")>(
31+"../hooks/internal-hooks.js",
32+);
33+return {
34+ ...actual,
35+triggerInternalHook: mocks.triggerInternalHook,
36+};
37+});
38+2439vi.mock("../agents/harness/registry.js", () => ({
2540disposeRegisteredAgentHarnesses: mocks.disposeAgentHarnesses,
2641}));
@@ -88,6 +103,91 @@ describe("createGatewayCloseHandler", () => {
88103mocks.disposeAgentHarnesses.mockClear();
89104mocks.disposeAllSessionMcpRuntimes.mockClear();
90105mocks.disposeAllSessionMcpRuntimes.mockResolvedValue(undefined);
106+mocks.triggerInternalHook.mockReset();
107+mocks.triggerInternalHook.mockResolvedValue(undefined);
108+});
109+110+afterEach(() => {
111+vi.useRealTimers();
112+});
113+114+it("emits gateway shutdown and pre-restart hooks", async () => {
115+const close = createGatewayCloseHandler(createGatewayCloseTestDeps());
116+117+await close({ reason: "gateway restarting", restartExpectedMs: 123 });
118+119+const hookCalls = mocks.triggerInternalHook.mock.calls as unknown as Array<
120+[{ type?: string; action?: string; context?: Record<string, unknown> }]
121+>;
122+const shutdownEvent = hookCalls.find(
123+([event]) => event?.type === "gateway" && event?.action === "shutdown",
124+)?.[0];
125+const preRestartEvent = hookCalls.find(
126+([event]) => event?.type === "gateway" && event?.action === "pre-restart",
127+)?.[0];
128+129+expect(shutdownEvent?.context).toMatchObject({
130+reason: "gateway restarting",
131+restartExpectedMs: 123,
132+});
133+expect(preRestartEvent?.context).toMatchObject({
134+reason: "gateway restarting",
135+restartExpectedMs: 123,
136+});
137+});
138+139+it("continues shutdown when gateway shutdown hook stalls", async () => {
140+vi.useFakeTimers();
141+mocks.triggerInternalHook.mockImplementation((event: InternalHookEvent) => {
142+if (event.action === "shutdown") {
143+return new Promise<void>(() => undefined);
144+}
145+return Promise.resolve(undefined);
146+});
147+const stopTaskRegistryMaintenance = vi.fn();
148+const close = createGatewayCloseHandler(
149+createGatewayCloseTestDeps({ stopTaskRegistryMaintenance }),
150+);
151+152+const closePromise = close({ reason: "test shutdown" });
153+await vi.advanceTimersByTimeAsync(GATEWAY_LIFECYCLE_HOOK_TIMEOUT_MS);
154+await closePromise;
155+156+expect(stopTaskRegistryMaintenance).toHaveBeenCalledTimes(1);
157+expect(
158+mocks.logWarn.mock.calls.some(([message]) =>
159+String(message).includes("gateway:shutdown hook timed out after 1000ms"),
160+),
161+).toBe(true);
162+});
163+164+it("continues restart shutdown when gateway pre-restart hook stalls", async () => {
165+vi.useFakeTimers();
166+mocks.triggerInternalHook.mockImplementation((event: InternalHookEvent) => {
167+if (event.action === "pre-restart") {
168+return new Promise<void>(() => undefined);
169+}
170+return Promise.resolve(undefined);
171+});
172+const stopTaskRegistryMaintenance = vi.fn();
173+const close = createGatewayCloseHandler(
174+createGatewayCloseTestDeps({ stopTaskRegistryMaintenance }),
175+);
176+177+const closePromise = close({
178+reason: "test restart",
179+restartExpectedMs: 123,
180+});
181+await vi.advanceTimersByTimeAsync(GATEWAY_LIFECYCLE_HOOK_TIMEOUT_MS);
182+await closePromise;
183+184+expect(stopTaskRegistryMaintenance).toHaveBeenCalledTimes(1);
185+expect(mocks.triggerInternalHook).toHaveBeenCalledTimes(2);
186+expect(
187+mocks.logWarn.mock.calls.some(([message]) =>
188+String(message).includes("gateway:pre-restart hook timed out after 1000ms"),
189+),
190+).toBe(true);
91191});
9219293193it("unsubscribes lifecycle listeners during shutdown", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。