Matrix: fix typecheck and boundary drift

This commit is contained in:
Gustavo Madeira Santana
2026-03-19 07:59:01 -04:00
parent c4a4050ce4
commit f69450b170
24 changed files with 170 additions and 95 deletions

View File

@@ -59,7 +59,7 @@ describe("matrixMessageActions", () => {
const discovery = describeMessageTool!({
cfg: createConfiguredMatrixConfig(),
} as never);
} as never) ?? { actions: [] };
const actions = discovery.actions;
expect(actions).toContain("poll");
@@ -74,7 +74,7 @@ describe("matrixMessageActions", () => {
const discovery = describeMessageTool!({
cfg: createConfiguredMatrixConfig(),
} as never);
} as never) ?? { actions: [], schema: null };
const actions = discovery.actions;
const properties =
(discovery.schema as { properties?: Record<string, unknown> } | null)?.properties ?? {};
@@ -87,64 +87,66 @@ describe("matrixMessageActions", () => {
});
it("hides gated actions when the default Matrix account disables them", () => {
const actions = matrixMessageActions.describeMessageTool!({
cfg: {
channels: {
matrix: {
defaultAccount: "assistant",
actions: {
messages: true,
reactions: true,
pins: true,
profile: true,
memberInfo: true,
channelInfo: true,
verification: true,
},
accounts: {
assistant: {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
encryption: true,
actions: {
messages: false,
reactions: false,
pins: false,
profile: false,
memberInfo: false,
channelInfo: false,
verification: false,
const actions =
matrixMessageActions.describeMessageTool!({
cfg: {
channels: {
matrix: {
defaultAccount: "assistant",
actions: {
messages: true,
reactions: true,
pins: true,
profile: true,
memberInfo: true,
channelInfo: true,
verification: true,
},
accounts: {
assistant: {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
encryption: true,
actions: {
messages: false,
reactions: false,
pins: false,
profile: false,
memberInfo: false,
channelInfo: false,
verification: false,
},
},
},
},
},
},
} as CoreConfig,
} as never).actions;
} as CoreConfig,
} as never)?.actions ?? [];
expect(actions).toEqual(["poll", "poll-vote"]);
});
it("hides actions until defaultAccount is set for ambiguous multi-account configs", () => {
const actions = matrixMessageActions.describeMessageTool!({
cfg: {
channels: {
matrix: {
accounts: {
assistant: {
homeserver: "https://matrix.example.org",
accessToken: "assistant-token",
},
ops: {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
const actions =
matrixMessageActions.describeMessageTool!({
cfg: {
channels: {
matrix: {
accounts: {
assistant: {
homeserver: "https://matrix.example.org",
accessToken: "assistant-token",
},
ops: {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
},
},
},
},
},
} as CoreConfig,
} as never).actions;
} as CoreConfig,
} as never)?.actions ?? [];
expect(actions).toEqual([]);
});

View File

@@ -2,11 +2,13 @@ import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./d
import { resolveMatrixAuth } from "./matrix/client.js";
import { probeMatrix } from "./matrix/probe.js";
import { sendMessageMatrix } from "./matrix/send.js";
import { matrixOutbound } from "./outbound.js";
import { resolveMatrixTargets } from "./resolve-targets.js";
export const matrixChannelRuntime = {
listMatrixDirectoryGroupsLive,
listMatrixDirectoryPeersLive,
matrixOutbound,
probeMatrix,
resolveMatrixAuth,
resolveMatrixTargets,

View File

@@ -15,8 +15,8 @@ import {
createTextPairingAdapter,
listResolvedDirectoryEntriesFromSources,
} from "openclaw/plugin-sdk/channel-runtime";
import { buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
import { buildTrafficStatusSummary } from "../../shared/channel-status-summary.js";
import {
buildChannelConfigSchema,
buildProbeChannelStatusSummary,
@@ -47,7 +47,6 @@ import {
import { getMatrixRuntime } from "./runtime.js";
import { resolveMatrixOutboundSessionRoute } from "./session-route.js";
import { matrixSetupAdapter } from "./setup-core.js";
import { matrixSetupWizard } from "./setup-surface.js";
import type { CoreConfig } from "./types.js";
// Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
@@ -190,7 +189,6 @@ function matchMatrixAcpConversation(params: {
export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
id: "matrix",
meta,
setupWizard: matrixSetupWizard,
pairing: createTextPairingAdapter({
idLabel: "matrixUserId",
message: PAIRING_APPROVED_MESSAGE,

View File

@@ -521,7 +521,9 @@ describe("matrix CLI verification commands", () => {
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
expect(process.exitCode).toBeUndefined();
const jsonOutput = console.log.mock.calls.at(-1)?.[0];
const jsonOutput = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.at(
-1,
)?.[0];
expect(typeof jsonOutput).toBe("string");
expect(JSON.parse(String(jsonOutput))).toEqual(
expect.objectContaining({

View File

@@ -12,7 +12,7 @@ function createSyncResponse(nextBatch: string): ISyncResponse {
rooms: {
join: {
"!room:example.org": {
summary: {},
summary: { "m.heroes": [] },
state: { events: [] },
timeline: {
events: [
@@ -34,6 +34,9 @@ function createSyncResponse(nextBatch: string): ISyncResponse {
unread_notifications: {},
},
},
invite: {},
leave: {},
knock: {},
},
account_data: {
events: [

View File

@@ -52,7 +52,7 @@ function toPersistedSyncData(value: unknown): ISyncData | null {
nextBatch: value.nextBatch,
accountData: value.accountData,
roomsData: value.roomsData,
} as ISyncData;
} as unknown as ISyncData;
}
// Older Matrix state files stored the raw /sync-shaped payload directly.
@@ -64,7 +64,7 @@ function toPersistedSyncData(value: unknown): ISyncData | null {
? value.account_data.events
: [],
roomsData: isRecord(value.rooms) ? value.rooms : {},
} as ISyncData;
} as unknown as ISyncData;
}
return null;

View File

@@ -0,0 +1 @@
export { monitorMatrixProvider } from "./monitor/index.js";

View File

@@ -62,7 +62,7 @@ function createHarness(params?: {
const ensureVerificationDmTracked = vi.fn(
params?.ensureVerificationDmTracked ?? (async () => null),
);
const sendMessage = vi.fn(async () => "$notice");
const sendMessage = vi.fn(async (_roomId: string, _payload: { body?: string }) => "$notice");
const invalidateRoom = vi.fn();
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
const formatNativeDependencyHint = vi.fn(() => "install hint");

View File

@@ -100,6 +100,7 @@ function createHandlerHarness() {
mediaMaxBytes: 5 * 1024 * 1024,
startupMs: Date.now() - 120_000,
startupGraceMs: 60_000,
dropPreStartupMessages: false,
directTracker: {
isDirectMessage: vi.fn().mockResolvedValue(true),
},

View File

@@ -588,11 +588,13 @@ describe("matrix monitor handler pairing account scope", () => {
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
dropPreStartupMessages: false,
directTracker: {
isDirectMessage: async () => false,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => "sender",
needsRoomAliasesForConfig: false,
});
await handler(

View File

@@ -115,6 +115,7 @@ describe("createMatrixRoomMessageHandler thread root media", () => {
mediaMaxBytes: 5 * 1024 * 1024,
startupMs: Date.now() - 120_000,
startupGraceMs: 60_000,
dropPreStartupMessages: false,
directTracker: {
isDirectMessage: vi.fn().mockResolvedValue(true),
},

View File

@@ -7,7 +7,6 @@ const hoisted = vi.hoisted(() => {
hasPersistedSyncState: vi.fn(() => false),
};
const createMatrixRoomMessageHandler = vi.fn(() => vi.fn());
let startClientError: Error | null = null;
const resolveTextChunkLimit = vi.fn<
(cfg: unknown, channel: unknown, accountId?: unknown) => number
>(() => 4000);
@@ -27,7 +26,7 @@ const hoisted = vi.hoisted(() => {
logger,
resolveTextChunkLimit,
setActiveMatrixClient,
startClientError,
startClientError: null as Error | null,
stopSharedClientInstance,
stopThreadBindingManager,
};

View File

@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../../../src/config/config.js";
import {
__testing as sessionBindingTesting,
createTestRegistry,
type OpenClawConfig,
resolveAgentRoute,
registerSessionBindingAdapter,
} from "../../../../../src/infra/outbound/session-binding-service.js";
import { setActivePluginRegistry } from "../../../../../src/plugins/runtime.js";
import { resolveAgentRoute } from "../../../../../src/routing/resolve-route.js";
import { createTestRegistry } from "../../../../../src/test-utils/channel-plugins.js";
sessionBindingTesting,
setActivePluginRegistry,
} from "../../../../../test/helpers/extensions/matrix-route-test.js";
import { matrixPlugin } from "../../channel.js";
import { resolveMatrixInboundRoute } from "./route.js";

View File

@@ -222,7 +222,10 @@ describe("MatrixClient request hardening", () => {
it("prefers authenticated client media downloads", async () => {
const payload = Buffer.from([1, 2, 3, 4]);
const fetchMock = vi.fn(async () => new Response(payload, { status: 200 }));
const fetchMock = vi.fn(
async (_input: RequestInfo | URL, _init?: RequestInit) =>
new Response(payload, { status: 200 }),
);
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
const client = new MatrixClient("https://matrix.example.org", "token");

View File

@@ -4,6 +4,7 @@ import { EventEmitter } from "node:events";
import {
ClientEvent,
MatrixEventEvent,
Preset,
createClient as createMatrixJsClient,
type MatrixClient as MatrixJsClient,
type MatrixEvent,
@@ -547,7 +548,7 @@ export class MatrixClient {
const result = await this.client.createRoom({
invite: [remoteUserId],
is_direct: true,
preset: "trusted_private_chat",
preset: Preset.TrustedPrivateChat,
initial_state: initialState,
});
return result.room_id;

View File

@@ -621,14 +621,6 @@ export async function createMatrixThreadBindingManager(params: {
});
return record ? toSessionBindingRecord(record, defaults) : null;
},
setIdleTimeoutBySession: ({ targetSessionKey, idleTimeoutMs }) =>
manager
.setIdleTimeoutBySessionKey({ targetSessionKey, idleTimeoutMs })
.map((record) => toSessionBindingRecord(record, defaults)),
setMaxAgeBySession: ({ targetSessionKey, maxAgeMs }) =>
manager
.setMaxAgeBySessionKey({ targetSessionKey, maxAgeMs })
.map((record) => toSessionBindingRecord(record, defaults)),
touch: (bindingId, at) => {
manager.touchBinding(bindingId, at);
},

View File

@@ -1,8 +1,5 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import {
type ChannelSetupDmPolicy,
type ChannelSetupWizardAdapter,
} from "openclaw/plugin-sdk/setup";
import { type ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
import { requiresExplicitMatrixDefaultAccount } from "./account-selection.js";
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
import {
@@ -36,6 +33,54 @@ import type { CoreConfig } from "./types.js";
const channel = "matrix" as const;
type MatrixOnboardingStatus = {
channel: typeof channel;
configured: boolean;
statusLines: string[];
selectionHint?: string;
quickstartScore?: number;
};
type MatrixAccountOverrides = Partial<Record<typeof channel, string>>;
type MatrixOnboardingConfigureContext = {
cfg: CoreConfig;
runtime: RuntimeEnv;
prompter: WizardPrompter;
options?: unknown;
forceAllowFrom: boolean;
accountOverrides: MatrixAccountOverrides;
shouldPromptAccountIds: boolean;
};
type MatrixOnboardingInteractiveContext = MatrixOnboardingConfigureContext & {
configured: boolean;
label?: string;
};
type MatrixOnboardingAdapter = {
channel: typeof channel;
getStatus: (ctx: {
cfg: CoreConfig;
options?: unknown;
accountOverrides: MatrixAccountOverrides;
}) => Promise<MatrixOnboardingStatus>;
configure: (
ctx: MatrixOnboardingConfigureContext,
) => Promise<{ cfg: CoreConfig; accountId?: string }>;
configureInteractive?: (
ctx: MatrixOnboardingInteractiveContext,
) => Promise<{ cfg: CoreConfig; accountId?: string } | "skip">;
afterConfigWritten?: (ctx: {
previousCfg: CoreConfig;
cfg: CoreConfig;
accountId: string;
runtime: RuntimeEnv;
}) => Promise<void> | void;
dmPolicy?: ChannelSetupDmPolicy;
disable?: (cfg: CoreConfig) => CoreConfig;
};
function resolveMatrixOnboardingAccountId(cfg: CoreConfig, accountId?: string): string {
return normalizeAccountId(
accountId?.trim() || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID,
@@ -473,7 +518,7 @@ async function runMatrixConfigure(params: {
return { cfg: next, accountId };
}
export const matrixOnboardingAdapter: ChannelSetupWizardAdapter = {
export const matrixOnboardingAdapter: MatrixOnboardingAdapter = {
channel,
getStatus: async ({ cfg, accountOverrides }) => {
const resolvedCfg = cfg as CoreConfig;