import { describe, expect, it, vi } from "vitest"; import { matrixOnboardingAdapter } from "./onboarding.js"; import { installMatrixOnboardingEnvRestoreHooks, createMatrixWizardPrompter, runMatrixAddAccountAllowlistConfigure, runMatrixInteractiveConfigure, } from "./onboarding.test-harness.js"; import { installMatrixTestRuntime } from "./test-runtime.js"; import type { CoreConfig } from "./types.js"; vi.mock("./matrix/deps.js", () => ({ ensureMatrixSdkInstalled: vi.fn(async () => {}), isMatrixSdkAvailable: vi.fn(() => true), })); describe("matrix onboarding", () => { installMatrixOnboardingEnvRestoreHooks(); it("offers env shortcut for non-default account when scoped env vars are present", async () => { installMatrixTestRuntime(); process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org"; process.env.MATRIX_USER_ID = "@env:example.org"; process.env.MATRIX_PASSWORD = "env-password"; // pragma: allowlist secret process.env.MATRIX_ACCESS_TOKEN = ""; process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org"; process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token"; const confirmMessages: string[] = []; const prompter = createMatrixWizardPrompter({ select: { "Matrix already configured. What do you want to do?": "add-account", "Matrix auth method": "token", }, text: { "Matrix account name": "ops", }, onConfirm: (message) => { confirmMessages.push(message); return message.startsWith("Matrix env vars detected"); }, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { accounts: { default: { homeserver: "https://matrix.main.example.org", accessToken: "main-token", }, }, }, }, } as CoreConfig, prompter, shouldPromptAccountIds: true, configured: true, }); expect(result).not.toBe("skip"); if (result !== "skip") { const opsAccount = result.cfg.channels?.["matrix"]?.accounts?.ops as | { enabled?: boolean; homeserver?: string; accessToken?: string; } | undefined; expect(result.accountId).toBe("ops"); expect(opsAccount).toMatchObject({ enabled: true, }); expect(opsAccount?.homeserver).toBeUndefined(); expect(opsAccount?.accessToken).toBeUndefined(); } expect( confirmMessages.some((message) => message.startsWith( "Matrix env vars detected (MATRIX_OPS_HOMESERVER (+ auth vars)). Use env values?", ), ), ).toBe(true); }); it("routes env-shortcut add-account flow through Matrix invite auto-join setup", async () => { installMatrixTestRuntime(); process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org"; process.env.MATRIX_USER_ID = "@env:example.org"; process.env.MATRIX_PASSWORD = "env-password"; // pragma: allowlist secret process.env.MATRIX_ACCESS_TOKEN = ""; process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org"; process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token"; const notes: string[] = []; const prompter = createMatrixWizardPrompter({ notes, select: { "Matrix already configured. What do you want to do?": "add-account", "Matrix rooms access": "allowlist", "Matrix invite auto-join": "allowlist", }, text: { "Matrix account name": "ops", "Matrix rooms allowlist (comma-separated)": "!ops-room:example.org", "Matrix invite auto-join allowlist (comma-separated)": "#ops-invites:example.org", }, confirm: { "Configure Matrix rooms access?": true, "Configure Matrix invite auto-join?": true, }, onConfirm: (message) => message.startsWith("Matrix env vars detected"), }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { accounts: { default: { homeserver: "https://matrix.main.example.org", accessToken: "main-token", }, }, }, }, } as CoreConfig, prompter, shouldPromptAccountIds: true, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.accountId).toBe("ops"); expect(result.cfg.channels?.matrix?.accounts?.ops).toMatchObject({ enabled: true, groupPolicy: "allowlist", groups: { "!ops-room:example.org": { enabled: true }, }, autoJoin: "allowlist", autoJoinAllowlist: ["#ops-invites:example.org"], }); expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off."); }); it("promotes legacy top-level Matrix config before adding a named account", async () => { installMatrixTestRuntime(); const prompter = createMatrixWizardPrompter({ select: { "Matrix already configured. What do you want to do?": "add-account", "Matrix auth method": "token", }, text: { "Matrix account name": "ops", "Matrix homeserver URL": "https://matrix.ops.example.org", "Matrix access token": "ops-token", "Matrix device name (optional)": "", }, onConfirm: async () => false, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { homeserver: "https://matrix.main.example.org", userId: "@main:example.org", accessToken: "main-token", avatarUrl: "mxc://matrix.main.example.org/main-avatar", }, }, } as CoreConfig, prompter, shouldPromptAccountIds: true, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.cfg.channels?.matrix?.homeserver).toBeUndefined(); expect(result.cfg.channels?.matrix?.accessToken).toBeUndefined(); expect(result.cfg.channels?.matrix?.avatarUrl).toBeUndefined(); expect(result.cfg.channels?.matrix?.accounts?.default).toMatchObject({ homeserver: "https://matrix.main.example.org", userId: "@main:example.org", accessToken: "main-token", avatarUrl: "mxc://matrix.main.example.org/main-avatar", }); expect(result.cfg.channels?.matrix?.accounts?.ops).toMatchObject({ name: "ops", homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", }); }); it("reuses an existing raw default-like key during onboarding promotion when defaultAccount is unset", async () => { installMatrixTestRuntime(); const prompter = createMatrixWizardPrompter({ select: { "Matrix already configured. What do you want to do?": "add-account", "Matrix auth method": "token", }, text: { "Matrix account name": "ops", "Matrix homeserver URL": "https://matrix.ops.example.org", "Matrix access token": "ops-token", "Matrix device name (optional)": "", }, onConfirm: async () => false, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { homeserver: "https://matrix.main.example.org", userId: "@main:example.org", accessToken: "main-token", avatarUrl: "mxc://matrix.main.example.org/main-avatar", accounts: { Default: { enabled: true, deviceName: "Legacy raw key", }, support: { homeserver: "https://matrix.support.example.org", accessToken: "support-token", }, }, }, }, } as CoreConfig, prompter, shouldPromptAccountIds: true, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.cfg.channels?.matrix?.accounts?.Default).toMatchObject({ enabled: true, deviceName: "Legacy raw key", homeserver: "https://matrix.main.example.org", userId: "@main:example.org", accessToken: "main-token", avatarUrl: "mxc://matrix.main.example.org/main-avatar", }); expect(result.cfg.channels?.matrix?.accounts?.default).toBeUndefined(); expect(result.cfg.channels?.matrix?.accounts?.support).toMatchObject({ homeserver: "https://matrix.support.example.org", accessToken: "support-token", }); expect(result.cfg.channels?.matrix?.accounts?.ops).toMatchObject({ name: "ops", homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", }); }); it("includes device env var names in auth help text", async () => { installMatrixTestRuntime(); const notes: string[] = []; const prompter = createMatrixWizardPrompter({ notes, onText: async () => { throw new Error("stop-after-help"); }, onConfirm: async () => false, onSelect: async () => "token", }); await expect( runMatrixInteractiveConfigure({ cfg: { channels: {} } as CoreConfig, prompter, }), ).rejects.toThrow("stop-after-help"); const noteText = notes.join("\n"); expect(noteText).toContain("MATRIX_DEVICE_ID"); expect(noteText).toContain("MATRIX_DEVICE_NAME"); expect(noteText).toContain("MATRIX__DEVICE_ID"); expect(noteText).toContain("MATRIX__DEVICE_NAME"); }); it("prompts for private-network access when onboarding an internal http homeserver", async () => { installMatrixTestRuntime(); const prompter = createMatrixWizardPrompter({ select: { "Matrix auth method": "token", }, text: { "Matrix homeserver URL": "http://localhost.localdomain:8008", "Matrix access token": "ops-token", "Matrix device name (optional)": "", }, confirm: { "Allow private/internal Matrix homeserver traffic for this account?": true, "Enable end-to-end encryption (E2EE)?": false, }, onConfirm: async () => false, }); const result = await runMatrixInteractiveConfigure({ cfg: {} as CoreConfig, prompter, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.cfg.channels?.matrix).toMatchObject({ homeserver: "http://localhost.localdomain:8008", network: { dangerouslyAllowPrivateNetwork: true, }, accessToken: "ops-token", }); }); it("preserves SecretRef access tokens when keeping existing credentials", async () => { installMatrixTestRuntime(); process.env.MATRIX_ACCESS_TOKEN = "env-token"; const prompter = createMatrixWizardPrompter({ select: { "Matrix already configured. What do you want to do?": "update", }, text: { "Matrix homeserver URL": "https://matrix.example.org", "Matrix device name (optional)": "OpenClaw Gateway", }, confirm: { "Matrix credentials already configured. Keep them?": true, "Enable end-to-end encryption (E2EE)?": false, "Configure Matrix rooms access?": false, "Configure Matrix invite auto-join?": false, }, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { homeserver: "https://matrix.example.org", accessToken: { source: "env", provider: "default", id: "MATRIX_ACCESS_TOKEN" }, }, }, secrets: { defaults: { env: "default", }, }, } as CoreConfig, prompter, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.cfg.channels?.matrix?.accessToken).toEqual({ source: "env", provider: "default", id: "MATRIX_ACCESS_TOKEN", }); }); it("resolves status using the overridden Matrix account", async () => { const status = await matrixOnboardingAdapter.getStatus({ cfg: { channels: { matrix: { defaultAccount: "default", accounts: { default: { homeserver: "https://matrix.default.example.org", }, ops: { homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", }, }, }, }, } as CoreConfig, options: undefined, accountOverrides: { matrix: "ops", }, }); expect(status.configured).toBe(true); expect(status.selectionHint).toBe("configured"); expect(status.statusLines).toEqual(["Matrix: configured"]); }); it("writes allowlists and room access to the selected Matrix account", async () => { installMatrixTestRuntime(); const notes: string[] = []; const result = await runMatrixAddAccountAllowlistConfigure({ cfg: { channels: { matrix: { accounts: { default: { homeserver: "https://matrix.main.example.org", accessToken: "main-token", }, }, }, }, } as CoreConfig, allowFromInput: "@alice:example.org", roomsAllowlistInput: "!ops-room:example.org", autoJoinAllowlistInput: "#ops-invites:example.org", deviceName: "Ops Gateway", notes, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.accountId).toBe("ops"); expect(result.cfg.channels?.["matrix"]?.accounts?.ops).toMatchObject({ homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", deviceName: "Ops Gateway", dm: { policy: "allowlist", allowFrom: ["@alice:example.org"], }, groupPolicy: "allowlist", autoJoin: "allowlist", autoJoinAllowlist: ["#ops-invites:example.org"], groups: { "!ops-room:example.org": { enabled: true }, }, }); expect(result.cfg.channels?.["matrix"]?.dm).toBeUndefined(); expect(result.cfg.channels?.["matrix"]?.groups).toBeUndefined(); expect(notes.join("\n")).toContain("WARNING: Matrix invite auto-join defaults to off."); }); it("clears Matrix invite auto-join allowlists when switching auto-join off", async () => { installMatrixTestRuntime(); const notes: string[] = []; const prompter = createMatrixWizardPrompter({ notes, select: { "Matrix already configured. What do you want to do?": "update", "Matrix invite auto-join": "off", }, text: { "Matrix homeserver URL": "https://matrix.example.org", "Matrix device name (optional)": "OpenClaw Gateway", }, confirm: { "Matrix credentials already configured. Keep them?": true, "Enable end-to-end encryption (E2EE)?": false, "Configure Matrix rooms access?": false, "Configure Matrix invite auto-join?": true, "Update Matrix invite auto-join?": true, }, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { homeserver: "https://matrix.example.org", accessToken: "matrix-token", autoJoin: "allowlist", autoJoinAllowlist: ["#ops:example.org"], }, }, } as CoreConfig, prompter, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(result.cfg.channels?.matrix?.autoJoin).toBe("off"); expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toBeUndefined(); expect(notes.join("\n")).toContain("Matrix invite auto-join remains off."); expect(notes.join("\n")).toContain( "Agents will not join invited rooms or fresh DM-style invites until you change autoJoin.", ); }); it("re-prompts Matrix invite auto-join allowlists until entries are stable invite targets", async () => { installMatrixTestRuntime(); const notes: string[] = []; let inviteAllowlistPrompts = 0; const prompter = createMatrixWizardPrompter({ notes, select: { "Matrix already configured. What do you want to do?": "update", "Matrix invite auto-join": "allowlist", }, text: { "Matrix homeserver URL": "https://matrix.example.org", "Matrix device name (optional)": "OpenClaw Gateway", }, confirm: { "Matrix credentials already configured. Keep them?": true, "Enable end-to-end encryption (E2EE)?": false, "Configure Matrix rooms access?": false, "Configure Matrix invite auto-join?": true, "Update Matrix invite auto-join?": true, }, onText: async (message) => { if (message === "Matrix invite auto-join allowlist (comma-separated)") { inviteAllowlistPrompts += 1; return inviteAllowlistPrompts === 1 ? "Project Room" : "#ops:example.org"; } throw new Error(`unexpected text prompt: ${message}`); }, }); const result = await runMatrixInteractiveConfigure({ cfg: { channels: { matrix: { homeserver: "https://matrix.example.org", accessToken: "matrix-token", }, }, } as CoreConfig, prompter, configured: true, }); expect(result).not.toBe("skip"); if (result === "skip") { return; } expect(inviteAllowlistPrompts).toBe(2); expect(result.cfg.channels?.matrix?.autoJoin).toBe("allowlist"); expect(result.cfg.channels?.matrix?.autoJoinAllowlist).toEqual(["#ops:example.org"]); expect(notes.join("\n")).toContain( "Use only stable Matrix invite targets for auto-join: !roomId:server, #alias:server, or *.", ); expect(notes.join("\n")).toContain("Invalid: Project Room"); }); it("reports account-scoped DM config keys for named accounts", () => { const resolveConfigKeys = matrixOnboardingAdapter.dmPolicy?.resolveConfigKeys; expect(resolveConfigKeys).toBeDefined(); if (!resolveConfigKeys) { return; } expect( resolveConfigKeys( { channels: { matrix: { accounts: { default: { homeserver: "https://matrix.main.example.org", }, ops: { homeserver: "https://matrix.ops.example.org", }, }, }, }, } as CoreConfig, "ops", ), ).toEqual({ policyKey: "channels.matrix.accounts.ops.dm.policy", allowFromKey: "channels.matrix.accounts.ops.dm.allowFrom", }); }); it("reports configured when only the effective default Matrix account is configured", async () => { installMatrixTestRuntime(); const status = await matrixOnboardingAdapter.getStatus({ cfg: { channels: { matrix: { defaultAccount: "ops", accounts: { ops: { homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", }, }, }, }, } as CoreConfig, accountOverrides: {}, }); expect(status.configured).toBe(true); expect(status.statusLines).toContain("Matrix: configured"); expect(status.selectionHint).toBe("configured"); }); it("asks for defaultAccount when multiple named Matrix accounts exist", async () => { installMatrixTestRuntime(); const status = await matrixOnboardingAdapter.getStatus({ cfg: { channels: { matrix: { accounts: { assistant: { homeserver: "https://matrix.assistant.example.org", accessToken: "assistant-token", }, ops: { homeserver: "https://matrix.ops.example.org", accessToken: "ops-token", }, }, }, }, } as CoreConfig, accountOverrides: {}, }); expect(status.configured).toBe(false); expect(status.statusLines).toEqual([ 'Matrix: set "channels.matrix.defaultAccount" to select a named account', ]); expect(status.selectionHint).toBe("set defaultAccount"); }); });