mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 16:38:11 +00:00
Matrix-js: add legacy config migration and bind integration coverage
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { matrixPlugin } from "./channel.js";
|
||||
import { migrateMatrixLegacyCredentialsToDefaultAccount } from "./config-migration.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
@@ -153,7 +154,11 @@ describe("matrix directory", () => {
|
||||
},
|
||||
}) as CoreConfig;
|
||||
|
||||
expect(updated.channels?.["matrix-js"]?.accessToken).toBe("default-token");
|
||||
expect(updated.channels?.["matrix-js"]?.accessToken).toBeUndefined();
|
||||
expect(updated.channels?.["matrix-js"]?.accounts?.default).toMatchObject({
|
||||
accessToken: "default-token",
|
||||
homeserver: "https://default.example.org",
|
||||
});
|
||||
expect(updated.channels?.["matrix-js"]?.accounts?.ops).toMatchObject({
|
||||
enabled: true,
|
||||
homeserver: "https://matrix.example.org",
|
||||
@@ -182,7 +187,7 @@ describe("matrix directory", () => {
|
||||
},
|
||||
}) as CoreConfig;
|
||||
|
||||
expect(updated.channels?.["matrix-js"]?.homeserver).toBe("https://legacy.example.org");
|
||||
expect(updated.channels?.["matrix-js"]?.homeserver).toBeUndefined();
|
||||
expect(updated.channels?.["matrix-js"]?.accounts?.default).toMatchObject({
|
||||
enabled: true,
|
||||
homeserver: "https://matrix.example.org",
|
||||
@@ -191,6 +196,32 @@ describe("matrix directory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates legacy top-level matrix-js credentials into accounts.default", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
homeserver: "https://legacy.example.org",
|
||||
userId: "@legacy:example.org",
|
||||
accessToken: "legacy-token",
|
||||
deviceName: "Legacy Device",
|
||||
encryption: true,
|
||||
},
|
||||
},
|
||||
} as unknown as CoreConfig;
|
||||
|
||||
const updated = migrateMatrixLegacyCredentialsToDefaultAccount(cfg);
|
||||
expect(updated.channels?.["matrix-js"]?.homeserver).toBeUndefined();
|
||||
expect(updated.channels?.["matrix-js"]?.accessToken).toBeUndefined();
|
||||
expect(updated.channels?.["matrix-js"]?.deviceName).toBeUndefined();
|
||||
expect(updated.channels?.["matrix-js"]?.encryption).toBe(true);
|
||||
expect(updated.channels?.["matrix-js"]?.accounts?.default).toMatchObject({
|
||||
homeserver: "https://legacy.example.org",
|
||||
userId: "@legacy:example.org",
|
||||
accessToken: "legacy-token",
|
||||
deviceName: "Legacy Device",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects useEnv for non-default matrix-js accounts", () => {
|
||||
const error = matrixPlugin.setup!.validateInput?.({
|
||||
cfg: {} as CoreConfig,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type ChannelPlugin,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import { matrixMessageActions } from "./actions.js";
|
||||
import { migrateMatrixLegacyCredentialsToDefaultAccount } from "./config-migration.js";
|
||||
import { MatrixConfigSchema } from "./config-schema.js";
|
||||
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
|
||||
import {
|
||||
@@ -357,8 +358,9 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const migratedConfig = migrateMatrixLegacyCredentialsToDefaultAccount(cfg as CoreConfig);
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg as CoreConfig,
|
||||
cfg: migratedConfig,
|
||||
channelKey: "matrix-js",
|
||||
accountId,
|
||||
name: input.name,
|
||||
|
||||
70
extensions/matrix-js/src/config-migration.ts
Normal file
70
extensions/matrix-js/src/config-migration.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
||||
import type { CoreConfig, MatrixAccountConfig, MatrixConfig } from "./types.js";
|
||||
|
||||
type LegacyAccountField =
|
||||
| "homeserver"
|
||||
| "userId"
|
||||
| "accessToken"
|
||||
| "password"
|
||||
| "deviceId"
|
||||
| "deviceName"
|
||||
| "initialSyncLimit";
|
||||
|
||||
const LEGACY_ACCOUNT_FIELDS: ReadonlyArray<LegacyAccountField> = [
|
||||
"homeserver",
|
||||
"userId",
|
||||
"accessToken",
|
||||
"password",
|
||||
"deviceId",
|
||||
"deviceName",
|
||||
"initialSyncLimit",
|
||||
];
|
||||
|
||||
export function migrateMatrixLegacyCredentialsToDefaultAccount(cfg: CoreConfig): CoreConfig {
|
||||
const matrix = cfg.channels?.["matrix-js"];
|
||||
if (!matrix) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const defaultAccount = {
|
||||
...(matrix.accounts?.[DEFAULT_ACCOUNT_ID] ?? {}),
|
||||
} as MatrixAccountConfig;
|
||||
let changed = false;
|
||||
|
||||
for (const field of LEGACY_ACCOUNT_FIELDS) {
|
||||
const legacyValue = matrix[field] as MatrixAccountConfig[LegacyAccountField] | undefined;
|
||||
if (legacyValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (defaultAccount[field] === undefined) {
|
||||
(
|
||||
defaultAccount as Record<
|
||||
LegacyAccountField,
|
||||
MatrixAccountConfig[LegacyAccountField] | undefined
|
||||
>
|
||||
)[field] = legacyValue;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const nextMatrix = { ...matrix } as MatrixConfig;
|
||||
for (const field of LEGACY_ACCOUNT_FIELDS) {
|
||||
delete nextMatrix[field];
|
||||
}
|
||||
nextMatrix.accounts = {
|
||||
...matrix.accounts,
|
||||
[DEFAULT_ACCOUNT_ID]: defaultAccount,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
"matrix-js": nextMatrix,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -189,6 +189,15 @@ describe("registerAgentCommands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("documents bind accountId resolution behavior in help text", () => {
|
||||
const program = new Command();
|
||||
registerAgentCommands(program, { agentChannelOptions: "last|telegram|discord" });
|
||||
const agents = program.commands.find((command) => command.name() === "agents");
|
||||
const bind = agents?.commands.find((command) => command.name() === "bind");
|
||||
const help = bind?.helpInformation() ?? "";
|
||||
expect(help).toContain("accountId is resolved by channel defaults/hooks");
|
||||
});
|
||||
|
||||
it("forwards agents unbind options", async () => {
|
||||
await runCli(["agents", "unbind", "--agent", "ops", "--all", "--json"]);
|
||||
expect(agentsUnbindCommandMock).toHaveBeenCalledWith(
|
||||
|
||||
@@ -126,7 +126,12 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age
|
||||
.command("bind")
|
||||
.description("Add routing bindings for an agent")
|
||||
.option("--agent <id>", "Agent id (defaults to current default agent)")
|
||||
.option("--bind <channel[:accountId]>", "Binding to add (repeatable)", collectOption, [])
|
||||
.option(
|
||||
"--bind <channel[:accountId]>",
|
||||
"Binding to add (repeatable). If omitted, accountId is resolved by channel defaults/hooks.",
|
||||
collectOption,
|
||||
[],
|
||||
)
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
|
||||
52
src/commands/agents.bind.matrix-js.integration.test.ts
Normal file
52
src/commands/agents.bind.matrix-js.integration.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { matrixPlugin } from "../../extensions/matrix-js/src/channel.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { agentsBindCommand } from "./agents.js";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "./channel-test-helpers.js";
|
||||
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
|
||||
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const writeConfigFileMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../config/config.js")>()),
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
writeConfigFile: writeConfigFileMock,
|
||||
}));
|
||||
|
||||
describe("agents bind matrix-js integration", () => {
|
||||
const runtime = createTestRuntime();
|
||||
|
||||
beforeEach(() => {
|
||||
readConfigFileSnapshotMock.mockClear();
|
||||
writeConfigFileMock.mockClear();
|
||||
runtime.log.mockClear();
|
||||
runtime.error.mockClear();
|
||||
runtime.exit.mockClear();
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "matrix-js", plugin: matrixPlugin, source: "test" }]),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
});
|
||||
|
||||
it("uses matrix-js plugin binding resolver when accountId is omitted", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {},
|
||||
});
|
||||
|
||||
await agentsBindCommand({ agent: "main", bind: ["matrix-js"] }, runtime);
|
||||
|
||||
expect(writeConfigFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bindings: [{ agentId: "main", match: { channel: "matrix-js", accountId: "main" } }],
|
||||
}),
|
||||
);
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user