mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 22:40:58 +00:00
fix(ci): repair main type and boundary regressions
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -12,7 +12,9 @@ function createSyncResponse(nextBatch: string): ISyncResponse {
|
||||
rooms: {
|
||||
join: {
|
||||
"!room:example.org": {
|
||||
summary: { "m.heroes": [] },
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
},
|
||||
state: { events: [] },
|
||||
timeline: {
|
||||
events: [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -177,6 +177,8 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
dmPolicy: "pairing",
|
||||
isDirectMessage: true,
|
||||
getMemberDisplayName: async () => "sender",
|
||||
dropPreStartupMessages: true,
|
||||
needsRoomAliasesForConfig: false,
|
||||
});
|
||||
|
||||
await handler(
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
export {
|
||||
matrixOnboardingAdapter,
|
||||
matrixOnboardingAdapter as matrixSetupWizard,
|
||||
} from "./onboarding.js";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
@@ -53,6 +53,7 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
||||
"ban",
|
||||
"set-profile",
|
||||
"set-presence",
|
||||
"set-profile",
|
||||
"download-file",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import type {
|
||||
OpenClawPluginCommandDefinition,
|
||||
OpenClawPluginConfigSchema,
|
||||
OpenClawPluginDefinition,
|
||||
PluginInteractiveTelegramHandlerContext,
|
||||
PluginCommandContext,
|
||||
PluginInteractiveTelegramHandlerContext,
|
||||
} from "../plugins/types.js";
|
||||
|
||||
export type {
|
||||
|
||||
@@ -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,
|
||||
|
||||
8
test/helpers/extensions/matrix-monitor-route.ts
Normal file
8
test/helpers/extensions/matrix-monitor-route.ts
Normal 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";
|
||||
Reference in New Issue
Block a user