From 2661de384f17ba0cd513fb20c3beae06ef643162 Mon Sep 17 00:00:00 2001 From: Josh Avant <830519+joshavant@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:33:42 -0500 Subject: [PATCH] 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> --- CHANGELOG.md | 1 + .../matrix/src/matrix/credentials.test.ts | 73 +++++++++++++++++++ extensions/matrix/src/matrix/credentials.ts | 7 +- .../monitor/handler.body-for-agent.test.ts | 17 +++++ extensions/matrix/src/runtime.ts | 10 ++- src/commands/onboard-channels.e2e.test.ts | 26 +++++++ 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 extensions/matrix/src/matrix/credentials.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a23d025fd8a..6f3edc4dc6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extensions/matrix/src/matrix/credentials.test.ts b/extensions/matrix/src/matrix/credentials.test.ts new file mode 100644 index 00000000000..43a5096618e --- /dev/null +++ b/extensions/matrix/src/matrix/credentials.test.ts @@ -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(); + }); +}); diff --git a/extensions/matrix/src/matrix/credentials.ts b/extensions/matrix/src/matrix/credentials.ts index 7da620324d7..8cd03e51e81 100644 --- a/extensions/matrix/src/matrix/credentials.ts +++ b/extensions/matrix/src/matrix/credentials.ts @@ -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"); } diff --git a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts index 15665563039..5926b032f58 100644 --- a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts @@ -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(); + 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", () => { diff --git a/extensions/matrix/src/runtime.ts b/extensions/matrix/src/runtime.ts index 09e0fa1da14..8738611fde6 100644 --- a/extensions/matrix/src/runtime.ts +++ b/extensions/matrix/src/runtime.ts @@ -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("Matrix runtime not initialized"); -export { getMatrixRuntime, setMatrixRuntime }; +const { + setRuntime: setMatrixRuntime, + clearRuntime: clearMatrixRuntime, + tryGetRuntime: tryGetMatrixRuntime, + getRuntime: getMatrixRuntime, +} = createPluginRuntimeStore("Matrix runtime not initialized"); +export { clearMatrixRuntime, getMatrixRuntime, setMatrixRuntime, tryGetMatrixRuntime }; diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index 4934d3674ff..31380c2cd48 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -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());