

















@@ -1,6 +1,77 @@
11import { describe, expect, it, vi } from "vitest";
22import type { GatewayServerLiveState } from "./server-live-state.js";
3-import { createGatewayRequestContext } from "./server-request-context.js";
3+import {
4+createGatewayRequestContext,
5+type GatewayRequestContextParams,
6+} from "./server-request-context.js";
7+8+function makeContextParams(
9+overrides: Partial<GatewayRequestContextParams> = {},
10+): GatewayRequestContextParams {
11+const runtimeState: Pick<GatewayServerLiveState, "cronState"> = {
12+cronState: {
13+cron: { start: vi.fn(), stop: vi.fn() } as never,
14+storePath: "/tmp/cron",
15+cronEnabled: true,
16+},
17+};
18+return {
19+deps: {} as never,
20+ runtimeState,
21+getRuntimeConfig: vi.fn(() => ({}) as never),
22+execApprovalManager: undefined,
23+pluginApprovalManager: undefined,
24+loadGatewayModelCatalog: vi.fn(async () => []),
25+getHealthCache: vi.fn(() => null),
26+refreshHealthSnapshot: vi.fn(async () => ({}) as never),
27+logHealth: { error: vi.fn() },
28+logGateway: { warn: vi.fn(), info: vi.fn(), error: vi.fn() } as never,
29+incrementPresenceVersion: vi.fn(() => 1),
30+getHealthVersion: vi.fn(() => 1),
31+broadcast: vi.fn(),
32+broadcastToConnIds: vi.fn(),
33+nodeSendToSession: vi.fn(),
34+nodeSendToAllSubscribed: vi.fn(),
35+nodeSubscribe: vi.fn(),
36+nodeUnsubscribe: vi.fn(),
37+nodeUnsubscribeAll: vi.fn(),
38+hasConnectedTalkNode: vi.fn(() => false),
39+clients: new Set(),
40+enforceSharedGatewayAuthGenerationForConfigWrite: vi.fn(),
41+nodeRegistry: {} as never,
42+agentRunSeq: new Map(),
43+chatAbortControllers: new Map(),
44+chatAbortedRuns: new Map(),
45+chatRunBuffers: new Map(),
46+chatDeltaSentAt: new Map(),
47+chatDeltaLastBroadcastLen: new Map(),
48+chatDeltaLastBroadcastText: new Map(),
49+agentDeltaSentAt: new Map(),
50+bufferedAgentEvents: new Map(),
51+addChatRun: vi.fn(),
52+removeChatRun: vi.fn(),
53+subscribeSessionEvents: vi.fn(),
54+unsubscribeSessionEvents: vi.fn(),
55+subscribeSessionMessageEvents: vi.fn(),
56+unsubscribeSessionMessageEvents: vi.fn(),
57+unsubscribeAllSessionEvents: vi.fn(),
58+getSessionEventSubscriberConnIds: vi.fn(() => new Set<string>()),
59+registerToolEventRecipient: vi.fn(),
60+dedupe: new Map(),
61+wizardSessions: new Map(),
62+findRunningWizard: vi.fn(() => null),
63+purgeWizardSession: vi.fn(),
64+getRuntimeSnapshot: vi.fn(() => ({}) as never),
65+startChannel: vi.fn(async () => undefined),
66+stopChannel: vi.fn(async () => undefined),
67+markChannelLoggedOut: vi.fn(),
68+wizardRunner: vi.fn(async () => undefined),
69+broadcastVoiceWakeChanged: vi.fn(),
70+broadcastVoiceWakeRoutingChanged: vi.fn(),
71+unavailableGatewayMethods: new Set(),
72+ ...overrides,
73+};
74+}
475576describe("createGatewayRequestContext", () => {
677it("reads cron state live from runtime state", () => {
@@ -82,4 +153,66 @@ describe("createGatewayRequestContext", () => {
82153expect(context.cron).toBe(cronB);
83154expect(context.cronStorePath).toBe("/tmp/cron-b");
84155});
156+157+it("invalidateClientsForDevice sets the flag on matching clients without closing the socket", () => {
158+const target = {
159+connId: "conn-target",
160+connect: { device: { id: "device-1" }, role: "primary" },
161+socket: { close: vi.fn() },
162+};
163+const unrelated = {
164+connId: "conn-unrelated",
165+connect: { device: { id: "device-2" }, role: "primary" },
166+socket: { close: vi.fn() },
167+};
168+const clients = new Set([target, unrelated]) as never;
169+170+const context = createGatewayRequestContext(makeContextParams({ clients }));
171+context.invalidateClientsForDevice?.("device-1", { reason: "device-token-rotated" });
172+173+expect((target as { invalidated?: boolean }).invalidated).toBe(true);
174+expect((target as { invalidatedReason?: string }).invalidatedReason).toBe(
175+"device-token-rotated",
176+);
177+expect(target.socket.close).not.toHaveBeenCalled();
178+179+expect((unrelated as { invalidated?: boolean }).invalidated).toBeUndefined();
180+expect(unrelated.socket.close).not.toHaveBeenCalled();
181+});
182+183+it("disconnectClientsForDevice also marks the invalidated flag before closing", () => {
184+const target = {
185+connId: "conn-target",
186+connect: { device: { id: "device-1" }, role: "primary" },
187+socket: { close: vi.fn() },
188+};
189+const clients = new Set([target]) as never;
190+191+const context = createGatewayRequestContext(makeContextParams({ clients }));
192+context.disconnectClientsForDevice?.("device-1");
193+194+expect((target as { invalidated?: boolean }).invalidated).toBe(true);
195+expect((target as { invalidatedReason?: string }).invalidatedReason).toBe("device-removed");
196+expect(target.socket.close).toHaveBeenCalledWith(4001, "device removed");
197+});
198+199+it("invalidateClientsForDevice filters by role when provided", () => {
200+const primary = {
201+connId: "conn-primary",
202+connect: { device: { id: "device-1" }, role: "primary" },
203+socket: { close: vi.fn() },
204+};
205+const secondary = {
206+connId: "conn-secondary",
207+connect: { device: { id: "device-1" }, role: "secondary" },
208+socket: { close: vi.fn() },
209+};
210+const clients = new Set([primary, secondary]) as never;
211+212+const context = createGatewayRequestContext(makeContextParams({ clients }));
213+context.invalidateClientsForDevice?.("device-1", { role: "primary" });
214+215+expect((primary as { invalidated?: boolean }).invalidated).toBe(true);
216+expect((secondary as { invalidated?: boolean }).invalidated).toBeUndefined();
217+});
85218});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。