























@@ -256,6 +256,183 @@ describe("iMessage monitor last-route updates", () => {
256256});
257257});
258258259+it("keeps direct progress options when imsg lacks native typing support", async () => {
260+setCachedIMessagePrivateApiStatus("imsg", {
261+available: true,
262+v2Ready: true,
263+selectors: {},
264+rpcMethods: ["watch.subscribe", "send", "read"],
265+});
266+dispatchInboundMessageMock.mockImplementationOnce(async (params) => {
267+expect(params.replyOptions?.suppressDefaultToolProgressMessages).toBe(true);
268+expect(params.replyOptions?.allowProgressCallbacksWhenSourceDeliverySuppressed).toBe(true);
269+expect(params.replyOptions?.onToolStart).toBeUndefined();
270+return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } } as const;
271+});
272+273+let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
274+const client = {
275+request: vi.fn(async (method: string) => {
276+if (method === "watch.subscribe") {
277+return { subscription: 1 };
278+}
279+if (method === "typing") {
280+throw new Error("typing should not start without native typing support");
281+}
282+throw new Error(`unexpected imsg method ${method}`);
283+}),
284+waitForClose: vi.fn(async () => {
285+onNotification?.({
286+method: "message",
287+params: {
288+message: {
289+id: 13,
290+chat_id: 123,
291+sender: "+15550001111",
292+is_from_me: false,
293+text: "run a long script without native typing",
294+is_group: false,
295+created_at: new Date().toISOString(),
296+},
297+},
298+});
299+await Promise.resolve();
300+await Promise.resolve();
301+}),
302+stop: vi.fn(async () => {}),
303+};
304+createIMessageRpcClientMock.mockImplementation(async (params) => {
305+if (!params?.onNotification) {
306+throw new Error("expected iMessage notification handler");
307+}
308+onNotification = params.onNotification;
309+return client as never;
310+});
311+312+await monitorIMessageProvider({
313+config: {
314+channels: {
315+imessage: {
316+dmPolicy: "allowlist",
317+allowFrom: ["+15550001111"],
318+sendReadReceipts: false,
319+},
320+},
321+messages: { inbound: { debounceMs: 0 } },
322+session: { mainKey: "main" },
323+} as never,
324+runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
325+});
326+327+await vi.waitFor(() => {
328+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
329+});
330+expect(client.request).not.toHaveBeenCalledWith(
331+"typing",
332+expect.objectContaining({ typing: true }),
333+expect.anything(),
334+);
335+});
336+337+it("starts direct typing before dispatching the inbound turn", async () => {
338+setCachedIMessagePrivateApiStatus("imsg", {
339+available: true,
340+v2Ready: true,
341+selectors: {},
342+rpcMethods: ["watch.subscribe", "send", "typing"],
343+});
344+345+let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
346+const earlyTypingClient = {
347+request: vi.fn(async (method: string) => {
348+if (method === "typing") {
349+return { ok: true };
350+}
351+throw new Error(`unexpected imsg typing-client method ${method}`);
352+}),
353+stop: vi.fn(async () => {}),
354+};
355+const watchClient = {
356+request: vi.fn(async (method: string) => {
357+if (method === "watch.subscribe") {
358+return { subscription: 1 };
359+}
360+if (method === "typing") {
361+return { ok: true };
362+}
363+throw new Error(`unexpected imsg watch-client method ${method}`);
364+}),
365+waitForClose: vi.fn(async () => {
366+onNotification?.({
367+method: "message",
368+params: {
369+message: {
370+id: 12,
371+chat_id: 123,
372+sender: "+15550001111",
373+is_from_me: false,
374+text: "respond after a slow context build",
375+is_group: false,
376+created_at: new Date().toISOString(),
377+},
378+},
379+});
380+await vi.waitFor(() => {
381+expect(earlyTypingClient.request).toHaveBeenCalledWith(
382+"typing",
383+expect.objectContaining({ typing: true, to: "+15550001111" }),
384+expect.any(Object),
385+);
386+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
387+});
388+}),
389+stop: vi.fn(async () => {}),
390+};
391+createIMessageRpcClientMock.mockImplementation(async (params) => {
392+if (params?.onNotification) {
393+onNotification = params.onNotification;
394+return watchClient as never;
395+}
396+return earlyTypingClient as never;
397+});
398+dispatchInboundMessageMock.mockImplementationOnce(async () => {
399+expect(earlyTypingClient.request).toHaveBeenCalledWith(
400+"typing",
401+expect.objectContaining({ typing: true, to: "+15550001111" }),
402+expect.any(Object),
403+);
404+return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } } as const;
405+});
406+407+await monitorIMessageProvider({
408+config: {
409+channels: {
410+imessage: {
411+dmPolicy: "allowlist",
412+allowFrom: ["+15550001111"],
413+sendReadReceipts: false,
414+},
415+},
416+messages: { inbound: { debounceMs: 0 } },
417+session: { mainKey: "main" },
418+} as never,
419+runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
420+});
421+422+expect(watchClient.request).not.toHaveBeenCalledWith(
423+"typing",
424+expect.objectContaining({ typing: true }),
425+expect.anything(),
426+);
427+await vi.waitFor(() => {
428+expect(earlyTypingClient.request).toHaveBeenCalledWith(
429+"typing",
430+expect.objectContaining({ typing: false, to: "+15550001111" }),
431+expect.any(Object),
432+);
433+});
434+});
435+259436it.each(["never", "message", "thinking"] as const)(
260437"does not start direct tool typing when typingMode is %s",
261438async (typingMode) => {
@@ -420,6 +597,87 @@ describe("iMessage monitor last-route updates", () => {
420597);
421598});
422599600+it("does not wait for read receipts before dispatching the inbound turn", async () => {
601+setCachedIMessagePrivateApiStatus("imsg", {
602+available: true,
603+v2Ready: true,
604+selectors: {},
605+rpcMethods: ["watch.subscribe", "read"],
606+});
607+608+let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
609+const readClient = {
610+request: vi.fn((method: string) => {
611+if (method === "read") {
612+return new Promise(() => {});
613+}
614+return Promise.reject(new Error(`unexpected imsg read-client method ${method}`));
615+}),
616+stop: vi.fn(async () => {}),
617+};
618+const watchClient = {
619+request: vi.fn((method: string) => {
620+if (method === "watch.subscribe") {
621+return Promise.resolve({ subscription: 1 });
622+}
623+return Promise.reject(new Error(`unexpected imsg watch-client method ${method}`));
624+}),
625+waitForClose: vi.fn(async () => {
626+onNotification?.({
627+method: "message",
628+params: {
629+message: {
630+id: 11,
631+chat_id: 123,
632+sender: "+15550001111",
633+is_from_me: false,
634+text: "respond without waiting for read receipt",
635+is_group: false,
636+created_at: new Date().toISOString(),
637+},
638+},
639+});
640+await vi.waitFor(() => {
641+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
642+});
643+}),
644+stop: vi.fn(async () => {}),
645+};
646+createIMessageRpcClientMock.mockImplementation(async (params) => {
647+if (params?.onNotification) {
648+onNotification = params.onNotification;
649+return watchClient as never;
650+}
651+return readClient as never;
652+});
653+654+await monitorIMessageProvider({
655+config: {
656+channels: {
657+imessage: {
658+dmPolicy: "allowlist",
659+allowFrom: ["+15550001111"],
660+},
661+},
662+messages: { inbound: { debounceMs: 0 } },
663+session: { mainKey: "main" },
664+} as never,
665+runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
666+});
667+668+expect(readClient.request).toHaveBeenCalledWith(
669+"read",
670+expect.objectContaining({ to: "+15550001111" }),
671+expect.any(Object),
672+);
673+expect(watchClient.request).not.toHaveBeenCalledWith(
674+"read",
675+expect.anything(),
676+expect.anything(),
677+);
678+expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
679+});
680+423681it.each([
424682{
425683label: "flat true",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。