From 7bb95354c4468084d0042c6acb8902656062eb44 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 17:04:23 +0000 Subject: [PATCH] test: dedupe matrix setup seams --- extensions/matrix/src/channel.setup.test.ts | 297 +++++++++--------- .../src/matrix/actions/messages.test.ts | 147 +++------ .../matrix/src/matrix/thread-bindings.test.ts | 112 ++++--- 3 files changed, 244 insertions(+), 312 deletions(-) diff --git a/extensions/matrix/src/channel.setup.test.ts b/extensions/matrix/src/channel.setup.test.ts index ca482668ace..fac9ee0a7b8 100644 --- a/extensions/matrix/src/channel.setup.test.ts +++ b/extensions/matrix/src/channel.setup.test.ts @@ -19,12 +19,108 @@ describe("matrix setup post-write bootstrap", () => { const exit = vi.fn((code: number): never => { throw new Error(`exit ${code}`); }); + const encryptedDefaultCfg = { + channels: { + matrix: { + encryption: true, + }, + }, + } as CoreConfig; + const defaultPasswordInput = { + homeserver: "https://matrix.example.org", + userId: "@flurry:example.org", + password: "secret", // pragma: allowlist secret + } as const; const runtime: RuntimeEnv = { log, error, exit, }; + function applyAccountConfig(params: { + previousCfg: CoreConfig; + accountId: string; + input: Record; + }) { + return { + previousCfg: params.previousCfg, + accountId: params.accountId, + input: params.input, + nextCfg: matrixPlugin.setup!.applyAccountConfig({ + cfg: params.previousCfg, + accountId: params.accountId, + input: params.input, + }) as CoreConfig, + }; + } + + function applyDefaultAccountConfig(input: Record = defaultPasswordInput) { + return applyAccountConfig({ + previousCfg: encryptedDefaultCfg, + accountId: "default", + input, + }); + } + + function mockBootstrapResult(params: { + success: boolean; + backupVersion?: string | null; + error?: string; + }) { + verificationMocks.bootstrapMatrixVerification.mockResolvedValue({ + success: params.success, + ...(params.error ? { error: params.error } : {}), + verification: { + backupVersion: params.backupVersion ?? null, + }, + crossSigning: {}, + pendingVerifications: 0, + cryptoBootstrap: null, + }); + } + + async function runAfterAccountConfigWritten(params: { + previousCfg: CoreConfig; + nextCfg: CoreConfig; + accountId: string; + input: Record; + }) { + await matrixPlugin.setup!.afterAccountConfigWritten?.({ + previousCfg: params.previousCfg, + cfg: params.nextCfg, + accountId: params.accountId, + input: params.input, + runtime, + }); + } + + async function withSavedEnv( + values: Record, + run: () => Promise | T, + ) { + const previousEnv = Object.fromEntries( + Object.keys(values).map((key) => [key, process.env[key]]), + ) as Record; + for (const [key, value] of Object.entries(values)) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + try { + return await run(); + } finally { + for (const [key, value] of Object.entries(previousEnv)) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } + } + beforeEach(() => { verificationMocks.bootstrapMatrixVerification.mockReset(); log.mockClear(); @@ -34,40 +130,10 @@ describe("matrix setup post-write bootstrap", () => { }); it("bootstraps verification for newly added encrypted accounts", async () => { - const previousCfg = { - channels: { - matrix: { - encryption: true, - }, - }, - } as CoreConfig; - const input = { - homeserver: "https://matrix.example.org", - userId: "@flurry:example.org", - password: "secret", // pragma: allowlist secret - }; - const nextCfg = matrixPlugin.setup!.applyAccountConfig({ - cfg: previousCfg, - accountId: "default", - input, - }) as CoreConfig; - verificationMocks.bootstrapMatrixVerification.mockResolvedValue({ - success: true, - verification: { - backupVersion: "7", - }, - crossSigning: {}, - pendingVerifications: 0, - cryptoBootstrap: null, - }); + const { previousCfg, nextCfg, accountId, input } = applyDefaultAccountConfig(); + mockBootstrapResult({ success: true, backupVersion: "7" }); - await matrixPlugin.setup!.afterAccountConfigWritten?.({ - previousCfg, - cfg: nextCfg, - accountId: "default", - input, - runtime, - }); + await runAfterAccountConfigWritten({ previousCfg, nextCfg, accountId, input }); expect(verificationMocks.bootstrapMatrixVerification).toHaveBeenCalledWith({ accountId: "default", @@ -97,61 +163,27 @@ describe("matrix setup post-write bootstrap", () => { userId: "@flurry:example.org", accessToken: "new-token", }; - const nextCfg = matrixPlugin.setup!.applyAccountConfig({ - cfg: previousCfg, - accountId: "flurry", - input, - }) as CoreConfig; - - await matrixPlugin.setup!.afterAccountConfigWritten?.({ + const { nextCfg, accountId } = applyAccountConfig({ previousCfg, - cfg: nextCfg, accountId: "flurry", input, - runtime, }); + await runAfterAccountConfigWritten({ previousCfg, nextCfg, accountId, input }); + expect(verificationMocks.bootstrapMatrixVerification).not.toHaveBeenCalled(); expect(log).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); }); it("logs a warning when verification bootstrap fails", async () => { - const previousCfg = { - channels: { - matrix: { - encryption: true, - }, - }, - } as CoreConfig; - const input = { - homeserver: "https://matrix.example.org", - userId: "@flurry:example.org", - password: "secret", // pragma: allowlist secret - }; - const nextCfg = matrixPlugin.setup!.applyAccountConfig({ - cfg: previousCfg, - accountId: "default", - input, - }) as CoreConfig; - verificationMocks.bootstrapMatrixVerification.mockResolvedValue({ + const { previousCfg, nextCfg, accountId, input } = applyDefaultAccountConfig(); + mockBootstrapResult({ success: false, error: "no room-key backup exists on the homeserver", - verification: { - backupVersion: null, - }, - crossSigning: {}, - pendingVerifications: 0, - cryptoBootstrap: null, }); - await matrixPlugin.setup!.afterAccountConfigWritten?.({ - previousCfg, - cfg: nextCfg, - accountId: "default", - input, - runtime, - }); + await runAfterAccountConfigWritten({ previousCfg, nextCfg, accountId, input }); expect(error).toHaveBeenCalledWith( 'Matrix verification bootstrap warning for "default": no room-key backup exists on the homeserver', @@ -159,92 +191,49 @@ describe("matrix setup post-write bootstrap", () => { }); it("bootstraps a newly added env-backed default account when encryption is already enabled", async () => { - const previousEnv = { - MATRIX_HOMESERVER: process.env.MATRIX_HOMESERVER, - MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN, - }; - process.env.MATRIX_HOMESERVER = "https://matrix.example.org"; - process.env.MATRIX_ACCESS_TOKEN = "env-token"; - try { - const previousCfg = { - channels: { - matrix: { - encryption: true, - }, - }, - } as CoreConfig; - const input = { - useEnv: true, - }; - const nextCfg = matrixPlugin.setup!.applyAccountConfig({ - cfg: previousCfg, - accountId: "default", - input, - }) as CoreConfig; - verificationMocks.bootstrapMatrixVerification.mockResolvedValue({ - success: true, - verification: { - backupVersion: "9", - }, - crossSigning: {}, - pendingVerifications: 0, - cryptoBootstrap: null, - }); + await withSavedEnv( + { + MATRIX_HOMESERVER: "https://matrix.example.org", + MATRIX_ACCESS_TOKEN: "env-token", + }, + async () => { + const { previousCfg, nextCfg, accountId, input } = applyDefaultAccountConfig({ + useEnv: true, + }); + mockBootstrapResult({ success: true, backupVersion: "9" }); - await matrixPlugin.setup!.afterAccountConfigWritten?.({ - previousCfg, - cfg: nextCfg, - accountId: "default", - input, - runtime, - }); + await runAfterAccountConfigWritten({ previousCfg, nextCfg, accountId, input }); - expect(verificationMocks.bootstrapMatrixVerification).toHaveBeenCalledWith({ - accountId: "default", - }); - expect(log).toHaveBeenCalledWith('Matrix verification bootstrap: complete for "default".'); - } finally { - for (const [key, value] of Object.entries(previousEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } + expect(verificationMocks.bootstrapMatrixVerification).toHaveBeenCalledWith({ + accountId: "default", + }); + expect(log).toHaveBeenCalledWith('Matrix verification bootstrap: complete for "default".'); + }, + ); }); it("rejects default useEnv setup when no Matrix auth env vars are available", () => { - const previousEnv = { - MATRIX_HOMESERVER: process.env.MATRIX_HOMESERVER, - MATRIX_USER_ID: process.env.MATRIX_USER_ID, - MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN, - MATRIX_PASSWORD: process.env.MATRIX_PASSWORD, - MATRIX_DEFAULT_HOMESERVER: process.env.MATRIX_DEFAULT_HOMESERVER, - MATRIX_DEFAULT_USER_ID: process.env.MATRIX_DEFAULT_USER_ID, - MATRIX_DEFAULT_ACCESS_TOKEN: process.env.MATRIX_DEFAULT_ACCESS_TOKEN, - MATRIX_DEFAULT_PASSWORD: process.env.MATRIX_DEFAULT_PASSWORD, - }; - for (const key of Object.keys(previousEnv)) { - delete process.env[key]; - } - try { - expect( - matrixPlugin.setup!.validateInput?.({ - cfg: {} as CoreConfig, - accountId: "default", - input: { useEnv: true }, - }), - ).toContain("Set Matrix env vars for the default account"); - } finally { - for (const [key, value] of Object.entries(previousEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } + return withSavedEnv( + { + MATRIX_HOMESERVER: undefined, + MATRIX_USER_ID: undefined, + MATRIX_ACCESS_TOKEN: undefined, + MATRIX_PASSWORD: undefined, + MATRIX_DEFAULT_HOMESERVER: undefined, + MATRIX_DEFAULT_USER_ID: undefined, + MATRIX_DEFAULT_ACCESS_TOKEN: undefined, + MATRIX_DEFAULT_PASSWORD: undefined, + }, + () => { + expect( + matrixPlugin.setup!.validateInput?.({ + cfg: {} as CoreConfig, + accountId: "default", + input: { useEnv: true }, + }), + ).toContain("Set Matrix env vars for the default account"); + }, + ); }); it("clears allowPrivateNetwork when deleting the default Matrix account config", () => { diff --git a/extensions/matrix/src/matrix/actions/messages.test.ts b/extensions/matrix/src/matrix/actions/messages.test.ts index 1ed2291d916..e74925f7b5b 100644 --- a/extensions/matrix/src/matrix/actions/messages.test.ts +++ b/extensions/matrix/src/matrix/actions/messages.test.ts @@ -2,6 +2,40 @@ import { describe, expect, it, vi } from "vitest"; import type { MatrixClient } from "../sdk.js"; import { readMatrixMessages } from "./messages.js"; +function createPollResponseEvent(): Record { + return { + event_id: "$vote", + sender: "@bob:example.org", + type: "m.poll.response", + origin_server_ts: 20, + content: { + "m.poll.response": { answers: ["a1"] }, + "m.relates_to": { rel_type: "m.reference", event_id: "$poll" }, + }, + }; +} + +function createPollStartEvent(params?: { + answers?: Array>; + includeDisclosedKind?: boolean; + maxSelections?: number; +}): Record { + return { + event_id: "$poll", + sender: "@alice:example.org", + type: "m.poll.start", + origin_server_ts: 1, + content: { + "m.poll.start": { + question: { "m.text": "Favorite fruit?" }, + ...(params?.includeDisclosedKind ? { kind: "m.poll.disclosed" } : {}), + ...(params?.maxSelections !== undefined ? { max_selections: params.maxSelections } : {}), + answers: params?.answers ?? [{ id: "a1", "m.text": "Apple" }], + }, + }, + }; +} + function createMessagesClient(params: { chunk: Array>; hydratedChunk?: Array>; @@ -43,16 +77,7 @@ describe("matrix message actions", () => { it("includes poll snapshots when reading message history", async () => { const { client, doRequest, getEvent, getRelations } = createMessagesClient({ chunk: [ - { - event_id: "$vote", - sender: "@bob:example.org", - type: "m.poll.response", - origin_server_ts: 20, - content: { - "m.poll.response": { answers: ["a1"] }, - "m.relates_to": { rel_type: "m.reference", event_id: "$poll" }, - }, - }, + createPollResponseEvent(), { event_id: "$msg", sender: "@alice:example.org", @@ -64,35 +89,15 @@ describe("matrix message actions", () => { }, }, ], - pollRoot: { - event_id: "$poll", - sender: "@alice:example.org", - type: "m.poll.start", - origin_server_ts: 1, - content: { - "m.poll.start": { - question: { "m.text": "Favorite fruit?" }, - kind: "m.poll.disclosed", - max_selections: 1, - answers: [ - { id: "a1", "m.text": "Apple" }, - { id: "a2", "m.text": "Strawberry" }, - ], - }, - }, - }, - pollRelations: [ - { - event_id: "$vote", - sender: "@bob:example.org", - type: "m.poll.response", - origin_server_ts: 20, - content: { - "m.poll.response": { answers: ["a1"] }, - "m.relates_to": { rel_type: "m.reference", event_id: "$poll" }, - }, - }, - ], + pollRoot: createPollStartEvent({ + includeDisclosedKind: true, + maxSelections: 1, + answers: [ + { id: "a1", "m.text": "Apple" }, + { id: "a2", "m.text": "Strawberry" }, + ], + }), + pollRelations: [createPollResponseEvent()], }); const result = await readMatrixMessages("room:!room:example.org", { client, limit: 2.9 }); @@ -127,42 +132,8 @@ describe("matrix message actions", () => { it("dedupes multiple poll events for the same poll within one read page", async () => { const { client, getEvent } = createMessagesClient({ - chunk: [ - { - event_id: "$vote", - sender: "@bob:example.org", - type: "m.poll.response", - origin_server_ts: 20, - content: { - "m.poll.response": { answers: ["a1"] }, - "m.relates_to": { rel_type: "m.reference", event_id: "$poll" }, - }, - }, - { - event_id: "$poll", - sender: "@alice:example.org", - type: "m.poll.start", - origin_server_ts: 1, - content: { - "m.poll.start": { - question: { "m.text": "Favorite fruit?" }, - answers: [{ id: "a1", "m.text": "Apple" }], - }, - }, - }, - ], - pollRoot: { - event_id: "$poll", - sender: "@alice:example.org", - type: "m.poll.start", - origin_server_ts: 1, - content: { - "m.poll.start": { - question: { "m.text": "Favorite fruit?" }, - answers: [{ id: "a1", "m.text": "Apple" }], - }, - }, - }, + chunk: [createPollResponseEvent(), createPollStartEvent()], + pollRoot: createPollStartEvent(), pollRelations: [], }); @@ -189,30 +160,8 @@ describe("matrix message actions", () => { content: {}, }, ], - hydratedChunk: [ - { - event_id: "$vote", - sender: "@bob:example.org", - type: "m.poll.response", - origin_server_ts: 20, - content: { - "m.poll.response": { answers: ["a1"] }, - "m.relates_to": { rel_type: "m.reference", event_id: "$poll" }, - }, - }, - ], - pollRoot: { - event_id: "$poll", - sender: "@alice:example.org", - type: "m.poll.start", - origin_server_ts: 1, - content: { - "m.poll.start": { - question: { "m.text": "Favorite fruit?" }, - answers: [{ id: "a1", "m.text": "Apple" }], - }, - }, - }, + hydratedChunk: [createPollResponseEvent()], + pollRoot: createPollStartEvent(), pollRelations: [], }); diff --git a/extensions/matrix/src/matrix/thread-bindings.test.ts b/extensions/matrix/src/matrix/thread-bindings.test.ts index be193a920a1..a22243b2147 100644 --- a/extensions/matrix/src/matrix/thread-bindings.test.ts +++ b/extensions/matrix/src/matrix/thread-bindings.test.ts @@ -41,6 +41,50 @@ describe("matrix thread bindings", () => { userId: "@bot:example.org", accessToken: "token", } as const; + const accountId = "ops"; + const idleTimeoutMs = 24 * 60 * 60 * 1000; + const matrixClient = {} as never; + + function currentThreadConversation(params?: { + conversationId?: string; + parentConversationId?: string; + }) { + return { + channel: "matrix" as const, + accountId, + conversationId: params?.conversationId ?? "$thread", + parentConversationId: params?.parentConversationId ?? "!room:example", + }; + } + + async function createStaticThreadBindingManager() { + return createMatrixThreadBindingManager({ + accountId, + auth, + client: matrixClient, + idleTimeoutMs, + maxAgeMs: 0, + enableSweeper: false, + }); + } + + async function bindCurrentThread(params?: { + targetSessionKey?: string; + conversationId?: string; + parentConversationId?: string; + metadata?: { introText?: string }; + }) { + return getSessionBindingService().bind({ + targetSessionKey: params?.targetSessionKey ?? "agent:ops:subagent:child", + targetKind: "subagent", + conversation: currentThreadConversation({ + conversationId: params?.conversationId, + parentConversationId: params?.parentConversationId, + }), + placement: "current", + ...(params?.metadata ? { metadata: params.metadata } : {}), + }); + } function resolveBindingsFilePath(customStateDir?: string) { return path.join( @@ -77,10 +121,10 @@ describe("matrix thread bindings", () => { it("creates child Matrix thread bindings from a top-level room context", async () => { await createMatrixThreadBindingManager({ - accountId: "ops", + accountId, auth, - client: {} as never, - idleTimeoutMs: 24 * 60 * 60 * 1000, + client: matrixClient, + idleTimeoutMs, maxAgeMs: 0, enableSweeper: false, }); @@ -112,25 +156,9 @@ describe("matrix thread bindings", () => { }); it("posts intro messages inside existing Matrix threads for current placement", async () => { - await createMatrixThreadBindingManager({ - accountId: "ops", - auth, - client: {} as never, - idleTimeoutMs: 24 * 60 * 60 * 1000, - maxAgeMs: 0, - enableSweeper: false, - }); + await createStaticThreadBindingManager(); - const binding = await getSessionBindingService().bind({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - conversation: { - channel: "matrix", - accountId: "ops", - conversationId: "$thread", - parentConversationId: "!room:example", - }, - placement: "current", + const binding = await bindCurrentThread({ metadata: { introText: "intro thread", }, @@ -574,25 +602,8 @@ describe("matrix thread bindings", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z")); try { - await createMatrixThreadBindingManager({ - accountId: "ops", - auth, - client: {} as never, - idleTimeoutMs: 24 * 60 * 60 * 1000, - maxAgeMs: 0, - enableSweeper: false, - }); - const binding = await getSessionBindingService().bind({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - conversation: { - channel: "matrix", - accountId: "ops", - conversationId: "$thread", - parentConversationId: "!room:example", - }, - placement: "current", - }); + await createStaticThreadBindingManager(); + const binding = await bindCurrentThread(); const bindingsPath = resolveBindingsFilePath(); const originalLastActivityAt = await readPersistedLastActivityAt(bindingsPath); @@ -618,25 +629,8 @@ describe("matrix thread bindings", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z")); try { - const manager = await createMatrixThreadBindingManager({ - accountId: "ops", - auth, - client: {} as never, - idleTimeoutMs: 24 * 60 * 60 * 1000, - maxAgeMs: 0, - enableSweeper: false, - }); - const binding = await getSessionBindingService().bind({ - targetSessionKey: "agent:ops:subagent:child", - targetKind: "subagent", - conversation: { - channel: "matrix", - accountId: "ops", - conversationId: "$thread", - parentConversationId: "!room:example", - }, - placement: "current", - }); + const manager = await createStaticThreadBindingManager(); + const binding = await bindCurrentThread(); const touchedAt = Date.parse("2026-03-06T12:00:00.000Z"); getSessionBindingService().touch(binding.bindingId, touchedAt);