diff --git a/extensions/imessage/src/probe.ts b/extensions/imessage/src/probe.ts index 55f1a51e3c9c..894297ae147e 100644 --- a/extensions/imessage/src/probe.ts +++ b/extensions/imessage/src/probe.ts @@ -243,6 +243,26 @@ export async function probeIMessagePrivateApi( try { const result = await runCommandWithTimeout([key, "status", "--json"], { timeoutMs }); const combined = `${result.stdout}\n${result.stderr}`.trim(); + const normalized = normalizeLowercaseStringOrEmpty(combined); + if ( + result.code !== 0 && + normalized.includes("unknown subcommand") && + normalized.includes("status") + ) { + const status: NonNullable = { + available: false, + v2Ready: false, + selectors: {}, + rpcMethods: [], + cliCapabilities: { + sendRichSupportsAttachment: false, + pollSendSupportsNoComment: false, + }, + error: `imsg CLI does not support the "status" subcommand. Update imsg on the Messages Mac: ${IMESSAGE_UPDATE_COMMAND}`, + }; + cacheIMessagePrivateApiStatus(key, status); + return status; + } const { payload, firstLineSnippet } = parseStatusPayload(result.stdout); const selectors = payload ? selectorsFromPayload(payload) : {}; const rpcMethods = payload ? rpcMethodsFromPayload(payload) : []; diff --git a/extensions/imessage/src/status.test.ts b/extensions/imessage/src/status.test.ts index 3178649a0ef9..0bbc3f52f2e6 100644 --- a/extensions/imessage/src/status.test.ts +++ b/extensions/imessage/src/status.test.ts @@ -545,6 +545,77 @@ describe("probeIMessage", () => { expect(createIMessageRpcClientMock).not.toHaveBeenCalled(); }); + it("explains how to update imsg when its private status subcommand is unsupported", async () => { + const runCommand = vi.spyOn(processRuntime, "runCommandWithTimeout").mockResolvedValueOnce({ + stdout: "", + stderr: "Unknown subcommand 'status' for command 'imsg'", + code: 1, + signal: null, + killed: false, + termination: "exit", + }); + + await expect( + probeIMessagePrivateApi("imsg-legacy-private-status", 1000), + ).resolves.toMatchObject({ + available: false, + v2Ready: false, + selectors: {}, + rpcMethods: [], + cliCapabilities: { + sendRichSupportsAttachment: false, + pollSendSupportsNoComment: false, + }, + error: + 'imsg CLI does not support the "status" subcommand. Update imsg on the Messages Mac: brew update && brew upgrade imsg', + }); + expect(runCommand).toHaveBeenCalledExactlyOnceWith( + ["imsg-legacy-private-status", "status", "--json"], + { timeoutMs: 1000 }, + ); + }); + + it("keeps foundational RPC healthy when an older imsg lacks private status", async () => { + const runCommand = vi + .spyOn(processRuntime, "runCommandWithTimeout") + .mockResolvedValueOnce({ + stdout: "rpc help", + stderr: "", + code: 0, + signal: null, + killed: false, + termination: "exit", + }) + .mockResolvedValueOnce({ + stdout: "", + stderr: "Unknown subcommand 'status' for command 'imsg'", + code: 1, + signal: null, + killed: false, + termination: "exit", + }); + const request = vi.fn().mockResolvedValue({ chats: [] }); + const stop = vi.fn().mockResolvedValue(undefined); + vi.spyOn(clientModule, "createIMessageRpcClient").mockResolvedValue({ + request, + stop, + } as unknown as Awaited>); + + await expect( + probeIMessage(1000, { cliPath: "imsg-legacy-foundational-rpc", platform: "darwin" }), + ).resolves.toMatchObject({ + ok: true, + privateApi: { + available: false, + error: + 'imsg CLI does not support the "status" subcommand. Update imsg on the Messages Mac: brew update && brew upgrade imsg', + }, + }); + expect(runCommand).toHaveBeenCalledTimes(2); + expect(request).toHaveBeenCalledWith("chats.list", { limit: 1 }, { timeoutMs: 1000 }); + expect(stop).toHaveBeenCalledOnce(); + }); + it("explains how to install imsg when the default binary is missing", async () => { vi.spyOn(setupRuntime, "detectBinary").mockResolvedValue(false); const createIMessageRpcClientMock = vi