



























@@ -4,6 +4,7 @@ import path from "node:path";
44import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
55import type { GetReplyOptions } from "../auto-reply/get-reply-options.types.js";
66import { clearConfigCache } from "../config/config.js";
7+import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
78import { __setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js";
89import type { GatewayRequestContext, RespondFn } from "./server-methods/shared-types.js";
910import {
@@ -331,6 +332,113 @@ describe("gateway server chat", () => {
331332}
332333});
333334335+test("chat.send reuses an active internal run for duplicate WebChat text sends", async () => {
336+const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
337+const dispatchRelease = createDeferred<void>();
338+try {
339+testState.sessionStorePath = path.join(sessionDir, "sessions.json");
340+await writeSessionStore({
341+entries: {
342+main: {
343+sessionId: "sess-main",
344+updatedAt: Date.now(),
345+},
346+},
347+});
348+349+const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = [];
350+const context = {
351+loadGatewayModelCatalog: vi.fn<GatewayRequestContext["loadGatewayModelCatalog"]>(),
352+logGateway: {
353+info: vi.fn(),
354+warn: vi.fn(),
355+error: vi.fn(),
356+debug: vi.fn(),
357+},
358+agentRunSeq: new Map<string, number>(),
359+chatAbortControllers: new Map(),
360+chatAbortedRuns: new Map(),
361+chatRunBuffers: new Map(),
362+chatDeltaSentAt: new Map(),
363+chatDeltaLastBroadcastLen: new Map(),
364+addChatRun: vi.fn(),
365+removeChatRun: vi.fn(),
366+broadcast: vi.fn(),
367+nodeSendToSession: vi.fn(),
368+registerToolEventRecipient: vi.fn(),
369+dedupe: new Map(),
370+} as unknown as GatewayRequestContext;
371+dispatchInboundMessageMock.mockImplementation(async () => dispatchRelease.promise);
372+373+const { chatHandlers } = await import("./server-methods/chat.js");
374+const callSend = (id: string, idempotencyKey: string) =>
375+chatHandlers["chat.send"]({
376+req: {
377+type: "req",
378+ id,
379+method: "chat.send",
380+params: {
381+sessionKey: "main",
382+message: "?",
383+ idempotencyKey,
384+},
385+},
386+params: {
387+sessionKey: "main",
388+message: "?",
389+ idempotencyKey,
390+},
391+client: {
392+connect: {
393+client: {
394+id: GATEWAY_CLIENT_NAMES.CONTROL_UI,
395+mode: GATEWAY_CLIENT_MODES.WEBCHAT,
396+},
397+scopes: ["operator.write"],
398+},
399+} as never,
400+isWebchatConnect: () => true,
401+respond: ((ok, payload, error) => {
402+responses.push({ id, ok, payload, error });
403+}) as RespondFn,
404+ context,
405+});
406+407+const first = Promise.resolve(callSend("first", "idem-active-a"));
408+await vi.waitFor(() => {
409+expect(responses).toContainEqual({
410+id: "first",
411+ok: true,
412+payload: { runId: "idem-active-a", status: "started" },
413+error: undefined,
414+});
415+}, FAST_WAIT_OPTS);
416+417+await callSend("duplicate", "idem-active-b");
418+419+expect(responses).toContainEqual({
420+id: "duplicate",
421+ok: true,
422+payload: { runId: "idem-active-a", status: "in_flight" },
423+error: undefined,
424+});
425+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
426+expect(context.addChatRun).toHaveBeenCalledTimes(1);
427+428+dispatchRelease.resolve();
429+await first;
430+await vi.waitFor(() => {
431+expect(context.removeChatRun).toHaveBeenCalledTimes(1);
432+}, FAST_WAIT_OPTS);
433+} finally {
434+dispatchRelease.resolve();
435+dispatchInboundMessageMock.mockReset();
436+testState.sessionStorePath = undefined;
437+clearConfigCache();
438+await fs.rm(sessionDir, { recursive: true, force: true });
439+}
440+});
441+334442test("chat.history backfills claude-cli sessions from Claude project files", async () => {
335443await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
336444await connectOk(ws);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。