Skip to content

Commit 2364e45

Browse files
authored
test: align extension runtime mocks with plugin-sdk (openclaw#51289)
* test: align extension runtime mocks with plugin-sdk Update stale extension tests to mock the plugin-sdk runtime barrels that production code now imports, and harden the Signal tool-result harness around system-event assertions so the channels lane matches current extension boundaries. Regeneration-Prompt: | Verify the failing channels-lane tests against current origin/main in an isolated worktree before changing anything. If the failures reproduce on main, keep the fix test-only unless production behavior is clearly wrong. Recent extension refactors moved Telegram, WhatsApp, and Signal code onto plugin-sdk runtime barrels, so update stale tests that still mock old core module paths to intercept the seams production code now uses. For Signal reaction notifications, avoid brittle assertions that depend on shared queued system-event state when a direct harness spy on enqueue behavior is sufficient. Preserve scope: only touch the failing tests and their local harness, then rerun the reproduced targeted tests plus the full channels lane and repo check gate. * test: fix extension test drift on main * fix: lazy-load bundled web search plugin registry * test: make matrix sweeper failure injection portable * fix: split heavy matrix runtime-api seams * fix: simplify bundled web search id lookup * test: tolerate windows env key casing
1 parent e635ced commit 2364e45

25 files changed

+287
-96
lines changed

extensions/bluebubbles/src/send.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
32
import "./test-mocks.js";
3+
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
44
import type { PluginRuntime } from "./runtime-api.js";
55
import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js";
66
import { sendMessageBlueBubbles, resolveChatGuidForTarget, createChatForHandle } from "./send.js";

extensions/bluebubbles/src/test-harness.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ export function createBlueBubblesProbeMockModule(): BlueBubblesProbeMockModule {
6262
export function installBlueBubblesFetchTestHooks(params: {
6363
mockFetch: ReturnType<typeof vi.fn>;
6464
privateApiStatusMock: {
65-
mockReset: () => unknown;
65+
mockReset?: () => unknown;
66+
mockClear?: () => unknown;
6667
mockReturnValue: (value: boolean | null) => unknown;
6768
};
6869
}) {
6970
beforeEach(() => {
7071
vi.stubGlobal("fetch", params.mockFetch);
7172
params.mockFetch.mockReset();
72-
params.privateApiStatusMock.mockReset();
73+
params.privateApiStatusMock.mockReset?.();
74+
params.privateApiStatusMock.mockClear?.();
7375
params.privateApiStatusMock.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown);
7476
});
7577

extensions/matrix/runtime-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export {
1111
ssrfPolicyFromAllowPrivateNetwork,
1212
type LookupFn,
1313
type SsrFPolicy,
14-
} from "openclaw/plugin-sdk/infra-runtime";
14+
} from "openclaw/plugin-sdk/ssrf-runtime";
1515
export {
1616
setMatrixThreadBindingIdleTimeoutBySessionKey,
1717
setMatrixThreadBindingMaxAgeBySessionKey,

extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,19 @@ function createHandlerHarness() {
5353
dispatcher: {},
5454
replyOptions: {},
5555
markDispatchIdle: vi.fn(),
56+
markRunComplete: vi.fn(),
5657
}),
5758
resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined),
5859
dispatchReplyFromConfig: vi
5960
.fn()
6061
.mockResolvedValue({ queuedFinal: false, counts: { final: 0, block: 0, tool: 0 } }),
62+
withReplyDispatcher: vi.fn().mockImplementation(async ({ run, onSettled }) => {
63+
try {
64+
return await run();
65+
} finally {
66+
await onSettled?.();
67+
}
68+
}),
6169
},
6270
commands: {
6371
shouldHandleTextCommands: vi.fn().mockReturnValue(true),

extensions/matrix/src/matrix/thread-bindings-shared.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {
22
BindingTargetKind,
33
SessionBindingRecord,
4-
} from "openclaw/plugin-sdk/conversation-runtime";
5-
import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/conversation-runtime";
4+
} from "openclaw/plugin-sdk/thread-bindings-runtime";
5+
import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/thread-bindings-runtime";
66

77
export type MatrixThreadBindingTargetKind = "subagent" | "acp";
88

extensions/matrix/src/matrix/thread-bindings.test.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,14 @@ import {
1616
setMatrixThreadBindingMaxAgeBySessionKey,
1717
} from "./thread-bindings.js";
1818

19-
const pluginSdkActual = vi.hoisted(() => ({
20-
writeJsonFileAtomically: null as null | ((filePath: string, value: unknown) => Promise<void>),
21-
}));
22-
2319
const sendMessageMatrixMock = vi.hoisted(() =>
2420
vi.fn(async (_to: string, _message: string, opts?: { threadId?: string }) => ({
2521
messageId: opts?.threadId ? "$reply" : "$root",
2622
roomId: "!room:example",
2723
})),
2824
);
29-
const writeJsonFileAtomicallyMock = vi.hoisted(() =>
30-
vi.fn<(filePath: string, value: unknown) => Promise<void>>(),
31-
);
32-
33-
vi.mock("../../runtime-api.js", async () => {
34-
const actual =
35-
await vi.importActual<typeof import("../../runtime-api.js")>("../../runtime-api.js");
36-
pluginSdkActual.writeJsonFileAtomically = actual.writeJsonFileAtomically;
37-
return {
38-
...actual,
39-
writeJsonFileAtomically: (filePath: string, value: unknown) =>
40-
writeJsonFileAtomicallyMock(filePath, value),
41-
};
42-
});
25+
const actualRename = fs.rename.bind(fs);
26+
const renameMock = vi.spyOn(fs, "rename");
4327

