mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 08:02:55 +00:00
605 lines
20 KiB
TypeScript
605 lines
20 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
clearIMessageApprovalReactionTargetsForTest,
|
|
resolveIMessageApprovalReactionTargetWithPersistence,
|
|
} from "./approval-reactions.js";
|
|
import type { IMessageRpcClient } from "./client.js";
|
|
import { sendMessageIMessage } from "./send.js";
|
|
|
|
const IMESSAGE_TEST_CFG = {
|
|
channels: {
|
|
imessage: {
|
|
accounts: {
|
|
default: {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
function createClient(result: Record<string, unknown>): IMessageRpcClient {
|
|
return {
|
|
request: vi.fn(async () => result),
|
|
stop: vi.fn(async () => {}),
|
|
} as unknown as IMessageRpcClient;
|
|
}
|
|
|
|
function createRejectingClient(error: Error): IMessageRpcClient {
|
|
return {
|
|
request: vi.fn(async () => {
|
|
throw error;
|
|
}),
|
|
stop: vi.fn(async () => {}),
|
|
} as unknown as IMessageRpcClient;
|
|
}
|
|
|
|
function createApprovalText(id = "approval-123"): string {
|
|
return [
|
|
"Exec approval required",
|
|
`ID: ${id}`,
|
|
"",
|
|
`Reply with: /approve ${id} allow-once|deny`,
|
|
].join("\n");
|
|
}
|
|
|
|
describe("sendMessageIMessage receipts", () => {
|
|
afterEach(() => {
|
|
clearIMessageApprovalReactionTargetsForTest();
|
|
vi.unstubAllEnvs();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("attaches a text receipt for native send ids", async () => {
|
|
const client = createClient({ guid: "p:0/imsg-1" });
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
replyToId: "reply-1",
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/imsg-1");
|
|
expect(result.sentText).toBe("hello");
|
|
expect(result.echoText).toBe("hello");
|
|
expect(result.receipt.primaryPlatformMessageId).toBe("p:0/imsg-1");
|
|
expect(result.receipt.platformMessageIds).toEqual(["p:0/imsg-1"]);
|
|
expect(result.receipt.replyToId).toBe("reply-1");
|
|
expect(result.receipt.raw).toEqual([
|
|
{
|
|
channel: "imessage",
|
|
messageId: "p:0/imsg-1",
|
|
chatId: "42",
|
|
meta: { targetKind: "chat_id" },
|
|
},
|
|
]);
|
|
expect(result.receipt.parts).toEqual([
|
|
{
|
|
index: 0,
|
|
platformMessageId: "p:0/imsg-1",
|
|
kind: "text",
|
|
replyToId: "reply-1",
|
|
raw: {
|
|
channel: "imessage",
|
|
messageId: "p:0/imsg-1",
|
|
chatId: "42",
|
|
meta: { targetKind: "chat_id" },
|
|
},
|
|
},
|
|
]);
|
|
expect(result.receipt.sentAt).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("sends explicit chat media-only payloads through send-attachment auto transport", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ messageId: "p:0/media-guid", transferGuid: "transfer-1" });
|
|
|
|
const result = await sendMessageIMessage("chat_guid:chat-1", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/media-guid");
|
|
expect(result.sentText).toBe("");
|
|
expect(result.echoText).toBe("<media:image>");
|
|
expect(result.receipt.primaryPlatformMessageId).toBe("p:0/media-guid");
|
|
expect(result.receipt.platformMessageIds).toEqual(["p:0/media-guid"]);
|
|
expect(client.request).not.toHaveBeenCalled();
|
|
expect(runCliJson.mock.calls).toEqual([
|
|
[["send-attachment", "--chat", "chat-1", "--file", "/tmp/image.png", "--transport", "auto"]],
|
|
]);
|
|
expect(result.receipt.raw).toEqual([
|
|
{
|
|
channel: "imessage",
|
|
messageId: "p:0/media-guid",
|
|
conversationId: "chat-1",
|
|
meta: { targetKind: "chat_guid" },
|
|
},
|
|
]);
|
|
expect(result.receipt.parts).toEqual([
|
|
{
|
|
index: 0,
|
|
platformMessageId: "p:0/media-guid",
|
|
kind: "media",
|
|
raw: {
|
|
channel: "imessage",
|
|
messageId: "p:0/media-guid",
|
|
conversationId: "chat-1",
|
|
meta: { targetKind: "chat_guid" },
|
|
},
|
|
},
|
|
]);
|
|
expect(result.receipt.sentAt).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("resolves chat_id media-only payloads before using send-attachment", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ guid: "any;+;group-guid" })
|
|
.mockResolvedValueOnce({ messageId: "p:0/media-guid" });
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/media-guid");
|
|
expect(client.request).not.toHaveBeenCalled();
|
|
expect(runCliJson.mock.calls).toEqual([
|
|
[["group", "--chat-id", "42"]],
|
|
[
|
|
[
|
|
"send-attachment",
|
|
"--chat",
|
|
"any;+;group-guid",
|
|
"--file",
|
|
"/tmp/image.png",
|
|
"--transport",
|
|
"auto",
|
|
],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it("falls back to the existing rpc send path when send-attachment is unavailable", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi.fn().mockRejectedValueOnce(new Error("unknown command send-attachment"));
|
|
|
|
const result = await sendMessageIMessage("chat_guid:chat-1", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
});
|
|
|
|
expect(result.messageId).toBe("12345");
|
|
expect(runCliJson.mock.calls).toEqual([
|
|
[["send-attachment", "--chat", "chat-1", "--file", "/tmp/image.png", "--transport", "auto"]],
|
|
]);
|
|
expect(client.request).toHaveBeenCalledWith(
|
|
"send",
|
|
expect.objectContaining({
|
|
chat_guid: "chat-1",
|
|
file: "/tmp/image.png",
|
|
text: "",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("falls back to the existing rpc send path when chat_id lookup is unavailable", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi.fn().mockRejectedValueOnce(new Error("private API bridge unavailable"));
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
});
|
|
|
|
expect(result.messageId).toBe("12345");
|
|
expect(runCliJson.mock.calls).toEqual([[["group", "--chat-id", "42"]]]);
|
|
expect(client.request).toHaveBeenCalledWith(
|
|
"send",
|
|
expect.objectContaining({
|
|
chat_id: 42,
|
|
file: "/tmp/image.png",
|
|
text: "",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("rejects failed send-attachment json instead of reporting success", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ success: false, error: "attachment delivery failed" });
|
|
|
|
await expect(
|
|
sendMessageIMessage("chat_guid:chat-1", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
}),
|
|
).rejects.toThrow("attachment delivery failed");
|
|
expect(client.request).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps DM handle media sends on the existing rpc send path", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const runCliJson = vi.fn();
|
|
|
|
await sendMessageIMessage("+15551234567", "", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
mediaUrl: "/tmp/image.png",
|
|
resolveAttachmentImpl: async () => ({ path: "/tmp/image.png", contentType: "image/png" }),
|
|
runCliJson,
|
|
});
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(client.request).toHaveBeenCalledWith(
|
|
"send",
|
|
expect.objectContaining({
|
|
to: "+15551234567",
|
|
file: "/tmp/image.png",
|
|
text: "",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("preserves literal media placeholder text when no attachment is sent", async () => {
|
|
const client = createClient({ guid: "p:0/imsg-text" });
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "literal <media:image> text", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
});
|
|
|
|
expect(result.sentText).toBe("literal <media:image> text");
|
|
expect(result.echoText).toBe("literal <media:image> text");
|
|
expect(client.request).toHaveBeenCalledWith(
|
|
"send",
|
|
expect.objectContaining({
|
|
chat_id: 42,
|
|
text: "literal <media:image> text",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("does not treat compatibility ok responses as visible platform ids", async () => {
|
|
const client = createClient({ ok: "true" });
|
|
|
|
const result = await sendMessageIMessage("+15551234567", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
});
|
|
|
|
expect(result.messageId).toBe("ok");
|
|
expect(result.receipt.platformMessageIds).toStrictEqual([]);
|
|
});
|
|
|
|
it("resolves numeric chat.db ROWIDs to GUIDs for approval reaction binding", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const resolveMessageGuidImpl = vi.fn(async () => "p:0/resolved-guid");
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("12345");
|
|
expect(result.guid).toBe("p:0/resolved-guid");
|
|
expect(resolveMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
messageId: "12345",
|
|
});
|
|
});
|
|
|
|
it("does not resolve chat.db GUIDs when the bridge already returned a GUID", async () => {
|
|
const client = createClient({ guid: "p:0/native-guid" });
|
|
const resolveMessageGuidImpl = vi.fn(async () => "p:0/resolved-guid");
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/native-guid");
|
|
expect(result.guid).toBe("p:0/native-guid");
|
|
expect(resolveMessageGuidImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("leaves reaction binding unset when numeric ROWID cannot be resolved", async () => {
|
|
const client = createClient({ message_id: 12345 });
|
|
const resolveMessageGuidImpl = vi.fn(async () => null);
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("12345");
|
|
expect(result.guid).toBeUndefined();
|
|
});
|
|
|
|
it("recovers approval prompt GUID without resending when rpc send times out", async () => {
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const createClient = vi.fn(async () => client);
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/fallback-guid");
|
|
const approvalText = createApprovalText();
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
createClient,
|
|
runCliJson,
|
|
service: "sms",
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/fallback-guid");
|
|
expect(result.guid).toBe("p:0/fallback-guid");
|
|
expect(client.stop).toHaveBeenCalledOnce();
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-123"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("uses the default local chat.db path for timeout GUID recovery", async () => {
|
|
vi.stubEnv("HOME", "/Users/me");
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/default-db-guid");
|
|
const approvalText = createApprovalText("approval-default");
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
runCliJson,
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/default-db-guid");
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-default"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("uses the default local chat.db path for Homebrew imsg paths", async () => {
|
|
vi.stubEnv("HOME", "/Users/me");
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/homebrew-guid");
|
|
const approvalText = createApprovalText("approval-homebrew");
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
cliPath: "/opt/homebrew/bin/imsg",
|
|
runCliJson,
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("p:0/homebrew-guid");
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-homebrew"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("does not use the local default chat.db path for custom cliPath wrappers", async () => {
|
|
vi.useFakeTimers();
|
|
vi.stubEnv("HOME", "/Users/me");
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => null);
|
|
const approvalText = createApprovalText("approval-remote");
|
|
|
|
const send = sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: {
|
|
channels: {
|
|
imessage: {
|
|
accounts: {
|
|
default: {
|
|
remoteHost: "bot@gateway-host",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
client,
|
|
cliPath: "/Users/me/.openclaw/scripts/imsg",
|
|
runCliJson,
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
const sendExpectation = expect(send).rejects.toThrow("imsg rpc timeout (send)");
|
|
await vi.advanceTimersByTimeAsync(5_250);
|
|
await sendExpectation;
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: undefined,
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-remote"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("does not use the local default chat.db path for auto-detected ssh wrappers", async () => {
|
|
vi.useFakeTimers();
|
|
vi.stubEnv("HOME", "/Users/me");
|
|
const wrapperDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-imsg-wrapper-"));
|
|
const wrapperPath = path.join(wrapperDir, "imsg");
|
|
fs.writeFileSync(wrapperPath, '#!/bin/sh\nexec ssh -T gateway-host imsg "$@"\n');
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => null);
|
|
const approvalText = createApprovalText("approval-ssh-wrapper");
|
|
|
|
try {
|
|
const send = sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
cliPath: wrapperPath,
|
|
runCliJson,
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
const sendExpectation = expect(send).rejects.toThrow("imsg rpc timeout (send)");
|
|
await vi.advanceTimersByTimeAsync(5_250);
|
|
await sendExpectation;
|
|
} finally {
|
|
fs.rmSync(wrapperDir, { recursive: true, force: true });
|
|
}
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: undefined,
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-ssh-wrapper"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("throws the rpc timeout without resending for generic text", async () => {
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/stale-guid");
|
|
|
|
await expect(
|
|
sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
runCliJson,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveSentMessageGuidImpl,
|
|
}),
|
|
).rejects.toThrow("imsg rpc timeout (send)");
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws the rpc timeout without resending when approval GUID recovery misses", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
|
const runCliJson = vi.fn();
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => null);
|
|
const approvalText = createApprovalText();
|
|
|
|
const send = sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
runCliJson,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
const sendExpectation = expect(send).rejects.toThrow("imsg rpc timeout (send)");
|
|
await vi.advanceTimersByTimeAsync(5_250);
|
|
await sendExpectation;
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalled();
|
|
});
|
|
|
|
it("recovers a GUID for approval prompts when rpc send returns only sent status", async () => {
|
|
const client = createClient({ status: "sent" });
|
|
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/recovered-guid");
|
|
const approvalText = createApprovalText();
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
resolveSentMessageGuidImpl,
|
|
});
|
|
|
|
expect(result.messageId).toBe("ok");
|
|
expect(result.guid).toBe("p:0/recovered-guid");
|
|
await expect(
|
|
resolveIMessageApprovalReactionTargetWithPersistence({
|
|
accountId: "default",
|
|
conversation: { chatId: 42 },
|
|
messageId: "p:0/recovered-guid",
|
|
reactionKey: "👍",
|
|
}),
|
|
).resolves.toEqual({
|
|
approvalId: "approval-123",
|
|
decision: "allow-once",
|
|
});
|
|
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
|
dbPath: "/Users/me/Library/Messages/chat.db",
|
|
target: expect.objectContaining({ kind: "chat_id", chatId: 42 }),
|
|
text: expect.stringContaining("ID: approval-123"),
|
|
sentAfterMs: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("does not poll for approval prompt GUIDs when chat.db is unavailable", async () => {
|
|
const client = createClient({ status: "sent" });
|
|
const approvalText = createApprovalText();
|
|
const startedAt = performance.now();
|
|
|
|
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
dbPath: "/path/to/missing/chat.db",
|
|
});
|
|
|
|
expect(performance.now() - startedAt).toBeLessThan(250);
|
|
expect(result.messageId).toBe("ok");
|
|
expect(result.guid).toBeUndefined();
|
|
});
|
|
|
|
it("does not use one-shot imsg fallback for non-timeout rpc send errors", async () => {
|
|
const client = createRejectingClient(new Error("imsg rpc error (send)"));
|
|
const runCliJson = vi.fn();
|
|
|
|
await expect(
|
|
sendMessageIMessage("chat_id:42", "hello", {
|
|
config: IMESSAGE_TEST_CFG,
|
|
client,
|
|
runCliJson,
|
|
}),
|
|
).rejects.toThrow("imsg rpc error (send)");
|
|
|
|
expect(runCliJson).not.toHaveBeenCalled();
|
|
});
|
|
});
|