diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index dbd2245046d..3754cfd178e 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -16,13 +16,14 @@ describe("registerMatrixMonitorEvents", () => { sendReadReceiptMatrixMock.mockClear(); }); - function createHarness() { + function createHarness(options?: { getUserId?: ReturnType }) { const handlers = new Map void>(); + const getUserId = options?.getUserId ?? vi.fn().mockResolvedValue("@bot:example.org"); const client = { on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { handlers.set(event, handler); }), - getUserId: vi.fn().mockResolvedValue("@bot:example.org"), + getUserId, crypto: undefined, } as unknown as MatrixClient; @@ -49,7 +50,7 @@ describe("registerMatrixMonitorEvents", () => { throw new Error("missing room.message handler"); } - return { client, onRoomMessage, roomMessageHandler }; + return { client, getUserId, onRoomMessage, roomMessageHandler, logVerboseMessage }; } it("sends read receipt immediately for non-self messages", async () => { @@ -93,4 +94,48 @@ describe("registerMatrixMonitorEvents", () => { }); expect(sendReadReceiptMatrixMock).not.toHaveBeenCalled(); }); + + it("caches self user id across messages", async () => { + const { getUserId, roomMessageHandler } = createHarness(); + const first = { event_id: "$e3", sender: "@alice:example.org" } as MatrixRawEvent; + const second = { event_id: "$e4", sender: "@bob:example.org" } as MatrixRawEvent; + + roomMessageHandler("!room:example.org", first); + roomMessageHandler("!room:example.org", second); + + await vi.waitFor(() => { + expect(sendReadReceiptMatrixMock).toHaveBeenCalledTimes(2); + }); + expect(getUserId).toHaveBeenCalledTimes(1); + }); + + it("logs and continues when sending read receipt fails", async () => { + sendReadReceiptMatrixMock.mockRejectedValueOnce(new Error("network boom")); + const { roomMessageHandler, onRoomMessage, logVerboseMessage } = createHarness(); + const event = { event_id: "$e5", sender: "@alice:example.org" } as MatrixRawEvent; + + roomMessageHandler("!room:example.org", event); + + await vi.waitFor(() => { + expect(onRoomMessage).toHaveBeenCalledWith("!room:example.org", event); + expect(logVerboseMessage).toHaveBeenCalledWith( + expect.stringContaining("matrix: early read receipt failed"), + ); + }); + }); + + it("skips read receipts if self-user lookup fails", async () => { + const { roomMessageHandler, onRoomMessage, getUserId } = createHarness({ + getUserId: vi.fn().mockRejectedValue(new Error("cannot resolve self")), + }); + const event = { event_id: "$e6", sender: "@alice:example.org" } as MatrixRawEvent; + + roomMessageHandler("!room:example.org", event); + + await vi.waitFor(() => { + expect(onRoomMessage).toHaveBeenCalledWith("!room:example.org", event); + }); + expect(getUserId).toHaveBeenCalledTimes(1); + expect(sendReadReceiptMatrixMock).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index ab548ef18c2..1f64f955851 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -27,19 +27,34 @@ export function registerMatrixMonitorEvents(params: { } = params; let selfUserId: string | undefined; + let selfUserIdLookup: Promise | undefined; + const resolveSelfUserId = async (): Promise => { + if (selfUserId) { + return selfUserId; + } + if (!selfUserIdLookup) { + selfUserIdLookup = client + .getUserId() + .then((userId) => { + selfUserId = userId; + return userId; + }) + .catch(() => undefined) + .finally(() => { + if (!selfUserId) { + selfUserIdLookup = undefined; + } + }); + } + return await selfUserIdLookup; + }; client.on("room.message", (roomId: string, event: MatrixRawEvent) => { const eventId = event?.event_id; const senderId = event?.sender; if (eventId && senderId) { void (async () => { - if (!selfUserId) { - try { - selfUserId = await client.getUserId(); - } catch { - return; - } - } - if (senderId === selfUserId) { + const currentSelfUserId = await resolveSelfUserId(); + if (!currentSelfUserId || senderId === currentSelfUserId) { return; } await sendReadReceiptMatrix(roomId, eventId, client).catch((err) => { diff --git a/extensions/matrix/src/matrix/send-queue.test.ts b/extensions/matrix/src/matrix/send-queue.test.ts index 34e6e30166c..508a01d301e 100644 --- a/extensions/matrix/src/matrix/send-queue.test.ts +++ b/extensions/matrix/src/matrix/send-queue.test.ts @@ -86,4 +86,34 @@ describe("enqueueSend", () => { await vi.advanceTimersByTimeAsync(150); await expect(second).resolves.toBe("ok"); }); + + it("continues queued work when the head task fails", async () => { + const gate = deferred(); + const events: string[] = []; + + const first = enqueueSend("!room:example.org", async () => { + events.push("start1"); + await gate.promise; + throw new Error("boom"); + }).then( + () => ({ ok: true as const }), + (error) => ({ ok: false as const, error }), + ); + const second = enqueueSend("!room:example.org", async () => { + events.push("start2"); + return "two"; + }); + + await vi.advanceTimersByTimeAsync(150); + expect(events).toEqual(["start1"]); + + gate.resolve(); + const firstResult = await first; + expect(firstResult.ok).toBe(false); + expect(firstResult.error).toBeInstanceOf(Error); + + await vi.advanceTimersByTimeAsync(150); + await expect(second).resolves.toBe("two"); + expect(events).toEqual(["start1", "start2"]); + }); }); diff --git a/src/agents/sandbox/fs-bridge.test.ts b/src/agents/sandbox/fs-bridge.test.ts index 982d3cbf6a5..a4ae727b330 100644 --- a/src/agents/sandbox/fs-bridge.test.ts +++ b/src/agents/sandbox/fs-bridge.test.ts @@ -91,6 +91,22 @@ describe("sandbox fs bridge shell compatibility", () => { expect(canonicalScript).toBeDefined(); // "; " joining can create "do; cmd", which is invalid in POSIX sh. expect(canonicalScript).not.toMatch(/\bdo;/); + // Keep command on the next line after "do" for POSIX-sh safety. + expect(canonicalScript).toMatch(/\bdo\n\s*parent=/); + }); + + it("reads inbound media-style filenames with triple-dash ids", async () => { + const bridge = createSandboxFsBridge({ sandbox: createSandbox() }); + const inboundPath = "media/inbound/file_1095---f00a04a2-99a0-4d98-99b0-dfe61c5a4198.ogg"; + + await bridge.readFile({ filePath: inboundPath }); + + const readCall = mockedExecDockerRaw.mock.calls.find(([args]) => + String(args[5] ?? "").includes('cat -- "$1"'), + ); + expect(readCall).toBeDefined(); + const readPath = String(readCall?.[0].at(-1) ?? ""); + expect(readPath).toContain("file_1095---"); }); it("resolves bind-mounted absolute container paths for reads", async () => { diff --git a/src/discord/monitor/message-handler.process.test.ts b/src/discord/monitor/message-handler.process.test.ts index 79af5ffa477..60eade41f3d 100644 --- a/src/discord/monitor/message-handler.process.test.ts +++ b/src/discord/monitor/message-handler.process.test.ts @@ -444,6 +444,24 @@ describe("processDiscordMessage draft streaming", () => { expect(deliverDiscordReply).not.toHaveBeenCalled(); }); + it("suppresses reasoning-tagged final payload delivery to Discord", async () => { + dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { + await params?.dispatcher.sendFinalReply({ + text: "Reasoning:\nthis should stay internal", + isReasoning: true, + } as never); + return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } }; + }); + + const ctx = await createBaseContext({ discordConfig: { streamMode: "off" } }); + + // oxlint-disable-next-line typescript/no-explicit-any + await processDiscordMessage(ctx as any); + + expect(deliverDiscordReply).not.toHaveBeenCalled(); + expect(editMessageDiscord).not.toHaveBeenCalled(); + }); + it("delivers non-reasoning block payloads to Discord", async () => { dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { await params?.dispatcher.sendBlockReply({ text: "hello from block stream" });