4428
vi.mock("./send.js", async () => {
4529
const actual = await vi.importActual<typeof import("./send.js")>("./send.js");
@@ -82,10 +66,8 @@ describe("matrix thread bindings", () => {
8266
__testing.resetSessionBindingAdaptersForTests();
8367
resetMatrixThreadBindingsForTests();
8468
sendMessageMatrixMock.mockClear();
85-
writeJsonFileAtomicallyMock.mockReset();
86-
writeJsonFileAtomicallyMock.mockImplementation(async (filePath: string, value: unknown) => {
87-
await pluginSdkActual.writeJsonFileAtomically?.(filePath, value);
88-
});
69+
renameMock.mockReset();
70+
renameMock.mockImplementation(actualRename);
8971
setMatrixRuntime({
9072
state: {
9173
resolveStateDir: () => stateDir,
@@ -216,7 +198,7 @@ describe("matrix thread bindings", () => {
216198
}
217199
});
218200

219-
it("persists a batch of expired bindings once per sweep", async () => {
201+
it("persists expired bindings after a sweep", async () => {
220202
vi.useFakeTimers();
221203
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
222204
try {
@@ -251,12 +233,8 @@ describe("matrix thread bindings", () => {
251233
placement: "current",
252234
});
253235

254-
writeJsonFileAtomicallyMock.mockClear();
255236
await vi.advanceTimersByTimeAsync(61_000);
256-
257-
await vi.waitFor(() => {
258-
expect(writeJsonFileAtomicallyMock).toHaveBeenCalledTimes(1);
259-
});
237+
await Promise.resolve();
260238

261239
await vi.waitFor(async () => {
262240
const persistedRaw = await fs.readFile(resolveBindingsFilePath(), "utf-8");
@@ -296,13 +274,23 @@ describe("matrix thread bindings", () => {
296274
placement: "current",
297275
});
298276

299-
writeJsonFileAtomicallyMock.mockClear();
300-
writeJsonFileAtomicallyMock.mockRejectedValueOnce(new Error("disk full"));
277+
renameMock.mockRejectedValueOnce(new Error("disk full"));
301278
await vi.advanceTimersByTimeAsync(61_000);
279+
await Promise.resolve();
280+
281+
await vi.waitFor(() => {
282+
expect(
283+
logVerboseMessage.mock.calls.some(
284+
([message]) =>
285+
typeof message === "string" &&
286+
message.includes("failed auto-unbinding expired bindings"),
287+
),
288+
).toBe(true);
289+
});
302290

303291
await vi.waitFor(() => {
304292
expect(logVerboseMessage).toHaveBeenCalledWith(
305-
expect.stringContaining("failed auto-unbinding expired bindings"),
293+
expect.stringContaining("matrix: auto-unbinding $thread due to idle-expired"),
306294
);
307295
});
308296

extensions/matrix/src/runtime-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export {
88
type LookupFn,
99
type SsrFPolicy,
1010
} from "openclaw/plugin-sdk/infra-runtime";
11+
export {
12+
dispatchReplyFromConfigWithSettledDispatcher,
13+
ensureConfiguredAcpBindingReady,
14+
maybeCreateMatrixMigrationSnapshot,
15+
resolveConfiguredAcpBindingRecord,
16+
} from "openclaw/plugin-sdk/matrix-runtime-heavy";
1117
// Keep auth-precedence available internally without re-exporting helper-api
1218
// twice through both plugin-sdk/matrix and ../runtime-api.js.
1319
export * from "./auth-precedence.js";

extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
2+
import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime";
13
import { describe, expect, it, vi } from "vitest";
24
import type { OpenClawConfig } from "../../../src/config/config.js";
3-
import { resolveAgentRoute } from "../../../src/routing/resolve-route.js";
4-
import { normalizeE164 } from "../../../src/utils.js";
55
import type { SignalDaemonExitEvent } from "./daemon.js";
66
import {
77
createMockSignalDaemonHandle,
@@ -16,16 +16,14 @@ installSignalToolResultTestHooks();
1616

1717
// Import after the harness registers `vi.mock(...)` for Signal internals.
1818
vi.resetModules();
19-
const [{ peekSystemEvents }, { monitorSignalProvider }] = await Promise.all([
20-
import("openclaw/plugin-sdk/infra-runtime"),
21-
import("./monitor.js"),
22-
]);
19+
const { monitorSignalProvider } = await import("./monitor.js");
2320

2421
const {
2522
replyMock,
2623
sendMock,
2724
streamMock,
2825
updateLastRouteMock,
26+
enqueueSystemEventMock,
2927
upsertPairingRequestMock,
3028
waitForTransportReadyMock,
3129
spawnSignalDaemonMock,
@@ -109,14 +107,23 @@ async function receiveSignalPayloads(params: {
109107
await flush();
110108
}
111109

112-
function getDirectSignalEventsFor(sender: string) {
110+
function hasQueuedReactionEventFor(sender: string) {
113111
const route = resolveAgentRoute({
114112
cfg: config as OpenClawConfig,
115113
channel: "signal",
116114
accountId: "default",
117115
peer: { kind: "direct", id: normalizeE164(sender) },
118116
});
119-
return peekSystemEvents(route.sessionKey);
117+
return enqueueSystemEventMock.mock.calls.some(([text, options]) => {
118+
return (
119+
typeof text === "string" &&
120+
text.includes("Signal reaction added") &&
121+
typeof options === "object" &&
122+
options !== null &&
123+
"sessionKey" in options &&
124+
(options as { sessionKey?: string }).sessionKey === route.sessionKey
125+
);
126+
});
120127
}
121128

122129
function makeBaseEnvelope(overrides: Record<string, unknown> = {}) {
@@ -383,8 +390,7 @@ describe("monitorSignalProvider tool results", () => {
383390
},
384391
});
385392

386-
const events = getDirectSignalEventsFor("+15550001111");
387-
expect(events.some((text) => text.includes("Signal reaction added"))).toBe(true);
393+
expect(hasQueuedReactionEventFor("+15550001111")).toBe(true);
388394
});
389395

390396
it.each([
@@ -424,8 +430,7 @@ describe("monitorSignalProvider tool results", () => {
424430
},
425431
});
426432

427-
const events = getDirectSignalEventsFor("+15550001111");
428-
expect(events.some((text) => text.includes("Signal reaction added"))).toBe(shouldEnqueue);
433+
expect(hasQueuedReactionEventFor("+15550001111")).toBe(shouldEnqueue);
429434
expect(sendMock).not.toHaveBeenCalled();
430435
expect(upsertPairingRequestMock).not.toHaveBeenCalled();
431436
});
@@ -442,8 +447,7 @@ describe("monitorSignalProvider tool results", () => {
442447
},
443448
});
444449

445-
const events = getDirectSignalEventsFor("+15550001111");
446-
expect(events.some((text) => text.includes("Signal reaction added"))).toBe(true);
450+
expect(hasQueuedReactionEventFor("+15550001111")).toBe(true);
447451
});
448452

449453
it("processes messages when reaction metadata is present", async () => {

extensions/signal/src/monitor.tool-result.test-harness.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SignalDaemonExitEvent, SignalDaemonHandle } from "./daemon.js";
44

55
type SignalToolResultTestMocks = {
66
waitForTransportReadyMock: MockFn;
7+
enqueueSystemEventMock: MockFn;
78
sendMock: MockFn;
89
replyMock: MockFn;
910
updateLastRouteMock: MockFn;
@@ -16,6 +17,7 @@ type SignalToolResultTestMocks = {
1617
};
1718

1819
const waitForTransportReadyMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
20+
const enqueueSystemEventMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
1921
const sendMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
2022
const replyMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
2123
const updateLastRouteMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
@@ -29,6 +31,7 @@ const spawnSignalDaemonMock = vi.hoisted(() => vi.fn()) as unknown as MockFn;
2931
export function getSignalToolResultTestMocks(): SignalToolResultTestMocks {
3032
return {
3133
waitForTransportReadyMock,
34+
enqueueSystemEventMock,
3235
sendMock,
3336
replyMock,
3437
updateLastRouteMock,
@@ -162,6 +165,10 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async () => {
162165
return {
163166
...actual,
164167
waitForTransportReady: (...args: unknown[]) => waitForTransportReadyMock(...args),
168+
enqueueSystemEvent: (...args: Parameters<typeof actual.enqueueSystemEvent>) => {
169+
enqueueSystemEventMock(...args);
170+
return actual.enqueueSystemEvent(...args);
171+
},
165172
};
166173
});
167174

@@ -189,6 +196,7 @@ export function installSignalToolResultTestHooks() {
189196
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
190197
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
191198
waitForTransportReadyMock.mockReset().mockResolvedValue(undefined);
199+
enqueueSystemEventMock.mockReset();
192200

193201
resetSystemEventsForTest();
194202
});

extensions/telegram/src/send.proxy.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ const { resolveTelegramFetch } = vi.hoisted(() => ({
2121
resolveTelegramFetch: vi.fn(),
2222
}));
2323

24-
vi.mock("../../../src/config/config.js", async (importOriginal) => {
25-
const actual = await importOriginal<typeof import("../../../src/config/config.js")>();
24+
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
25+
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
26+
"openclaw/plugin-sdk/config-runtime",
27+
);
2628
return {
2729
...actual,
2830
loadConfig,

0 commit comments

Comments
 (0)