mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
test(matrix,discord,sandbox): expand breakage regression coverage
This commit is contained in:
@@ -16,13 +16,14 @@ describe("registerMatrixMonitorEvents", () => {
|
||||
sendReadReceiptMatrixMock.mockClear();
|
||||
});
|
||||
|
||||
function createHarness() {
|
||||
function createHarness(options?: { getUserId?: ReturnType<typeof vi.fn> }) {
|
||||
const handlers = new Map<string, (...args: unknown[]) => 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,19 +27,34 @@ export function registerMatrixMonitorEvents(params: {
|
||||
} = params;
|
||||
|
||||
let selfUserId: string | undefined;
|
||||
let selfUserIdLookup: Promise<string | undefined> | undefined;
|
||||
const resolveSelfUserId = async (): Promise<string | undefined> => {
|
||||
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) => {
|
||||
|
||||
@@ -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<void>();
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user