test: align extension runtime mocks with plugin-sdk (#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
This commit is contained in:
Josh Lehman
2026-03-20 15:59:53 -07:00
committed by GitHub
parent e635cedb85
commit 2364e45fe4
25 changed files with 287 additions and 96 deletions

View File

@@ -53,11 +53,19 @@ function createHandlerHarness() {
dispatcher: {},
replyOptions: {},
markDispatchIdle: vi.fn(),
markRunComplete: vi.fn(),
}),
resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined),
dispatchReplyFromConfig: vi
.fn()
.mockResolvedValue({ queuedFinal: false, counts: { final: 0, block: 0, tool: 0 } }),
withReplyDispatcher: vi.fn().mockImplementation(async ({ run, onSettled }) => {
try {
return await run();
} finally {
await onSettled?.();
}
}),
},
commands: {
shouldHandleTextCommands: vi.fn().mockReturnValue(true),

View File

@@ -1,8 +1,8 @@
import type {
BindingTargetKind,
SessionBindingRecord,
} from "openclaw/plugin-sdk/conversation-runtime";
import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/conversation-runtime";
} from "openclaw/plugin-sdk/thread-bindings-runtime";
import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/thread-bindings-runtime";
export type MatrixThreadBindingTargetKind = "subagent" | "acp";

View File

@@ -16,30 +16,14 @@ import {
setMatrixThreadBindingMaxAgeBySessionKey,
} from "./thread-bindings.js";
const pluginSdkActual = vi.hoisted(() => ({
writeJsonFileAtomically: null as null | ((filePath: string, value: unknown) => Promise<void>),
}));
const sendMessageMatrixMock = vi.hoisted(() =>
vi.fn(async (_to: string, _message: string, opts?: { threadId?: string }) => ({
messageId: opts?.threadId ? "$reply" : "$root",
roomId: "!room:example",
})),
);
const writeJsonFileAtomicallyMock = vi.hoisted(() =>
vi.fn<(filePath: string, value: unknown) => Promise<void>>(),
);
vi.mock("../../runtime-api.js", async () => {
const actual =
await vi.importActual<typeof import("../../runtime-api.js")>("../../runtime-api.js");
pluginSdkActual.writeJsonFileAtomically = actual.writeJsonFileAtomically;
return {
...actual,
writeJsonFileAtomically: (filePath: string, value: unknown) =>
writeJsonFileAtomicallyMock(filePath, value),
};
});
const actualRename = fs.rename.bind(fs);
const renameMock = vi.spyOn(fs, "rename");
vi.mock("./send.js", async () => {
const actual = await vi.importActual<typeof import("./send.js")>("./send.js");
@@ -82,10 +66,8 @@ describe("matrix thread bindings", () => {
__testing.resetSessionBindingAdaptersForTests();
resetMatrixThreadBindingsForTests();
sendMessageMatrixMock.mockClear();
writeJsonFileAtomicallyMock.mockReset();
writeJsonFileAtomicallyMock.mockImplementation(async (filePath: string, value: unknown) => {
await pluginSdkActual.writeJsonFileAtomically?.(filePath, value);
});
renameMock.mockReset();
renameMock.mockImplementation(actualRename);
setMatrixRuntime({
state: {
resolveStateDir: () => stateDir,
@@ -216,7 +198,7 @@ describe("matrix thread bindings", () => {
}
});
it("persists a batch of expired bindings once per sweep", async () => {
it("persists expired bindings after a sweep", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
try {
@@ -251,12 +233,8 @@ describe("matrix thread bindings", () => {
placement: "current",
});
writeJsonFileAtomicallyMock.mockClear();
await vi.advanceTimersByTimeAsync(61_000);
await vi.waitFor(() => {
expect(writeJsonFileAtomicallyMock).toHaveBeenCalledTimes(1);
});
await Promise.resolve();
await vi.waitFor(async () => {
const persistedRaw = await fs.readFile(resolveBindingsFilePath(), "utf-8");
@@ -296,13 +274,23 @@ describe("matrix thread bindings", () => {
placement: "current",
});
writeJsonFileAtomicallyMock.mockClear();
writeJsonFileAtomicallyMock.mockRejectedValueOnce(new Error("disk full"));
renameMock.mockRejectedValueOnce(new Error("disk full"));
await vi.advanceTimersByTimeAsync(61_000);
await Promise.resolve();
await vi.waitFor(() => {
expect(
logVerboseMessage.mock.calls.some(
([message]) =>
typeof message === "string" &&
message.includes("failed auto-unbinding expired bindings"),
),
).toBe(true);
});
await vi.waitFor(() => {
expect(logVerboseMessage).toHaveBeenCalledWith(
expect.stringContaining("failed auto-unbinding expired bindings"),
expect.stringContaining("matrix: auto-unbinding $thread due to idle-expired"),
);
});

View File

@@ -8,6 +8,12 @@ export {
type LookupFn,
type SsrFPolicy,
} from "openclaw/plugin-sdk/infra-runtime";
export {
dispatchReplyFromConfigWithSettledDispatcher,
ensureConfiguredAcpBindingReady,
maybeCreateMatrixMigrationSnapshot,
resolveConfiguredAcpBindingRecord,
} from "openclaw/plugin-sdk/matrix-runtime-heavy";
// Keep auth-precedence available internally without re-exporting helper-api
// twice through both plugin-sdk/matrix and ../runtime-api.js.
export * from "./auth-precedence.js";