test: dedupe matrix setup seams

This commit is contained in:
Peter Steinberger
2026-03-26 17:04:23 +00:00
parent c12623a857
commit 7bb95354c4
3 changed files with 244 additions and 312 deletions

View File

@@ -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<string, unknown>;
}) {
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<string, unknown> = 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<string, unknown>;
}) {
await matrixPlugin.setup!.afterAccountConfigWritten?.({
previousCfg: params.previousCfg,
cfg: params.nextCfg,
accountId: params.accountId,
input: params.input,
runtime,
});
}
async function withSavedEnv<T>(
values: Record<string, string | undefined>,
run: () => Promise<T> | T,
) {
const previousEnv = Object.fromEntries(
Object.keys(values).map((key) => [key, process.env[key]]),
) as Record<string, string | undefined>;
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,76 +191,40 @@ 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,
await withSavedEnv(
{
MATRIX_HOMESERVER: "https://matrix.example.org",
MATRIX_ACCESS_TOKEN: "env-token",
},
},
} as CoreConfig;
const input = {
async () => {
const { previousCfg, nextCfg, accountId, input } = applyDefaultAccountConfig({
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,
});
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;
}
}
}
},
);
});
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 {
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,
@@ -236,15 +232,8 @@ describe("matrix setup post-write bootstrap", () => {
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;
}
}
}
},
);
});
it("clears allowPrivateNetwork when deleting the default Matrix account config", () => {

View File

@@ -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<string, unknown> {
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<Record<string, unknown>>;
includeDisclosedKind?: boolean;
maxSelections?: number;
}): Record<string, unknown> {
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<Record<string, unknown>>;
hydratedChunk?: Array<Record<string, unknown>>;
@@ -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,
pollRoot: createPollStartEvent({
includeDisclosedKind: true,
maxSelections: 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" },
},
},
],
}),
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: [],
});

View File

@@ -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);