




















@@ -0,0 +1,155 @@
1+import { describe, expect, it, vi } from "vitest";
2+import { isIMessageAnchorless, repairIMessageConversationAnchor } from "./conversation-repair.js";
3+import type { IMessagePayload } from "./types.js";
4+5+function anchorlessMessage(overrides: Partial<IMessagePayload> = {}): IMessagePayload {
6+return {
7+id: 9500,
8+guid: "ANCHORLESS-GUID-1",
9+chat_id: 0,
10+sender: "+15550001111",
11+is_from_me: false,
12+text: "https://example.com",
13+chat_guid: "",
14+chat_identifier: "",
15+chat_name: "",
16+participants: null,
17+is_group: false,
18+ ...overrides,
19+};
20+}
21+22+function mockClient(chats: Array<{ id: number; messages: Record<string, unknown>[] }>) {
23+const request = vi.fn(async (method: string, params?: Record<string, unknown>) => {
24+if (method === "chats.list") {
25+return { chats: chats.map((chat) => ({ id: chat.id })) };
26+}
27+if (method === "messages.history") {
28+return {
29+messages: chats.find((chat) => chat.id === params?.chat_id)?.messages ?? [],
30+};
31+}
32+throw new Error(`unexpected method ${method}`);
33+});
34+return { request };
35+}
36+37+describe("isIMessageAnchorless", () => {
38+it("detects explicit broken conversation anchors", () => {
39+expect(isIMessageAnchorless(anchorlessMessage())).toBe(true);
40+expect(isIMessageAnchorless(anchorlessMessage({ chat_guid: undefined }))).toBe(true);
41+expect(isIMessageAnchorless(anchorlessMessage({ chat_identifier: undefined }))).toBe(true);
42+expect(
43+isIMessageAnchorless(
44+anchorlessMessage({ chat_id: undefined, chat_guid: "", chat_identifier: "" }),
45+),
46+).toBe(true);
47+});
48+49+it("does not classify sender-only direct messages as anchorless", () => {
50+expect(
51+isIMessageAnchorless({
52+guid: "DM-GUID",
53+sender: "+15550001111",
54+is_from_me: false,
55+text: "hello",
56+}),
57+).toBe(false);
58+});
59+60+it("does not classify messages with any usable conversation anchor", () => {
61+expect(isIMessageAnchorless(anchorlessMessage({ chat_id: 349 }))).toBe(false);
62+expect(isIMessageAnchorless(anchorlessMessage({ chat_guid: "iMessage;+;chat349" }))).toBe(
63+false,
64+);
65+expect(isIMessageAnchorless(anchorlessMessage({ chat_identifier: "chat349" }))).toBe(false);
66+});
67+});
68+69+describe("repairIMessageConversationAnchor", () => {
70+it("passes through non-anchorless messages without recovery RPCs", async () => {
71+const message = anchorlessMessage({ chat_id: 349, is_group: true });
72+const client = mockClient([]);
73+74+await expect(
75+repairIMessageConversationAnchor({ client: client as never, message }),
76+).resolves.toBe(message);
77+expect(client.request).not.toHaveBeenCalled();
78+});
79+80+it("recovers the conversation from recent history by GUID", async () => {
81+const message = anchorlessMessage();
82+const client = mockClient([
83+{ id: 100, messages: [{ guid: "OTHER-GUID", chat_id: 100, is_group: true }] },
84+{
85+id: 349,
86+messages: [
87+{
88+guid: "ANCHORLESS-GUID-1",
89+chat_id: 349,
90+chat_guid: "iMessage;+;chat349",
91+chat_identifier: "chat349",
92+chat_name: "Project group",
93+participants: ["+15550001111", "+15550002222"],
94+is_group: true,
95+},
96+],
97+},
98+]);
99+100+const repaired = await repairIMessageConversationAnchor({
101+client: client as never,
102+ message,
103+});
104+105+expect(repaired).toMatchObject({
106+chat_id: 349,
107+chat_guid: "iMessage;+;chat349",
108+chat_identifier: "chat349",
109+chat_name: "Project group",
110+participants: ["+15550001111", "+15550002222"],
111+is_group: true,
112+});
113+});
114+115+it("drops fail-closed when the GUID cannot be matched", async () => {
116+const runtime = { error: vi.fn() };
117+const client = mockClient([{ id: 349, messages: [{ guid: "OTHER-GUID", chat_id: 349 }] }]);
118+119+await expect(
120+repairIMessageConversationAnchor({
121+client: client as never,
122+message: anchorlessMessage(),
123+ runtime,
124+}),
125+).resolves.toBeNull();
126+expect(runtime.error.mock.calls.at(-1)?.[0]).toContain("no recent chat matched");
127+});
128+129+it("drops fail-closed when history finds the GUID but no usable anchor", async () => {
130+const runtime = { error: vi.fn() };
131+const client = mockClient([
132+{
133+id: 349,
134+messages: [
135+{
136+guid: "ANCHORLESS-GUID-1",
137+chat_id: 0,
138+chat_guid: "",
139+chat_identifier: "",
140+is_group: false,
141+},
142+],
143+},
144+]);
145+146+await expect(
147+repairIMessageConversationAnchor({
148+client: client as never,
149+message: anchorlessMessage(),
150+ runtime,
151+}),
152+).resolves.toBeNull();
153+expect(runtime.error.mock.calls.at(-1)?.[0]).toContain("no usable conversation anchor");
154+});
155+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。