fix(ci): repair main type and boundary regressions

This commit is contained in:
Vincent Koc
2026-03-19 07:50:02 -07:00
parent 0c4fdf1284
commit 44cd4fb55f
20 changed files with 246 additions and 144 deletions

View File

@@ -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>,
): 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",

View File

@@ -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<string, unknown> } | null)?.properties ?? {};
const schema = discovery.schema;
if (!schema) {
throw new Error("matrix schema missing");
}
const properties = (schema as { properties?: Record<string, unknown> }).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([]);
});

View File

@@ -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({

View File

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

View File

@@ -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<string, unknown> {
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;

View File

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

View File

@@ -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 {

View File

@@ -177,6 +177,8 @@ describe("matrix monitor handler pairing account scope", () => {
dmPolicy: "pairing",
isDirectMessage: true,
getMemberDisplayName: async () => "sender",
dropPreStartupMessages: true,
needsRoomAliasesForConfig: false,
});
await handler(

View File

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

View File

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

View File

@@ -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<Response>>(
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<unknown[]>;
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 () => {

View File

@@ -1 +1,4 @@
export { matrixOnboardingAdapter } from "./onboarding.js";
export {
matrixOnboardingAdapter,
matrixOnboardingAdapter as matrixSetupWizard,
} from "./onboarding.js";

View File

@@ -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<typeof acpSessionManager.getAcpSessionManager>);
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,
},
},
},

View File

@@ -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<string | undefined> => "raw subagent reply",
);
const embeddedPiRunActiveMock = vi.fn<typeof piEmbedded.isEmbeddedPiRunActive>(
(_sessionId: string) => false,
);
const embeddedPiRunStreamingMock = vi.fn<typeof piEmbedded.isEmbeddedPiRunStreaming>(
(_sessionId: string) => false,
);
const queueEmbeddedPiMessageMock = vi.fn<typeof piEmbedded.queueEmbeddedPiMessage>(
(_sessionId: string, _text: string) => false,
);
const waitForEmbeddedPiRunEndMock = vi.fn<typeof piEmbedded.waitForEmbeddedPiRunEnd>(
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<HookRunner["hasHooks"]>(
(hookName) => hookName === "subagent_delivery_target" && hasSubagentDeliveryTargetHook,
);
const hookRunSubagentDeliveryTargetMock = vi.fn<HookRunner["runSubagentDeliveryTarget"]>(
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<unknown>,
}));
let sessionStore: Record<string, Record<string, unknown>> = {};
type TestSessionStore = Record<string, Partial<SessionEntry>>;
let sessionStore: TestSessionStore = {};
let configOverride: OpenClawConfig = {
session: {
mainKey: "main",
@@ -131,19 +148,34 @@ function setConfigOverride(next: OpenClawConfig): void {
setRuntimeConfigSnapshot(configOverride);
}
function loadSessionStoreFixture(): ReturnType<typeof configSessions.loadSessionStore> {
return new Proxy(sessionStore as ReturnType<typeof configSessions.loadSessionStore>, {
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>,
): SessionEntry | undefined {
if (!entry) {
return undefined;
}
return {
sessionId: entry.sessionId ?? sessionKey,
updatedAt: entry.updatedAt ?? Date.now(),
...entry,
};
}
function loadSessionStoreFixture(): Record<string, SessionEntry> {
return new Proxy({} as Record<string, SessionEntry>, {
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: [] });

View File

@@ -53,6 +53,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
"ban",
"set-profile",
"set-presence",
"set-profile",
"download-file",
] as const;

View File

@@ -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,

View File

@@ -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}".`,
);
}
}

View File

@@ -13,8 +13,8 @@ import type {
OpenClawPluginCommandDefinition,
OpenClawPluginConfigSchema,
OpenClawPluginDefinition,
PluginInteractiveTelegramHandlerContext,
PluginCommandContext,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
export type {

View File

@@ -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,

View File

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