From 44cd4fb55fde2d1715aa163be2e296ad032e9924 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 19 Mar 2026 07:50:02 -0700 Subject: [PATCH] fix(ci): repair main type and boundary regressions --- .../src/actions.account-propagation.test.ts | 6 +- extensions/matrix/src/actions.test.ts | 134 ++++++++++-------- extensions/matrix/src/cli.test.ts | 14 +- .../src/matrix/client/file-sync-store.test.ts | 4 +- .../src/matrix/client/file-sync-store.ts | 35 ++++- .../matrix/src/matrix/monitor/events.test.ts | 2 +- .../matrix/monitor/handler.test-helpers.ts | 3 +- .../matrix/src/matrix/monitor/handler.test.ts | 2 + .../matrix/src/matrix/monitor/index.test.ts | 13 +- .../matrix/src/matrix/monitor/route.test.ts | 8 +- extensions/matrix/src/matrix/sdk.test.ts | 27 ++-- extensions/matrix/src/setup-surface.ts | 5 +- src/agents/acp-spawn.test.ts | 17 ++- .../subagent-announce.format.e2e.test.ts | 89 ++++++++---- src/channels/plugins/message-action-names.ts | 1 + src/commands/channels/add.ts | 6 +- src/commands/channels/remove.ts | 9 +- src/plugin-sdk/core.ts | 2 +- src/plugin-sdk/setup.ts | 5 +- .../extensions/matrix-monitor-route.ts | 8 ++ 20 files changed, 246 insertions(+), 144 deletions(-) create mode 100644 test/helpers/extensions/matrix-monitor-route.ts diff --git a/extensions/matrix/src/actions.account-propagation.test.ts b/extensions/matrix/src/actions.account-propagation.test.ts index 0675fb2e440..12dfea963f3 100644 --- a/extensions/matrix/src/actions.account-propagation.test.ts +++ b/extensions/matrix/src/actions.account-propagation.test.ts @@ -12,6 +12,8 @@ vi.mock("./tool-actions.js", () => ({ const { matrixMessageActions } = await import("./actions.js"); +const profileAction = "set-profile" as ChannelMessageActionContext["action"]; + function createContext( overrides: Partial, ): ChannelMessageActionContext { @@ -88,7 +90,7 @@ describe("matrixMessageActions account propagation", () => { it("forwards accountId for self-profile updates", async () => { await matrixMessageActions.handleAction?.( createContext({ - action: "set-profile", + action: profileAction, accountId: "ops", params: { displayName: "Ops Bot", @@ -112,7 +114,7 @@ describe("matrixMessageActions account propagation", () => { it("forwards local avatar paths for self-profile updates", async () => { await matrixMessageActions.handleAction?.( createContext({ - action: "set-profile", + action: profileAction, accountId: "ops", params: { path: "/tmp/avatar.jpg", diff --git a/extensions/matrix/src/actions.test.ts b/extensions/matrix/src/actions.test.ts index df34411b806..5e657bb4603 100644 --- a/extensions/matrix/src/actions.test.ts +++ b/extensions/matrix/src/actions.test.ts @@ -4,6 +4,8 @@ import { matrixMessageActions } from "./actions.js"; import { setMatrixRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; +const profileAction = "set-profile" as const; + const runtimeStub = { config: { loadConfig: () => ({}), @@ -52,101 +54,115 @@ describe("matrixMessageActions", () => { it("exposes poll create but only handles poll votes inside the plugin", () => { const describeMessageTool = matrixMessageActions.describeMessageTool; - const supportsAction = matrixMessageActions.supportsAction; + const supportsAction = matrixMessageActions.supportsAction ?? (() => false); expect(describeMessageTool).toBeTypeOf("function"); expect(supportsAction).toBeTypeOf("function"); const discovery = describeMessageTool!({ cfg: createConfiguredMatrixConfig(), - } as never) ?? { actions: [] }; + } as never); + if (!discovery) { + throw new Error("describeMessageTool returned null"); + } const actions = discovery.actions; - expect(actions).toContain("poll"); expect(actions).toContain("poll-vote"); - expect(supportsAction!({ action: "poll" } as never)).toBe(false); - expect(supportsAction!({ action: "poll-vote" } as never)).toBe(true); + expect(supportsAction({ action: "poll" } as never)).toBe(false); + expect(supportsAction({ action: "poll-vote" } as never)).toBe(true); }); it("exposes and describes self-profile updates", () => { const describeMessageTool = matrixMessageActions.describeMessageTool; - const supportsAction = matrixMessageActions.supportsAction; + const supportsAction = matrixMessageActions.supportsAction ?? (() => false); const discovery = describeMessageTool!({ cfg: createConfiguredMatrixConfig(), - } as never) ?? { actions: [], schema: null }; + } as never); + if (!discovery) { + throw new Error("describeMessageTool returned null"); + } const actions = discovery.actions; - const properties = - (discovery.schema as { properties?: Record } | null)?.properties ?? {}; + const schema = discovery.schema; + if (!schema) { + throw new Error("matrix schema missing"); + } + const properties = (schema as { properties?: Record }).properties ?? {}; - expect(actions).toContain("set-profile"); - expect(supportsAction!({ action: "set-profile" } as never)).toBe(true); + expect(actions).toContain(profileAction); + expect(supportsAction({ action: profileAction } as never)).toBe(true); expect(properties.displayName).toBeDefined(); expect(properties.avatarUrl).toBeDefined(); expect(properties.avatarPath).toBeDefined(); }); 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 discovery = 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); + if (!discovery) { + throw new Error("describeMessageTool returned null"); + } + const actions = discovery.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 discovery = 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); + if (!discovery) { + throw new Error("describeMessageTool returned null"); + } + const actions = discovery.actions; expect(actions).toEqual([]); }); diff --git a/extensions/matrix/src/cli.test.ts b/extensions/matrix/src/cli.test.ts index 008fd46795d..da10215f435 100644 --- a/extensions/matrix/src/cli.test.ts +++ b/extensions/matrix/src/cli.test.ts @@ -20,6 +20,8 @@ const setMatrixSdkConsoleLoggingMock = vi.fn(); const setMatrixSdkLogModeMock = vi.fn(); const updateMatrixOwnProfileMock = vi.fn(); const verifyMatrixRecoveryKeyMock = vi.fn(); +const consoleLogMock = vi.fn(); +const consoleErrorMock = vi.fn(); vi.mock("./matrix/actions/verification.js", () => ({ bootstrapMatrixVerification: (...args: unknown[]) => bootstrapMatrixVerificationMock(...args), @@ -86,8 +88,12 @@ describe("matrix CLI verification commands", () => { beforeEach(() => { vi.clearAllMocks(); process.exitCode = undefined; - vi.spyOn(console, "log").mockImplementation(() => {}); - vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => consoleLogMock(...args)); + vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => + consoleErrorMock(...args), + ); + consoleLogMock.mockReset(); + consoleErrorMock.mockReset(); matrixSetupValidateInputMock.mockReturnValue(null); matrixSetupApplyAccountConfigMock.mockImplementation(({ cfg }: { cfg: unknown }) => cfg); matrixRuntimeLoadConfigMock.mockReturnValue({}); @@ -521,9 +527,7 @@ describe("matrix CLI verification commands", () => { expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled(); expect(process.exitCode).toBeUndefined(); - const jsonOutput = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.at( - -1, - )?.[0]; + const jsonOutput = consoleLogMock.mock.calls.at(-1)?.[0]; expect(typeof jsonOutput).toBe("string"); expect(JSON.parse(String(jsonOutput))).toEqual( expect.objectContaining({ diff --git a/extensions/matrix/src/matrix/client/file-sync-store.test.ts b/extensions/matrix/src/matrix/client/file-sync-store.test.ts index 632ec309210..56c88433d9c 100644 --- a/extensions/matrix/src/matrix/client/file-sync-store.test.ts +++ b/extensions/matrix/src/matrix/client/file-sync-store.test.ts @@ -12,7 +12,9 @@ function createSyncResponse(nextBatch: string): ISyncResponse { rooms: { join: { "!room:example.org": { - summary: { "m.heroes": [] }, + summary: { + "m.heroes": [], + }, state: { events: [] }, timeline: { events: [ diff --git a/extensions/matrix/src/matrix/client/file-sync-store.ts b/extensions/matrix/src/matrix/client/file-sync-store.ts index cbb71e09727..453e6b1bd38 100644 --- a/extensions/matrix/src/matrix/client/file-sync-store.ts +++ b/extensions/matrix/src/matrix/client/file-sync-store.ts @@ -1,9 +1,11 @@ import { readFileSync } from "node:fs"; import fs from "node:fs/promises"; import { + Category, MemoryStore, SyncAccumulator, type ISyncData, + type IRooms, type ISyncResponse, type IStoredClientOpts, } from "matrix-js-sdk"; @@ -41,31 +43,54 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +function normalizeRoomsData(value: unknown): IRooms | null { + if (!isRecord(value)) { + return null; + } + return { + [Category.Join]: isRecord(value[Category.Join]) ? (value[Category.Join] as IRooms["join"]) : {}, + [Category.Invite]: isRecord(value[Category.Invite]) + ? (value[Category.Invite] as IRooms["invite"]) + : {}, + [Category.Leave]: isRecord(value[Category.Leave]) + ? (value[Category.Leave] as IRooms["leave"]) + : {}, + [Category.Knock]: isRecord(value[Category.Knock]) + ? (value[Category.Knock] as IRooms["knock"]) + : {}, + }; +} + function toPersistedSyncData(value: unknown): ISyncData | null { if (!isRecord(value)) { return null; } if (typeof value.nextBatch === "string" && value.nextBatch.trim()) { - if (!Array.isArray(value.accountData) || !isRecord(value.roomsData)) { + const roomsData = normalizeRoomsData(value.roomsData); + if (!Array.isArray(value.accountData) || !roomsData) { return null; } return { nextBatch: value.nextBatch, accountData: value.accountData, - roomsData: value.roomsData, - } as unknown as ISyncData; + roomsData, + }; } // Older Matrix state files stored the raw /sync-shaped payload directly. if (typeof value.next_batch === "string" && value.next_batch.trim()) { + const roomsData = normalizeRoomsData(value.rooms); + if (!roomsData) { + return null; + } return { nextBatch: value.next_batch, accountData: isRecord(value.account_data) && Array.isArray(value.account_data.events) ? value.account_data.events : [], - roomsData: isRecord(value.rooms) ? value.rooms : {}, - } as unknown as ISyncData; + roomsData, + }; } return null; diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 5d4642bdb5e..bd4caa97fa7 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -516,7 +516,7 @@ describe("registerMatrixMonitorEvents verification routing", () => { await vi.waitFor(() => { expect(sendMessage).toHaveBeenCalledTimes(1); }); - const roomId = (sendMessage.mock.calls[0]?.[0] ?? "") as string; + const roomId = ((sendMessage.mock.calls as unknown[][])[0]?.[0] ?? "") as string; const body = getSentNoticeBody(sendMessage, 0); expect(roomId).toBe("!dm-active:example.org"); expect(body).toContain("SAS decimal: 4321 8765 2109"); diff --git a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts index a39b9efec06..7a04948a191 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts @@ -35,6 +35,7 @@ type MatrixHandlerTestHarnessOptions = { startupMs?: number; startupGraceMs?: number; dropPreStartupMessages?: boolean; + needsRoomAliasesForConfig?: boolean; isDirectMessage?: boolean; readAllowFromStore?: MatrixMonitorHandlerParams["core"]["channel"]["pairing"]["readAllowFromStore"]; upsertPairingRequest?: MatrixMonitorHandlerParams["core"]["channel"]["pairing"]["upsertPairingRequest"]; @@ -179,7 +180,7 @@ export function createMatrixHandlerTestHarness( }, getRoomInfo: options.getRoomInfo ?? (async () => ({ altAliases: [] })), getMemberDisplayName: options.getMemberDisplayName ?? (async () => "sender"), - needsRoomAliasesForConfig: false, + needsRoomAliasesForConfig: options.needsRoomAliasesForConfig ?? false, }); return { diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index e28afdff33d..fc55012a6b5 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -177,6 +177,8 @@ describe("matrix monitor handler pairing account scope", () => { dmPolicy: "pairing", isDirectMessage: true, getMemberDisplayName: async () => "sender", + dropPreStartupMessages: true, + needsRoomAliasesForConfig: false, }); await handler( diff --git a/extensions/matrix/src/matrix/monitor/index.test.ts b/extensions/matrix/src/matrix/monitor/index.test.ts index 34538ed5b80..7039968dd0b 100644 --- a/extensions/matrix/src/matrix/monitor/index.test.ts +++ b/extensions/matrix/src/matrix/monitor/index.test.ts @@ -2,6 +2,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => { const callOrder: string[] = []; + const state = { + startClientError: null as Error | null, + }; const client = { id: "matrix-client", hasPersistedSyncState: vi.fn(() => false), @@ -27,7 +30,7 @@ const hoisted = vi.hoisted(() => { releaseSharedClientInstance, resolveTextChunkLimit, setActiveMatrixClient, - startClientError: null as Error | null, + state, stopThreadBindingManager, }; }); @@ -121,8 +124,8 @@ vi.mock("../client.js", () => ({ if (!hoisted.callOrder.includes("create-manager")) { throw new Error("Matrix client started before thread bindings were registered"); } - if (hoisted.startClientError) { - throw hoisted.startClientError; + if (hoisted.state.startClientError) { + throw hoisted.state.startClientError; } hoisted.callOrder.push("start-client"); return hoisted.client; @@ -207,7 +210,7 @@ describe("monitorMatrixProvider", () => { beforeEach(() => { vi.resetModules(); hoisted.callOrder.length = 0; - hoisted.startClientError = null; + hoisted.state.startClientError = null; hoisted.resolveTextChunkLimit.mockReset().mockReturnValue(4000); hoisted.releaseSharedClientInstance.mockReset().mockResolvedValue(true); hoisted.setActiveMatrixClient.mockReset(); @@ -249,7 +252,7 @@ describe("monitorMatrixProvider", () => { it("cleans up thread bindings and shared clients when startup fails", async () => { const { monitorMatrixProvider } = await import("./index.js"); - hoisted.startClientError = new Error("start failed"); + hoisted.state.startClientError = new Error("start failed"); await expect(monitorMatrixProvider()).rejects.toThrow("start failed"); diff --git a/extensions/matrix/src/matrix/monitor/route.test.ts b/extensions/matrix/src/matrix/monitor/route.test.ts index 5846d45dd9c..f170db9080b 100644 --- a/extensions/matrix/src/matrix/monitor/route.test.ts +++ b/extensions/matrix/src/matrix/monitor/route.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { + __testing as sessionBindingTesting, createTestRegistry, - type OpenClawConfig, - resolveAgentRoute, registerSessionBindingAdapter, - sessionBindingTesting, + resolveAgentRoute, setActivePluginRegistry, -} from "../../../../../test/helpers/extensions/matrix-route-test.js"; + type OpenClawConfig, +} from "../../../../../test/helpers/extensions/matrix-monitor-route.js"; import { matrixPlugin } from "../../channel.js"; import { resolveMatrixInboundRoute } from "./route.js"; diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index e25d215af05..8975af5bdff 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -222,9 +222,8 @@ describe("MatrixClient request hardening", () => { it("prefers authenticated client media downloads", async () => { const payload = Buffer.from([1, 2, 3, 4]); - const fetchMock = vi.fn( - async (_input: RequestInfo | URL, _init?: RequestInit) => - new Response(payload, { status: 200 }), + const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>( + async () => new Response(payload, { status: 200 }), ); vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); @@ -232,7 +231,7 @@ describe("MatrixClient request hardening", () => { await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload); expect(fetchMock).toHaveBeenCalledTimes(1); - const firstUrl = String(fetchMock.mock.calls[0]?.[0]); + const firstUrl = String((fetchMock.mock.calls as unknown[][])[0]?.[0] ?? ""); expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media"); }); @@ -260,8 +259,8 @@ describe("MatrixClient request hardening", () => { await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload); expect(fetchMock).toHaveBeenCalledTimes(2); - const firstUrl = String(fetchMock.mock.calls[0]?.[0]); - const secondUrl = String(fetchMock.mock.calls[1]?.[0]); + const firstUrl = String((fetchMock.mock.calls as unknown[][])[0]?.[0] ?? ""); + const secondUrl = String((fetchMock.mock.calls as unknown[][])[1]?.[0] ?? ""); expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media"); expect(secondUrl).toContain("/_matrix/media/v3/download/example.org/media"); }); @@ -977,7 +976,7 @@ describe("MatrixClient crypto bootstrapping", () => { await client.start(); expect(bootstrapSpy).toHaveBeenCalledTimes(2); - expect(bootstrapSpy.mock.calls[1]?.[1]).toEqual({ + expect((bootstrapSpy.mock.calls as unknown[][])[1]?.[1] ?? {}).toEqual({ forceResetCrossSigning: true, strict: true, }); @@ -1025,7 +1024,7 @@ describe("MatrixClient crypto bootstrapping", () => { await client.start(); expect(bootstrapSpy).toHaveBeenCalledTimes(1); - expect(bootstrapSpy.mock.calls[0]?.[1]).toEqual({ + expect((bootstrapSpy.mock.calls as unknown[][])[0]?.[1] ?? {}).toEqual({ allowAutomaticCrossSigningReset: false, }); }); @@ -2061,12 +2060,12 @@ describe("MatrixClient crypto bootstrapping", () => { expect(result.success).toBe(true); expect(result.verification.backupVersion).toBe("9"); - const bootstrapSecretStorageCalls = bootstrapSecretStorage.mock.calls as Array< - [{ setupNewKeyBackup?: boolean }?] - >; - expect(bootstrapSecretStorageCalls.some((call) => Boolean(call[0]?.setupNewKeyBackup))).toBe( - false, - ); + const bootstrapSecretStorageCalls = bootstrapSecretStorage.mock.calls as Array; + expect( + bootstrapSecretStorageCalls.some((call) => + Boolean((call[0] as { setupNewKeyBackup?: boolean })?.setupNewKeyBackup), + ), + ).toBe(false); }); it("does not report bootstrap errors when final verification state is healthy", async () => { diff --git a/extensions/matrix/src/setup-surface.ts b/extensions/matrix/src/setup-surface.ts index ed601b90400..cd4ab580eb3 100644 --- a/extensions/matrix/src/setup-surface.ts +++ b/extensions/matrix/src/setup-surface.ts @@ -1 +1,4 @@ -export { matrixOnboardingAdapter } from "./onboarding.js"; +export { + matrixOnboardingAdapter, + matrixOnboardingAdapter as matrixSetupWizard, +} from "./onboarding.js"; diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 3b93bf0a826..0ca4dd2c903 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as acpSessionManager from "../acp/control-plane/manager.js"; +import type { + AcpCloseSessionInput, + AcpInitializeSessionInput, +} from "../acp/control-plane/manager.types.js"; import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot, @@ -180,16 +184,12 @@ describe("spawnAcpDirect", () => { metaCleared: false, }); getAcpSessionManagerSpy.mockReset().mockReturnValue({ - initializeSession: async (params: unknown) => await hoisted.initializeSessionMock(params), - closeSession: async (params: unknown) => await hoisted.closeSessionMock(params), + initializeSession: async (params: AcpInitializeSessionInput) => + await hoisted.initializeSessionMock(params), + closeSession: async (params: AcpCloseSessionInput) => await hoisted.closeSessionMock(params), } as unknown as ReturnType); hoisted.initializeSessionMock.mockReset().mockImplementation(async (argsUnknown: unknown) => { - const args = argsUnknown as { - sessionKey: string; - agent: string; - mode: "persistent" | "oneshot"; - cwd?: string; - }; + const args = argsUnknown as AcpInitializeSessionInput; const runtimeSessionName = `${args.sessionKey}:runtime`; const cwd = typeof args.cwd === "string" ? args.cwd : undefined; return { @@ -386,7 +386,6 @@ describe("spawnAcpDirect", () => { matrix: { threadBindings: { enabled: true, - spawnAcpSessions: true, }, }, }, diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index 280172dc073..265fda978e9 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -6,12 +6,14 @@ import { type OpenClawConfig, } from "../config/config.js"; import * as configSessions from "../config/sessions.js"; +import type { SessionEntry } from "../config/sessions/types.js"; import * as gatewayCall from "../gateway/call.js"; import { __testing as sessionBindingServiceTesting, registerSessionBindingAdapter, } from "../infra/outbound/session-binding-service.js"; import * as hookRunnerGlobal from "../plugins/hook-runner-global.js"; +import type { HookRunner } from "../plugins/hooks.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import * as piEmbedded from "./pi-embedded.js"; @@ -65,11 +67,23 @@ const waitForEmbeddedPiRunEndSpy = vi.spyOn(piEmbedded, "waitForEmbeddedPiRunEnd const readLatestAssistantReplyMock = vi.fn( async (_sessionKey?: string): Promise => "raw subagent reply", ); +const embeddedPiRunActiveMock = vi.fn( + (_sessionId: string) => false, +); +const embeddedPiRunStreamingMock = vi.fn( + (_sessionId: string) => false, +); +const queueEmbeddedPiMessageMock = vi.fn( + (_sessionId: string, _text: string) => false, +); +const waitForEmbeddedPiRunEndMock = vi.fn( + async (_sessionId: string, _timeoutMs?: number) => true, +); const embeddedRunMock = { - isEmbeddedPiRunActive: vi.fn(() => false), - isEmbeddedPiRunStreaming: vi.fn(() => false), - queueEmbeddedPiMessage: vi.fn((_: string, __: string) => false), - waitForEmbeddedPiRunEnd: vi.fn(async (_: string, __?: number) => true), + isEmbeddedPiRunActive: embeddedPiRunActiveMock, + isEmbeddedPiRunStreaming: embeddedPiRunStreamingMock, + queueEmbeddedPiMessage: queueEmbeddedPiMessageMock, + waitForEmbeddedPiRunEnd: waitForEmbeddedPiRunEndMock, }; const { subagentRegistryMock } = vi.hoisted(() => ({ subagentRegistryMock: { @@ -92,18 +106,21 @@ const subagentDeliveryTargetHookMock = vi.fn( undefined, ); let hasSubagentDeliveryTargetHook = false; +const hookHasHooksMock = vi.fn( + (hookName) => hookName === "subagent_delivery_target" && hasSubagentDeliveryTargetHook, +); +const hookRunSubagentDeliveryTargetMock = vi.fn( + async (event, ctx) => await subagentDeliveryTargetHookMock(event, ctx), +); const hookRunnerMock = { - hasHooks: vi.fn( - (hookName: string) => hookName === "subagent_delivery_target" && hasSubagentDeliveryTargetHook, - ), - runSubagentDeliveryTarget: vi.fn((event: unknown, ctx: unknown) => - subagentDeliveryTargetHookMock(event, ctx), - ), -}; + hasHooks: hookHasHooksMock, + runSubagentDeliveryTarget: hookRunSubagentDeliveryTargetMock, +} as unknown as HookRunner; const chatHistoryMock = vi.fn(async (_sessionKey?: string) => ({ messages: [] as Array, })); -let sessionStore: Record> = {}; +type TestSessionStore = Record>; +let sessionStore: TestSessionStore = {}; let configOverride: OpenClawConfig = { session: { mainKey: "main", @@ -131,19 +148,34 @@ function setConfigOverride(next: OpenClawConfig): void { setRuntimeConfigSnapshot(configOverride); } -function loadSessionStoreFixture(): ReturnType { - return new Proxy(sessionStore as ReturnType, { - get(target, key: string | symbol) { - if (typeof key === "string" && !(key in target) && key.includes(":subagent:")) { - return { - sessionId: key, - updatedAt: Date.now(), +function toSessionEntry( + sessionKey: string, + entry?: Partial, +): SessionEntry | undefined { + if (!entry) { + return undefined; + } + return { + sessionId: entry.sessionId ?? sessionKey, + updatedAt: entry.updatedAt ?? Date.now(), + ...entry, + }; +} + +function loadSessionStoreFixture(): Record { + return new Proxy({} as Record, { + get(_target, key: string | symbol) { + if (typeof key !== "string") { + return undefined; + } + if (!(key in sessionStore) && key.includes(":subagent:")) { + return toSessionEntry(key, { inputTokens: 1, outputTokens: 1, totalTokens: 2, - }; + }); } - return target[key as keyof typeof target]; + return toSessionEntry(key, sessionStore[key]); }, }); } @@ -223,17 +255,20 @@ describe("subagent announce formatting", () => { .mockImplementation(async (params) => await readLatestAssistantReplyMock(params?.sessionKey)); isEmbeddedPiRunActiveSpy .mockReset() - .mockImplementation(() => embeddedRunMock.isEmbeddedPiRunActive()); + .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedPiRunActive(sessionId)); isEmbeddedPiRunStreamingSpy .mockReset() - .mockImplementation(() => embeddedRunMock.isEmbeddedPiRunStreaming()); + .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedPiRunStreaming(sessionId)); queueEmbeddedPiMessageSpy .mockReset() - .mockImplementation((...args) => embeddedRunMock.queueEmbeddedPiMessage(...args)); + .mockImplementation((sessionId, text) => + embeddedRunMock.queueEmbeddedPiMessage(sessionId, text), + ); waitForEmbeddedPiRunEndSpy .mockReset() .mockImplementation( - async (...args) => await embeddedRunMock.waitForEmbeddedPiRunEnd(...args), + async (sessionId, timeoutMs) => + await embeddedRunMock.waitForEmbeddedPiRunEnd(sessionId, timeoutMs), ); embeddedRunMock.isEmbeddedPiRunActive.mockClear().mockReturnValue(false); embeddedRunMock.isEmbeddedPiRunStreaming.mockClear().mockReturnValue(false); @@ -258,8 +293,8 @@ describe("subagent announce formatting", () => { subagentRegistryMock.replaceSubagentRunAfterSteer.mockClear().mockReturnValue(true); subagentRegistryMock.resolveRequesterForChildSession.mockClear().mockReturnValue(null); hasSubagentDeliveryTargetHook = false; - hookRunnerMock.hasHooks.mockClear(); - hookRunnerMock.runSubagentDeliveryTarget.mockClear(); + hookHasHooksMock.mockClear(); + hookRunSubagentDeliveryTargetMock.mockClear(); subagentDeliveryTargetHookMock.mockReset().mockResolvedValue(undefined); readLatestAssistantReplyMock.mockClear().mockResolvedValue("raw subagent reply"); chatHistoryMock.mockReset().mockResolvedValue({ messages: [] }); diff --git a/src/channels/plugins/message-action-names.ts b/src/channels/plugins/message-action-names.ts index 3bf58083d14..4952ec03c2b 100644 --- a/src/channels/plugins/message-action-names.ts +++ b/src/channels/plugins/message-action-names.ts @@ -53,6 +53,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [ "ban", "set-profile", "set-presence", + "set-profile", "download-file", ] as const; diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index ddddae5ee71..a96fd8eaa85 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -350,15 +350,15 @@ export async function channelsAddCommand( await writeConfigFile(nextConfig); runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`); - const setup = plugin.setup; - if (setup?.afterAccountConfigWritten) { + const afterAccountConfigWritten = plugin.setup?.afterAccountConfigWritten; + if (afterAccountConfigWritten) { await runCollectedChannelOnboardingPostWriteHooks({ hooks: [ { channel, accountId, run: async ({ cfg: writtenCfg, runtime: hookRuntime }) => - await setup.afterAccountConfigWritten?.({ + await afterAccountConfigWritten({ previousCfg: cfg, cfg: writtenCfg, accountId, diff --git a/src/commands/channels/remove.ts b/src/commands/channels/remove.ts index 127dee5a3f9..d35cd285fc7 100644 --- a/src/commands/channels/remove.ts +++ b/src/commands/channels/remove.ts @@ -119,7 +119,6 @@ export async function channelsRemoveCommand( runtime.exit(1); return; } - const resolvedAccountId = normalizeAccountId(accountId) ?? resolveChannelDefaultAccountId({ plugin, cfg }); const accountKey = resolvedAccountId || DEFAULT_ACCOUNT_ID; @@ -164,14 +163,14 @@ export async function channelsRemoveCommand( if (useWizard && prompter) { await prompter.outro( deleteConfig - ? `Deleted ${channelLabel(channel)} account "${accountKey}".` - : `Disabled ${channelLabel(channel)} account "${accountKey}".`, + ? `Deleted ${channelLabel(resolvedChannel)} account "${accountKey}".` + : `Disabled ${channelLabel(resolvedChannel)} account "${accountKey}".`, ); } else { runtime.log( deleteConfig - ? `Deleted ${channelLabel(channel)} account "${accountKey}".` - : `Disabled ${channelLabel(channel)} account "${accountKey}".`, + ? `Deleted ${channelLabel(resolvedChannel)} account "${accountKey}".` + : `Disabled ${channelLabel(resolvedChannel)} account "${accountKey}".`, ); } } diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index e5605756e90..3c588f5a06e 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -13,8 +13,8 @@ import type { OpenClawPluginCommandDefinition, OpenClawPluginConfigSchema, OpenClawPluginDefinition, - PluginInteractiveTelegramHandlerContext, PluginCommandContext, + PluginInteractiveTelegramHandlerContext, } from "../plugins/types.js"; export type { diff --git a/src/plugin-sdk/setup.ts b/src/plugin-sdk/setup.ts index 3ebce5a8f47..6865c64e841 100644 --- a/src/plugin-sdk/setup.ts +++ b/src/plugin-sdk/setup.ts @@ -6,7 +6,10 @@ export type { SecretInput } from "../config/types.secrets.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; export type { ChannelSetupInput } from "../channels/plugins/types.core.js"; -export type { ChannelSetupDmPolicy } from "../channels/plugins/setup-wizard-types.js"; +export type { + ChannelSetupDmPolicy, + ChannelSetupWizardAdapter, +} from "../channels/plugins/setup-wizard-types.js"; export type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry, diff --git a/test/helpers/extensions/matrix-monitor-route.ts b/test/helpers/extensions/matrix-monitor-route.ts new file mode 100644 index 00000000000..1668a7e441a --- /dev/null +++ b/test/helpers/extensions/matrix-monitor-route.ts @@ -0,0 +1,8 @@ +export type { OpenClawConfig } from "../../../src/config/config.js"; +export { + __testing, + registerSessionBindingAdapter, +} from "../../../src/infra/outbound/session-binding-service.js"; +export { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; +export { resolveAgentRoute } from "../../../src/routing/resolve-route.js"; +export { createTestRegistry } from "../../../src/test-utils/channel-plugins.js";