



























@@ -5,9 +5,11 @@ import { 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";
77import { __setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js";
8+import type { GatewayRequestContext, RespondFn } from "./server-methods/shared-types.js";
89import {
910connectOk,
1011createGatewaySuiteHarness,
12+dispatchInboundMessageMock,
1113getReplyFromConfig,
1214installGatewayTestHooks,
1315mockGetReplyFromConfigOnce,
@@ -47,6 +49,16 @@ const sendReq = (
4749);
4850};
495152+function createDeferred<T>() {
53+let resolve!: (value: T | PromiseLike<T>) => void;
54+let reject!: (reason?: unknown) => void;
55+const promise = new Promise<T>((res, rej) => {
56+resolve = res;
57+reject = rej;
58+});
59+return { promise, resolve, reject };
60+}
61+5062async function withGatewayChatHarness(
5163run: (ctx: { ws: GatewaySocket; createSessionDir: () => Promise<string> }) => Promise<void>,
5264) {
@@ -123,6 +135,130 @@ async function prepareMainHistoryHarness(params: {
123135}
124136125137describe("gateway server chat", () => {
138+test("chat.send returns in_flight when duplicate attachment send wins parsing race", async () => {
139+const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
140+const dispatchRelease = createDeferred<void>();
141+try {
142+testState.sessionStorePath = path.join(sessionDir, "sessions.json");
143+await writeSessionStore({
144+entries: {
145+main: {
146+sessionId: "sess-main",
147+modelProvider: "test-provider",
148+model: "vision-model",
149+updatedAt: Date.now(),
150+},
151+},
152+});
153+154+const firstCatalog =
155+createDeferred<Awaited<ReturnType<GatewayRequestContext["loadGatewayModelCatalog"]>>>();
156+const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = [];
157+const context = {
158+loadGatewayModelCatalog: vi
159+.fn<GatewayRequestContext["loadGatewayModelCatalog"]>()
160+.mockImplementationOnce(() => firstCatalog.promise)
161+.mockResolvedValue([
162+{
163+id: "vision-model",
164+name: "Vision Model",
165+provider: "test-provider",
166+input: ["text", "image"],
167+},
168+]),
169+logGateway: {
170+info: vi.fn(),
171+warn: vi.fn(),
172+error: vi.fn(),
173+debug: vi.fn(),
174+},
175+agentRunSeq: new Map<string, number>(),
176+chatAbortControllers: new Map(),
177+chatAbortedRuns: new Map(),
178+chatRunBuffers: new Map(),
179+chatDeltaSentAt: new Map(),
180+chatDeltaLastBroadcastLen: new Map(),
181+addChatRun: vi.fn(),
182+removeChatRun: vi.fn(),
183+broadcast: vi.fn(),
184+nodeSendToSession: vi.fn(),
185+registerToolEventRecipient: vi.fn(),
186+dedupe: new Map(),
187+} as unknown as GatewayRequestContext;
188+dispatchInboundMessageMock.mockImplementation(async () => dispatchRelease.promise);
189+190+const pngB64 =
191+"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
192+const params = {
193+sessionKey: "main",
194+message: "see image",
195+idempotencyKey: "idem-attachment-race",
196+attachments: [
197+{
198+type: "image",
199+mimeType: "image/png",
200+fileName: "dot.png",
201+content: pngB64,
202+},
203+],
204+};
205+const { chatHandlers } = await import("./server-methods/chat.js");
206+const callSend = (id: string) =>
207+chatHandlers["chat.send"]({
208+req: { type: "req", id, method: "chat.send", params },
209+ params,
210+client: null,
211+isWebchatConnect: () => false,
212+respond: ((ok, payload, error) => {
213+responses.push({ id, ok, payload, error });
214+}) as RespondFn,
215+ context,
216+});
217+218+const first = Promise.resolve(callSend("first"));
219+await vi.waitFor(() => {
220+expect(context.loadGatewayModelCatalog).toHaveBeenCalledTimes(1);
221+}, FAST_WAIT_OPTS);
222+223+await callSend("duplicate");
224+expect(responses).toContainEqual({
225+id: "duplicate",
226+ok: true,
227+payload: { runId: "idem-attachment-race", status: "started" },
228+error: undefined,
229+});
230+231+firstCatalog.resolve([
232+{
233+id: "vision-model",
234+name: "Vision Model",
235+provider: "test-provider",
236+input: ["text", "image"],
237+},
238+]);
239+await first;
240+241+expect(responses).toContainEqual({
242+id: "first",
243+ok: true,
244+payload: { runId: "idem-attachment-race", status: "in_flight" },
245+error: undefined,
246+});
247+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
248+expect(context.addChatRun).toHaveBeenCalledTimes(1);
249+dispatchRelease.resolve();
250+await vi.waitFor(() => {
251+expect(context.removeChatRun).toHaveBeenCalledTimes(1);
252+}, FAST_WAIT_OPTS);
253+} finally {
254+dispatchRelease.resolve();
255+dispatchInboundMessageMock.mockReset();
256+testState.sessionStorePath = undefined;
257+clearConfigCache();
258+await fs.rm(sessionDir, { recursive: true, force: true });
259+}
260+});
261+126262test("chat.history backfills claude-cli sessions from Claude project files", async () => {
127263await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
128264await connectOk(ws);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。