mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(imessage): consolidate status test fixtures (#118249)
This commit is contained in:
committed by
GitHub
parent
cef675233b
commit
2a635f21df
@@ -32,6 +32,110 @@ const setupToolsMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
const installIMessageCliMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
type CommandResult = Awaited<ReturnType<typeof processRuntime.runCommandWithTimeout>>;
|
||||
type RpcClient = Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>;
|
||||
type RpcPendingRequest = {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
};
|
||||
type RpcClientInternals = {
|
||||
handleStdoutChunk: (chunk: Buffer | string) => void;
|
||||
pending: Map<string, RpcPendingRequest>;
|
||||
};
|
||||
type TestPrompterOverrides = NonNullable<Parameters<typeof createTestWizardPrompter>[0]>;
|
||||
|
||||
function commandResult(overrides: Partial<CommandResult> = {}): CommandResult {
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function privateStatusResult(overrides: Record<string, unknown> = {}): Partial<CommandResult> {
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
advanced_features: true,
|
||||
v2_ready: true,
|
||||
selectors: {},
|
||||
rpc_methods: ["chats.list"],
|
||||
...overrides,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function mockCommandResult(overrides: Partial<CommandResult>) {
|
||||
return vi
|
||||
.spyOn(processRuntime, "runCommandWithTimeout")
|
||||
.mockResolvedValue(commandResult(overrides));
|
||||
}
|
||||
|
||||
function mockCommandSequence(...results: Array<Partial<CommandResult>>) {
|
||||
const runCommand = vi.spyOn(processRuntime, "runCommandWithTimeout");
|
||||
for (const result of results) {
|
||||
runCommand.mockResolvedValueOnce(commandResult(result));
|
||||
}
|
||||
return runCommand;
|
||||
}
|
||||
|
||||
function mockRpcClient(requestResult: unknown = { chats: [] }) {
|
||||
const request = vi.fn().mockResolvedValue(requestResult);
|
||||
const stop = vi.fn().mockResolvedValue(undefined);
|
||||
const create = vi.spyOn(clientModule, "createIMessageRpcClient").mockResolvedValue({
|
||||
request,
|
||||
stop,
|
||||
} as unknown as RpcClient);
|
||||
return { create, request, stop };
|
||||
}
|
||||
|
||||
function rpcClientInternals(client: RpcClient): RpcClientInternals {
|
||||
return client as unknown as RpcClientInternals;
|
||||
}
|
||||
|
||||
function mockSuccessfulInstall(detected: boolean, version: string): void {
|
||||
setupToolsMocks.detectBinary.mockResolvedValueOnce(detected);
|
||||
installIMessageCliMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
version,
|
||||
});
|
||||
}
|
||||
|
||||
async function getSetupStatus(imessage: Record<string, unknown>, accountId?: string) {
|
||||
return await getIMessageSetupStatus({
|
||||
cfg: { channels: { imessage } } as never,
|
||||
accountOverrides: accountId ? { imessage: accountId } : {},
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareIMessage(params: {
|
||||
platform: NodeJS.Platform;
|
||||
cliPath?: string;
|
||||
prepare?: NonNullable<typeof imessageSetupWizard.prepare>;
|
||||
confirm: NonNullable<TestPrompterOverrides["confirm"]>;
|
||||
note?: TestPrompterOverrides["note"];
|
||||
}) {
|
||||
return await withPlatform(params.platform, () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: params.prepare ?? imessageSetupWizard.prepare,
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: params.cliPath ? { cliPath: params.cliPath } : {},
|
||||
},
|
||||
} as never,
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({
|
||||
confirm: params.confirm,
|
||||
...(params.note ? { note: params.note } : {}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/setup-tools", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("openclaw/plugin-sdk/setup-tools")>()),
|
||||
...setupToolsMocks,
|
||||
@@ -128,16 +232,7 @@ describe("createIMessageRpcClient", () => {
|
||||
async (_, separator) => {
|
||||
const { IMessageRpcClient } = await import("./client.js");
|
||||
const client = new IMessageRpcClient();
|
||||
const internals = client as unknown as {
|
||||
handleStdoutChunk: (chunk: Buffer | string) => void;
|
||||
pending: Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
>;
|
||||
};
|
||||
const internals = rpcClientInternals(client);
|
||||
const result = new Promise((resolve, reject) => {
|
||||
internals.pending.set("1", { resolve, reject });
|
||||
});
|
||||
@@ -162,16 +257,7 @@ describe("createIMessageRpcClient", () => {
|
||||
it("handles multiple LF-delimited stdout responses in one chunk", async () => {
|
||||
const { IMessageRpcClient } = await import("./client.js");
|
||||
const client = new IMessageRpcClient();
|
||||
const internals = client as unknown as {
|
||||
handleStdoutChunk: (chunk: Buffer | string) => void;
|
||||
pending: Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
>;
|
||||
};
|
||||
const internals = rpcClientInternals(client);
|
||||
const first = new Promise((resolve, reject) => {
|
||||
internals.pending.set("1", { resolve, reject });
|
||||
});
|
||||
@@ -218,65 +304,43 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("does not inherit configured state from a sibling account", async () => {
|
||||
const result = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
accounts: {
|
||||
default: {
|
||||
cliPath: "/usr/local/bin/imsg",
|
||||
},
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
const result = await getSetupStatus(
|
||||
{
|
||||
accounts: {
|
||||
default: { cliPath: "/usr/local/bin/imsg" },
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
accountOverrides: {
|
||||
imessage: "work",
|
||||
},
|
||||
});
|
||||
"work",
|
||||
);
|
||||
|
||||
expect(result.configured).toBe(false);
|
||||
expect(result.statusLines).toContain("iMessage: needs setup");
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount for omitted setup status cliPath", async () => {
|
||||
const status = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "/tmp/root-imsg",
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
cliPath: "/tmp/work-imsg",
|
||||
},
|
||||
},
|
||||
},
|
||||
const status = await getSetupStatus({
|
||||
cliPath: "/tmp/root-imsg",
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
cliPath: "/tmp/work-imsg",
|
||||
},
|
||||
} as never,
|
||||
accountOverrides: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status.statusLines).toContain("imsg: missing (/tmp/work-imsg)");
|
||||
});
|
||||
|
||||
it("does not inherit configured state from a sibling when defaultAccount is named", async () => {
|
||||
const status = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
default: {
|
||||
cliPath: "/usr/local/bin/imsg",
|
||||
},
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
const status = await getSetupStatus({
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
default: {
|
||||
cliPath: "/usr/local/bin/imsg",
|
||||
},
|
||||
} as never,
|
||||
accountOverrides: {},
|
||||
work: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status.configured).toBe(false);
|
||||
@@ -284,34 +348,23 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("setup status lines use the selected account cliPath", async () => {
|
||||
const status = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "/tmp/root-imsg",
|
||||
accounts: {
|
||||
work: {
|
||||
cliPath: "/tmp/work-imsg",
|
||||
},
|
||||
},
|
||||
const status = await getSetupStatus(
|
||||
{
|
||||
cliPath: "/tmp/root-imsg",
|
||||
accounts: {
|
||||
work: {
|
||||
cliPath: "/tmp/work-imsg",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
accountOverrides: { imessage: "work" },
|
||||
});
|
||||
},
|
||||
"work",
|
||||
);
|
||||
|
||||
expect(status.statusLines).toContain("imsg: missing (/tmp/work-imsg)");
|
||||
});
|
||||
|
||||
it("setup status explains how to install imsg when the binary is missing", async () => {
|
||||
const status = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {},
|
||||
},
|
||||
} as never,
|
||||
accountOverrides: {},
|
||||
});
|
||||
const status = await getSetupStatus({});
|
||||
|
||||
expect(status.statusLines).toContain(
|
||||
"Install imsg on the Messages Mac: brew install steipete/tap/imsg",
|
||||
@@ -319,23 +372,11 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("prepare offers to install imsg and returns the installed cliPath", async () => {
|
||||
setupToolsMocks.detectBinary.mockResolvedValueOnce(false);
|
||||
installIMessageCliMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
version: "0.13.0",
|
||||
});
|
||||
mockSuccessfulInstall(false, "0.13.0");
|
||||
const confirm = vi.fn(async () => true);
|
||||
const note = vi.fn(async () => {});
|
||||
|
||||
const result = await withPlatform("darwin", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: imessageSetupWizard.prepare,
|
||||
cfg: { channels: { imessage: {} } },
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm, note }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({ platform: "darwin", confirm, note });
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith({
|
||||
message: "imsg not found. Install now?",
|
||||
@@ -351,16 +392,7 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("setup status preserves an explicit PATH-based imsg wrapper", async () => {
|
||||
const status = await getIMessageSetupStatus({
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "imsg",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
accountOverrides: {},
|
||||
});
|
||||
const status = await getSetupStatus({ cliPath: "imsg" });
|
||||
|
||||
expect(status.statusLines).toContain(
|
||||
"imsg command not found (imsg). Check the configured cliPath or wrapper.",
|
||||
@@ -368,29 +400,16 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("prepare offers to update Homebrew-managed imsg paths", async () => {
|
||||
setupToolsMocks.detectBinary.mockResolvedValueOnce(true);
|
||||
installIMessageCliMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
version: "0.13.1",
|
||||
});
|
||||
mockSuccessfulInstall(true, "0.13.1");
|
||||
const confirm = vi.fn(async () => true);
|
||||
const note = vi.fn(async () => {});
|
||||
|
||||
const result = await withPlatform("darwin", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: imessageSetupWizard.prepare,
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm, note }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({
|
||||
platform: "darwin",
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
confirm,
|
||||
note,
|
||||
});
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith({
|
||||
message: "imsg detected. Reinstall/update now?",
|
||||
@@ -405,23 +424,15 @@ describe("imessage setup status", () => {
|
||||
});
|
||||
|
||||
it("setup wizard proxy delegates imsg install preparation", async () => {
|
||||
setupToolsMocks.detectBinary.mockResolvedValueOnce(false);
|
||||
installIMessageCliMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
version: "0.13.0",
|
||||
});
|
||||
mockSuccessfulInstall(false, "0.13.0");
|
||||
const proxy = createIMessageSetupWizardProxy(async () => imessageSetupWizard);
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
const result = await withPlatform("darwin", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: proxy.prepare,
|
||||
cfg: { channels: { imessage: {} } },
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({
|
||||
platform: "darwin",
|
||||
prepare: proxy.prepare,
|
||||
confirm,
|
||||
});
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith({
|
||||
message: "imsg not found. Install now?",
|
||||
@@ -437,20 +448,11 @@ describe("imessage setup status", () => {
|
||||
it("prepare preserves custom imsg cliPath values", async () => {
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
const result = await withPlatform("darwin", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: imessageSetupWizard.prepare,
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "ssh imessage-host imsg",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({
|
||||
platform: "darwin",
|
||||
cliPath: "ssh imessage-host imsg",
|
||||
confirm,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(setupToolsMocks.detectBinary).not.toHaveBeenCalled();
|
||||
@@ -461,20 +463,7 @@ describe("imessage setup status", () => {
|
||||
it("prepare preserves explicit PATH-based imsg wrappers", async () => {
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
const result = await withPlatform("darwin", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: imessageSetupWizard.prepare,
|
||||
cfg: {
|
||||
channels: {
|
||||
imessage: {
|
||||
cliPath: "imsg",
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({ platform: "darwin", cliPath: "imsg", confirm });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(setupToolsMocks.detectBinary).not.toHaveBeenCalled();
|
||||
@@ -485,14 +474,7 @@ describe("imessage setup status", () => {
|
||||
it("prepare skips automatic imsg install on non-macOS hosts", async () => {
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
const result = await withPlatform("linux", () =>
|
||||
runSetupWizardPrepare({
|
||||
prepare: imessageSetupWizard.prepare,
|
||||
cfg: { channels: { imessage: {} } },
|
||||
options: { allowIMessageInstall: true },
|
||||
prompter: createTestWizardPrompter({ confirm }),
|
||||
}),
|
||||
);
|
||||
const result = await prepareIMessage({ platform: "linux", confirm });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(setupToolsMocks.detectBinary).not.toHaveBeenCalled();
|
||||
@@ -520,39 +502,26 @@ describe("probeIMessage", () => {
|
||||
vi.restoreAllMocks();
|
||||
spawnMock.mockClear();
|
||||
vi.spyOn(setupRuntime, "detectBinary").mockResolvedValue(true);
|
||||
vi.spyOn(processRuntime, "runCommandWithTimeout").mockResolvedValue({
|
||||
stdout: "",
|
||||
mockCommandResult({
|
||||
stderr: 'unknown command "rpc" for "imsg"',
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks unknown rpc subcommand as fatal", async () => {
|
||||
const createIMessageRpcClientMock = vi
|
||||
.spyOn(clientModule, "createIMessageRpcClient")
|
||||
.mockResolvedValue({
|
||||
request: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
const { create } = mockRpcClient();
|
||||
const result = await probeIMessage(1000, { cliPath: "imsg-test-rpc" });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.fatal).toBe(true);
|
||||
expect(result.error).toMatch(/rpc/i);
|
||||
expect(result.error).toContain("brew update && brew upgrade imsg");
|
||||
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("explains how to update imsg when its private status subcommand is unsupported", async () => {
|
||||
const runCommand = vi.spyOn(processRuntime, "runCommandWithTimeout").mockResolvedValueOnce({
|
||||
stdout: "",
|
||||
const runCommand = mockCommandSequence({
|
||||
stderr: "Unknown subcommand 'status' for command 'imsg'",
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -576,30 +545,14 @@ describe("probeIMessage", () => {
|
||||
});
|
||||
|
||||
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: "",
|
||||
const runCommand = mockCommandSequence(
|
||||
{ stdout: "rpc help" },
|
||||
{
|
||||
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<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
},
|
||||
);
|
||||
const { request, stop } = mockRpcClient();
|
||||
|
||||
await expect(
|
||||
probeIMessage(1000, { cliPath: "imsg-legacy-foundational-rpc", platform: "darwin" }),
|
||||
@@ -618,12 +571,7 @@ describe("probeIMessage", () => {
|
||||
|
||||
it("explains how to install imsg when the default binary is missing", async () => {
|
||||
vi.spyOn(setupRuntime, "detectBinary").mockResolvedValue(false);
|
||||
const createIMessageRpcClientMock = vi
|
||||
.spyOn(clientModule, "createIMessageRpcClient")
|
||||
.mockResolvedValue({
|
||||
request: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
const { create } = mockRpcClient();
|
||||
|
||||
const result = await probeIMessage(1000, { platform: "darwin" });
|
||||
|
||||
@@ -632,17 +580,12 @@ describe("probeIMessage", () => {
|
||||
"imsg not found (imsg). Install imsg on the Messages Mac: brew install steipete/tap/imsg",
|
||||
);
|
||||
expect(processRuntime.runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("explains how to fix an explicit PATH-based imsg wrapper", async () => {
|
||||
vi.spyOn(setupRuntime, "detectBinary").mockResolvedValue(false);
|
||||
const createIMessageRpcClientMock = vi
|
||||
.spyOn(clientModule, "createIMessageRpcClient")
|
||||
.mockResolvedValue({
|
||||
request: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
const { create } = mockRpcClient();
|
||||
|
||||
const result = await probeIMessage(1000, { cliPath: "imsg", platform: "darwin" });
|
||||
|
||||
@@ -651,17 +594,12 @@ describe("probeIMessage", () => {
|
||||
"imsg command not found (imsg). Check the configured iMessage cliPath or wrapper.",
|
||||
);
|
||||
expect(processRuntime.runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("explains how to fix a missing custom imsg wrapper", async () => {
|
||||
vi.spyOn(setupRuntime, "detectBinary").mockResolvedValue(false);
|
||||
const createIMessageRpcClientMock = vi
|
||||
.spyOn(clientModule, "createIMessageRpcClient")
|
||||
.mockResolvedValue({
|
||||
request: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
const { create } = mockRpcClient();
|
||||
|
||||
const result = await probeIMessage(1000, { cliPath: "/usr/local/bin/imsg-wrapper" });
|
||||
|
||||
@@ -670,7 +608,7 @@ describe("probeIMessage", () => {
|
||||
"imsg command not found (/usr/local/bin/imsg-wrapper). Check the configured iMessage cliPath or wrapper.",
|
||||
);
|
||||
expect(processRuntime.runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops cached rpc support when the current clock is not a valid date timestamp", async () => {
|
||||
@@ -678,49 +616,16 @@ describe("probeIMessage", () => {
|
||||
.mockReturnValueOnce(1_700_000_000_000)
|
||||
.mockReturnValueOnce(Number.NaN)
|
||||
.mockReturnValue(1_700_000_000_000);
|
||||
const runCommand = vi
|
||||
.spyOn(processRuntime, "runCommandWithTimeout")
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "",
|
||||
const runCommand = mockCommandSequence(
|
||||
{
|
||||
stderr: 'unknown command "rpc" for "imsg"',
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "rpc help",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
advanced_features: true,
|
||||
v2_ready: true,
|
||||
selectors: {},
|
||||
rpc_methods: ["chats.list"],
|
||||
}),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "send-rich --file",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
vi.spyOn(clientModule, "createIMessageRpcClient").mockResolvedValue({
|
||||
request: vi.fn().mockResolvedValue({ chats: [] }),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
},
|
||||
{ stdout: "rpc help" },
|
||||
privateStatusResult(),
|
||||
{ stdout: "send-rich --file" },
|
||||
);
|
||||
mockRpcClient();
|
||||
|
||||
await expect(probeIMessage(1000, { cliPath: "imsg-invalid-rpc-clock" })).resolves.toMatchObject(
|
||||
{
|
||||
@@ -744,13 +649,9 @@ describe("probeIMessage", () => {
|
||||
|
||||
it("does not cache rpc support when the expiry timestamp would exceed the valid date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000);
|
||||
const runCommand = vi.spyOn(processRuntime, "runCommandWithTimeout").mockResolvedValue({
|
||||
stdout: "",
|
||||
const runCommand = mockCommandResult({
|
||||
stderr: 'unknown command "rpc" for "imsg"',
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -771,14 +672,7 @@ describe("probeIMessage", () => {
|
||||
|
||||
it("does not cache unavailable private API status when the process clock is invalid", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
|
||||
const runCommand = vi.spyOn(processRuntime, "runCommandWithTimeout").mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "bridge unavailable",
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
const runCommand = mockCommandResult({ stderr: "bridge unavailable", code: 1 });
|
||||
|
||||
await expect(
|
||||
probeIMessagePrivateApi("imsg-invalid-private-status-clock", 1000),
|
||||
@@ -799,29 +693,10 @@ describe("probeIMessage", () => {
|
||||
it("propagates imsg's status message when advanced features are unavailable", async () => {
|
||||
const note =
|
||||
"System Integrity Protection (SIP) is enabled.\nAdvanced IMCore features are intentionally disabled.";
|
||||
vi.spyOn(processRuntime, "runCommandWithTimeout")
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
advanced_features: false,
|
||||
v2_ready: false,
|
||||
selectors: {},
|
||||
rpc_methods: ["chats.list"],
|
||||
message: note,
|
||||
}),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "send-rich --help",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
mockCommandSequence(
|
||||
privateStatusResult({ advanced_features: false, v2_ready: false, message: note }),
|
||||
{ stdout: "send-rich --help" },
|
||||
);
|
||||
|
||||
await expect(probeIMessagePrivateApi("imsg-status-message-test", 1000)).resolves.toMatchObject({
|
||||
available: false,
|
||||
@@ -830,37 +705,11 @@ describe("probeIMessage", () => {
|
||||
});
|
||||
|
||||
it("detects poll caption suppression from the exact poll send help contract", async () => {
|
||||
const runCommand = vi
|
||||
.spyOn(processRuntime, "runCommandWithTimeout")
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
advanced_features: true,
|
||||
v2_ready: true,
|
||||
selectors: { pollPayloadMessage: true },
|
||||
rpc_methods: ["chats.list"],
|
||||
}),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "send-rich --file",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "poll send --question <text> --option <text> --no-comment",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
const runCommand = mockCommandSequence(
|
||||
privateStatusResult({ selectors: { pollPayloadMessage: true } }),
|
||||
{ stdout: "send-rich --file" },
|
||||
{ stdout: "poll send --question <text> --option <text> --no-comment" },
|
||||
);
|
||||
|
||||
await expect(
|
||||
probeIMessagePrivateApi("imsg-poll-no-comment-supported", 1000),
|
||||
@@ -875,36 +724,11 @@ describe("probeIMessage", () => {
|
||||
});
|
||||
|
||||
it("does not infer poll caption suppression from selectors when the flag is absent", async () => {
|
||||
vi.spyOn(processRuntime, "runCommandWithTimeout")
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
advanced_features: true,
|
||||
v2_ready: true,
|
||||
selectors: { pollPayloadMessage: true },
|
||||
rpc_methods: ["chats.list"],
|
||||
}),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "send-rich --file",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "poll send --question <text> --option <text>",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
mockCommandSequence(
|
||||
privateStatusResult({ selectors: { pollPayloadMessage: true } }),
|
||||
{ stdout: "send-rich --file" },
|
||||
{ stdout: "poll send --question <text> --option <text>" },
|
||||
);
|
||||
|
||||
await expect(
|
||||
probeIMessagePrivateApi("imsg-poll-no-comment-absent", 1000),
|
||||
@@ -916,12 +740,7 @@ describe("probeIMessage", () => {
|
||||
});
|
||||
|
||||
it("fails fast for default local imsg probes on non-mac hosts", async () => {
|
||||
const createIMessageRpcClientMock = vi
|
||||
.spyOn(clientModule, "createIMessageRpcClient")
|
||||
.mockResolvedValue({
|
||||
request: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
|
||||
const { create } = mockRpcClient();
|
||||
|
||||
const result = await probeIMessage(1000, { cliPath: "imsg", platform: "linux" });
|
||||
|
||||
@@ -930,7 +749,7 @@ describe("probeIMessage", () => {
|
||||
expect(result.error).toMatch(/macOS/i);
|
||||
expect(result.error).toMatch(/SSH wrapper/i);
|
||||
expect(setupRuntime.detectBinary).not.toHaveBeenCalled();
|
||||
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("status probe uses account-scoped cliPath and dbPath", async () => {
|
||||
|
||||
Reference in New Issue
Block a user