Matrix: make onboarding status runtime-safe (#49995)

* Matrix: make onboarding status runtime-safe

* Matrix tests: mock reply dispatch in BodyForAgent coverage

* changelog

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>

---------

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
Josh Avant
2026-03-18 17:33:42 -05:00
committed by GitHub
parent 859889aae9
commit 2661de384f
6 changed files with 129 additions and 5 deletions

View File

@@ -152,6 +152,7 @@ Docs: https://docs.openclaw.ai
- Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob.
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
- WhatsApp: stabilize inbound monitor and setup tests (#50007) Thanks @joshavant.
- Matrix: make onboarding status runtime-safe (#49995) Thanks @joshavant.
### Breaking

View File

@@ -0,0 +1,73 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { clearMatrixRuntime, setMatrixRuntime } from "../runtime.js";
import { loadMatrixCredentials, resolveMatrixCredentialsDir } from "./credentials.js";
describe("matrix credentials paths", () => {
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
beforeEach(() => {
clearMatrixRuntime();
delete process.env.OPENCLAW_STATE_DIR;
});
afterEach(() => {
clearMatrixRuntime();
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
});
it("falls back to OPENCLAW_STATE_DIR when runtime is not initialized", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-creds-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
expect(resolveMatrixCredentialsDir(process.env)).toBe(
path.join(stateDir, "credentials", "matrix"),
);
});
it("prefers runtime state dir when runtime is initialized", () => {
const runtimeStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-runtime-"));
const envStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-env-"));
process.env.OPENCLAW_STATE_DIR = envStateDir;
setMatrixRuntime({
state: {
resolveStateDir: () => runtimeStateDir,
},
} as never);
expect(resolveMatrixCredentialsDir(process.env)).toBe(
path.join(runtimeStateDir, "credentials", "matrix"),
);
});
it("prefers explicit stateDir argument over runtime/env", () => {
const explicitStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-explicit-"));
const runtimeStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-runtime-"));
process.env.OPENCLAW_STATE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-env-"));
setMatrixRuntime({
state: {
resolveStateDir: () => runtimeStateDir,
},
} as never);
expect(resolveMatrixCredentialsDir(process.env, explicitStateDir)).toBe(
path.join(explicitStateDir, "credentials", "matrix"),
);
});
it("returns null without throwing when credentials are missing and runtime is absent", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-creds-missing-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
expect(() => loadMatrixCredentials(process.env)).not.toThrow();
expect(loadMatrixCredentials(process.env)).toBeNull();
});
});

View File

@@ -2,7 +2,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { getMatrixRuntime } from "../runtime.js";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { tryGetMatrixRuntime } from "../runtime.js";
export type MatrixStoredCredentials = {
homeserver: string;
@@ -27,7 +28,9 @@ export function resolveMatrixCredentialsDir(
env: NodeJS.ProcessEnv = process.env,
stateDir?: string,
): string {
const resolvedStateDir = stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
const runtime = tryGetMatrixRuntime();
const resolvedStateDir =
stateDir ?? runtime?.state.resolveStateDir(env, os.homedir) ?? resolveStateDir(env, os.homedir);
return path.join(resolvedStateDir, "credentials", "matrix");
}

View File

@@ -8,6 +8,22 @@ import {
} from "./handler.js";
import { EventType, type MatrixRawEvent } from "./types.js";
const dispatchReplyFromConfigWithSettledDispatcherMock = vi.hoisted(() =>
vi.fn().mockResolvedValue({
queuedFinal: false,
counts: { final: 0, partial: 0, tool: 0 },
}),
);
vi.mock("../../../runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../runtime-api.js")>();
return {
...actual,
dispatchReplyFromConfigWithSettledDispatcher: (...args: unknown[]) =>
dispatchReplyFromConfigWithSettledDispatcherMock(...args),
};
});
describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
it("stores sender-labeled BodyForAgent for group thread messages", async () => {
const recordInboundSession = vi.fn().mockResolvedValue(undefined);
@@ -149,6 +165,7 @@ describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
}),
}),
);
expect(dispatchReplyFromConfigWithSettledDispatcherMock).toHaveBeenCalled();
});
it("uses room-scoped session keys for DM rooms matched via parentPeer binding", () => {

View File

@@ -1,6 +1,10 @@
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "../runtime-api.js";
const { setRuntime: setMatrixRuntime, getRuntime: getMatrixRuntime } =
createPluginRuntimeStore<PluginRuntime>("Matrix runtime not initialized");
export { getMatrixRuntime, setMatrixRuntime };
const {
setRuntime: setMatrixRuntime,
clearRuntime: clearMatrixRuntime,
tryGetRuntime: tryGetMatrixRuntime,
getRuntime: getMatrixRuntime,
} = createPluginRuntimeStore<PluginRuntime>("Matrix runtime not initialized");
export { clearMatrixRuntime, getMatrixRuntime, setMatrixRuntime, tryGetMatrixRuntime };

View File

@@ -303,6 +303,32 @@ describe("setupChannels", () => {
expect(multiselect).not.toHaveBeenCalled();
});
it("renders the QuickStart channel picker without requiring the Matrix runtime", async () => {
const select = vi.fn(async ({ message }: { message: string }) => {
if (message === "Select channel (QuickStart)") {
return "__skip__";
}
return "__done__";
});
const { multiselect, text } = createUnexpectedPromptGuards();
const prompter = createPrompter({
select: select as unknown as WizardPrompter["select"],
multiselect,
text,
});
await expect(
runSetupChannels({} as OpenClawConfig, prompter, {
quickstartDefaults: true,
}),
).resolves.toEqual({} as OpenClawConfig);
expect(select).toHaveBeenCalledWith(
expect.objectContaining({ message: "Select channel (QuickStart)" }),
);
expect(multiselect).not.toHaveBeenCalled();
});
it("continues Telegram setup when the plugin registry is empty", async () => {
// Simulate missing registry entries (the scenario reported in #25545).
setActivePluginRegistry(createEmptyPluginRegistry());