

























@@ -5,8 +5,15 @@ import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js";
55import type { ActivityEntry } from "./activity-model.ts";
66import { connectGateway, resolveControlUiClientVersion } from "./app-gateway.ts";
77import type { GatewayHelloOk } from "./gateway.ts";
8+import type { ChatQueueItem } from "./ui-types.ts";
89910const loadChatHistoryMock = vi.hoisted(() => vi.fn(async () => undefined));
11+const loadChatComposerSnapshotMock = vi.hoisted(() =>
12+vi.fn<(...args: unknown[]) => { draft: string; queue: ChatQueueItem[] } | null>(() => null),
13+);
14+const restoreChatComposerStateMock = vi.hoisted(() =>
15+vi.fn<(...args: unknown[]) => boolean>(() => false),
16+);
1017const loadControlUiBootstrapConfigMock = vi.hoisted(() => vi.fn(async () => undefined));
11181219type GatewayRequest = (method: string, payload?: unknown) => Promise<unknown>;
@@ -118,6 +125,15 @@ vi.mock("./controllers/control-ui-bootstrap.ts", () => ({
118125loadControlUiBootstrapConfig: loadControlUiBootstrapConfigMock,
119126}));
120127128+vi.mock("./chat/composer-persistence.ts", async (importOriginal) => {
129+const actual = await importOriginal<typeof import("./chat/composer-persistence.ts")>();
130+return {
131+ ...actual,
132+loadChatComposerSnapshot: loadChatComposerSnapshotMock,
133+restoreChatComposerState: restoreChatComposerStateMock,
134+};
135+});
136+121137type TestGatewayHost = Parameters<typeof connectGateway>[0] & {
122138chatMessages: unknown[];
123139chatQueue: import("./ui-types.ts").ChatQueueItem[];
@@ -172,7 +188,9 @@ function createHost(): TestGatewayHost {
172188updateStatusBanner: null,
173189sessionKey: "main",
174190chatMessages: [],
191+chatMessage: "",
175192chatQueue: [],
193+chatComposerProvisionalRestore: null,
176194chatQueueBySession: {},
177195chatToolMessages: [],
178196activityEntries: [],
@@ -245,6 +263,10 @@ describe("connectGateway", () => {
245263beforeEach(() => {
246264gatewayClientInstances.length = 0;
247265loadChatHistoryMock.mockClear();
266+loadChatComposerSnapshotMock.mockReset();
267+loadChatComposerSnapshotMock.mockReturnValue(null);
268+restoreChatComposerStateMock.mockReset();
269+restoreChatComposerStateMock.mockReturnValue(false);
248270loadControlUiBootstrapConfigMock.mockClear();
249271vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) =>
250272setTimeout(() => callback(Date.now()), 0),
@@ -275,6 +297,79 @@ describe("connectGateway", () => {
275297expect(host.lastError).toBeNull();
276298});
277299300+it("lets hello-scoped composer state replace an unchanged provisional restore", () => {
301+const { host, client } = connectHostGateway();
302+const provisionalQueue = [{ id: "queued-old", text: "wrong agent", createdAt: 1 }];
303+const scopedQueue = [{ id: "queued-new", text: "right agent", createdAt: 2 }];
304+host.chatMessage = "fallback draft";
305+host.chatQueue = provisionalQueue;
306+host.chatComposerProvisionalRestore = {
307+sessionKey: "main",
308+chatMessage: "fallback draft",
309+chatQueue: provisionalQueue,
310+};
311+loadChatComposerSnapshotMock.mockReturnValueOnce({
312+draft: "scoped draft",
313+queue: scopedQueue,
314+});
315+restoreChatComposerStateMock.mockImplementationOnce((target: unknown) => {
316+const hostTarget = target as typeof host;
317+expect(hostTarget.chatMessage).toBe("");
318+expect(hostTarget.chatQueue).toEqual([]);
319+hostTarget.chatMessage = "scoped draft";
320+hostTarget.chatQueue = scopedQueue;
321+return true;
322+});
323+324+client.emitHello({
325+type: "hello-ok",
326+protocol: 4,
327+snapshot: {
328+sessionDefaults: {
329+defaultAgentId: "agent-b",
330+mainKey: "main",
331+mainSessionKey: "agent:agent-b:main",
332+},
333+},
334+auth: { role: "operator", scopes: [] },
335+});
336+337+expect(loadChatComposerSnapshotMock).toHaveBeenCalledWith(host, "agent:agent-b:main");
338+expect(host.sessionKey).toBe("agent:agent-b:main");
339+expect(host.chatMessage).toBe("scoped draft");
340+expect(host.chatQueue).toBe(scopedQueue);
341+expect(host.chatComposerProvisionalRestore).toBeNull();
342+});
343+344+it("keeps a provisional composer restore when the user edited before hello", () => {
345+const { host, client } = connectHostGateway();
346+const provisionalQueue = [{ id: "queued-old", text: "offline", createdAt: 1 }];
347+host.chatMessage = "user edit";
348+host.chatQueue = provisionalQueue;
349+host.chatComposerProvisionalRestore = {
350+sessionKey: "main",
351+chatMessage: "fallback draft",
352+chatQueue: provisionalQueue,
353+};
354+loadChatComposerSnapshotMock.mockReturnValueOnce({
355+draft: "scoped draft",
356+queue: [{ id: "queued-new", text: "right agent", createdAt: 2 }],
357+});
358+restoreChatComposerStateMock.mockImplementationOnce((target: unknown) => {
359+const hostTarget = target as typeof host;
360+expect(hostTarget.chatMessage).toBe("user edit");
361+expect(hostTarget.chatQueue).toBe(provisionalQueue);
362+return false;
363+});
364+365+client.emitHello();
366+367+expect(loadChatComposerSnapshotMock).not.toHaveBeenCalled();
368+expect(host.chatMessage).toBe("user edit");
369+expect(host.chatQueue).toBe(provisionalQueue);
370+expect(host.chatComposerProvisionalRestore).toBeNull();
371+});
372+278373it("ignores stale client onEvent callbacks after reconnect", () => {
279374const host = createHost();
280375@@ -863,15 +958,15 @@ describe("connectGateway", () => {
863958expect(host.lastError).toBe("disconnected (1006): no reason");
864959});
865960866-it("refreshes bootstrap config after hello", () => {
961+it("refreshes bootstrap config after hello", async () => {
867962const host = createHost();
868963869964connectGateway(host);
870965const client = requireGatewayClient();
871966872967client.emitHello();
873968874-expect(loadControlUiBootstrapConfigMock).toHaveBeenCalledTimes(1);
969+await vi.waitFor(() => expect(loadControlUiBootstrapConfigMock).toHaveBeenCalledTimes(1));
875970expect(loadControlUiBootstrapConfigMock).toHaveBeenCalledWith(host, { applyIdentity: false });
876971});
877972此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。