mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:20:42 +00:00
Merged via squash.
Prepared head SHA: ec32828b52
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
460 lines
14 KiB
TypeScript
460 lines
14 KiB
TypeScript
import type { ChannelSetupWizardAdapter } from "openclaw/plugin-sdk/setup";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js";
|
|
import type { CoreConfig } from "./types.js";
|
|
|
|
function applyOpsAccountConfig(cfg: CoreConfig): CoreConfig {
|
|
return matrixSetupAdapter.applyAccountConfig({
|
|
cfg,
|
|
accountId: "ops",
|
|
input: {
|
|
name: "Ops",
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
},
|
|
}) as CoreConfig;
|
|
}
|
|
|
|
function expectPromotedDefaultAccount(next: CoreConfig): void {
|
|
expect(next.channels?.matrix?.accounts?.Default).toMatchObject({
|
|
enabled: true,
|
|
deviceName: "Legacy raw key",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
avatarUrl: "mxc://example.org/default-avatar",
|
|
});
|
|
expect(next.channels?.matrix?.accounts?.default).toBeUndefined();
|
|
}
|
|
|
|
function expectOpsAccount(next: CoreConfig): void {
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
name: "Ops",
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
});
|
|
}
|
|
|
|
function makeFakeSetupWizard(
|
|
overrides: Partial<ChannelSetupWizardAdapter> = {},
|
|
): ChannelSetupWizardAdapter {
|
|
return {
|
|
channel: "matrix",
|
|
getStatus: vi.fn(async () => ({
|
|
channel: "matrix",
|
|
configured: false,
|
|
statusLines: [],
|
|
})),
|
|
configure: vi.fn(async ({ cfg }) => ({ cfg })),
|
|
...overrides,
|
|
} as ChannelSetupWizardAdapter;
|
|
}
|
|
|
|
describe("createMatrixSetupWizardProxy", () => {
|
|
it("does not load the setup surface when constructing the proxy", () => {
|
|
const loader = vi.fn(async () => ({ matrixSetupWizard: makeFakeSetupWizard() }));
|
|
|
|
const proxy = createMatrixSetupWizardProxy(loader);
|
|
|
|
expect(proxy.channel).toBe("matrix");
|
|
expect(loader).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("loads the setup surface when setup status is requested", async () => {
|
|
const status = {
|
|
channel: "matrix" as const,
|
|
configured: true,
|
|
statusLines: ["Matrix: configured"],
|
|
};
|
|
const getStatus = vi.fn(async () => status);
|
|
const configure = vi.fn(async ({ cfg }) => ({ cfg }));
|
|
const loader = vi.fn(async () => ({
|
|
matrixSetupWizard: makeFakeSetupWizard({ configure, getStatus }),
|
|
}));
|
|
const proxy = createMatrixSetupWizardProxy(loader);
|
|
const cfg = { channels: { matrix: {} } } as CoreConfig;
|
|
|
|
const result = await proxy.getStatus({ cfg, accountOverrides: {} });
|
|
const configured = await proxy.configure({
|
|
cfg,
|
|
runtime: {} as never,
|
|
prompter: {} as never,
|
|
forceAllowFrom: false,
|
|
accountOverrides: {},
|
|
shouldPromptAccountIds: false,
|
|
});
|
|
|
|
expect(loader).toHaveBeenCalledTimes(1);
|
|
expect(getStatus).toHaveBeenCalledWith({ cfg, accountOverrides: {} });
|
|
expect(configure).toHaveBeenCalledTimes(1);
|
|
expect(result).toBe(status);
|
|
expect(configured).toEqual({ cfg });
|
|
});
|
|
|
|
it("keeps sync dmPolicy helpers local and lazy-loads only promptAllowFrom", async () => {
|
|
const promptAllowFrom = vi.fn(async ({ cfg }) => cfg);
|
|
const loader = vi.fn(async () => ({
|
|
matrixSetupWizard: makeFakeSetupWizard({
|
|
dmPolicy: {
|
|
label: "Matrix",
|
|
channel: "matrix",
|
|
policyKey: "unused",
|
|
allowFromKey: "unused",
|
|
getCurrent: () => "pairing",
|
|
setPolicy: (cfg) => cfg,
|
|
promptAllowFrom,
|
|
},
|
|
}),
|
|
}));
|
|
const proxy = createMatrixSetupWizardProxy(loader);
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: {
|
|
dm: {
|
|
allowFrom: [" @ops:example.org ", "", "*"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
expect(proxy.dmPolicy?.getCurrent(cfg, "ops")).toBe("pairing");
|
|
const next = proxy.dmPolicy?.setPolicy(cfg, "open", "ops") as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops?.dm).toMatchObject({
|
|
policy: "open",
|
|
allowFrom: ["@ops:example.org", "*"],
|
|
});
|
|
expect(loader).not.toHaveBeenCalled();
|
|
|
|
await proxy.dmPolicy?.promptAllowFrom?.({
|
|
cfg,
|
|
prompter: {} as never,
|
|
});
|
|
|
|
expect(loader).toHaveBeenCalledTimes(1);
|
|
expect(promptAllowFrom).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("removes wildcard allowFrom when switching from open to a restrictive policy", () => {
|
|
const loader = vi.fn(async () => ({ matrixSetupWizard: makeFakeSetupWizard() }));
|
|
const proxy = createMatrixSetupWizardProxy(loader);
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: {
|
|
dm: {
|
|
policy: "open",
|
|
allowFrom: ["*", " @ops:example.org "],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = proxy.dmPolicy?.setPolicy(cfg, "allowlist", "ops") as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops?.dm).toMatchObject({
|
|
policy: "allowlist",
|
|
allowFrom: ["@ops:example.org"],
|
|
});
|
|
expect(loader).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("matrixSetupAdapter", () => {
|
|
it("moves legacy default config before writing a named account", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
deviceName: "Default device",
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg,
|
|
accountId: "ops",
|
|
input: {
|
|
name: "Ops",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@ops:example.org",
|
|
accessToken: "ops-token",
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.homeserver).toBeUndefined();
|
|
expect(next.channels?.matrix?.userId).toBeUndefined();
|
|
expect(next.channels?.matrix?.accessToken).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.default).toMatchObject({
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
deviceName: "Default device",
|
|
});
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
name: "Ops",
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@ops:example.org",
|
|
accessToken: "ops-token",
|
|
});
|
|
});
|
|
|
|
it("reuses an existing raw default-account key during promotion", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
defaultAccount: "default",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
avatarUrl: "mxc://example.org/default-avatar",
|
|
accounts: {
|
|
Default: {
|
|
enabled: true,
|
|
deviceName: "Legacy raw key",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = applyOpsAccountConfig(cfg);
|
|
|
|
expectPromotedDefaultAccount(next);
|
|
expectOpsAccount(next);
|
|
});
|
|
|
|
it("reuses an existing raw default-like key during promotion when defaultAccount is unset", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
avatarUrl: "mxc://example.org/default-avatar",
|
|
accounts: {
|
|
Default: {
|
|
enabled: true,
|
|
deviceName: "Legacy raw key",
|
|
},
|
|
support: {
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "support-token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = applyOpsAccountConfig(cfg);
|
|
|
|
expectPromotedDefaultAccount(next);
|
|
expect(next.channels?.matrix?.accounts?.support).toMatchObject({
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "support-token",
|
|
});
|
|
expectOpsAccount(next);
|
|
});
|
|
|
|
it("clears stored auth fields when switching an account to env-backed auth", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: {
|
|
name: "Ops",
|
|
homeserver: "https://matrix.example.org",
|
|
proxy: "http://127.0.0.1:7890",
|
|
userId: "@ops:example.org",
|
|
accessToken: "ops-token",
|
|
password: "secret",
|
|
deviceId: "DEVICE",
|
|
deviceName: "Ops device",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg,
|
|
accountId: "ops",
|
|
input: {
|
|
name: "Ops",
|
|
useEnv: true,
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
name: "Ops",
|
|
enabled: true,
|
|
});
|
|
expect(next.channels?.matrix?.accounts?.ops?.homeserver).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.proxy).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.userId).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.accessToken).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.password).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.deviceId).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.deviceName).toBeUndefined();
|
|
});
|
|
|
|
it("keeps avatarUrl when switching an account to env-backed auth", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: {
|
|
name: "Ops",
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg,
|
|
accountId: "ops",
|
|
input: {
|
|
name: "Ops",
|
|
useEnv: true,
|
|
avatarUrl: " mxc://example.org/ops-avatar ",
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
name: "Ops",
|
|
enabled: true,
|
|
avatarUrl: "mxc://example.org/ops-avatar",
|
|
});
|
|
expect(next.channels?.matrix?.accounts?.ops?.homeserver).toBeUndefined();
|
|
expect(next.channels?.matrix?.accounts?.ops?.accessToken).toBeUndefined();
|
|
});
|
|
|
|
it("stores proxy in account setup updates", () => {
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg: {} as CoreConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
proxy: "http://127.0.0.1:7890",
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
proxy: "http://127.0.0.1:7890",
|
|
});
|
|
});
|
|
|
|
it("stores avatarUrl from setup input on the target account", () => {
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg: {} as CoreConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
avatarUrl: " mxc://example.org/ops-avatar ",
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
avatarUrl: "mxc://example.org/ops-avatar",
|
|
});
|
|
});
|
|
|
|
it("rejects unsupported avatar URL schemes during setup validation", () => {
|
|
const validationError = matrixSetupAdapter.validateInput?.({
|
|
cfg: {} as CoreConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
avatarUrl: "file:///tmp/avatar.png",
|
|
},
|
|
});
|
|
|
|
expect(validationError).toBe("Matrix avatar URL must be an mxc:// URI or an http(s) URL.");
|
|
});
|
|
|
|
it("stores canonical dangerous private-network opt-in from setup input", () => {
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg: {} as CoreConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
homeserver: "http://matrix.internal:8008",
|
|
accessToken: "ops-token",
|
|
dangerouslyAllowPrivateNetwork: true,
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
enabled: true,
|
|
homeserver: "http://matrix.internal:8008",
|
|
accessToken: "ops-token",
|
|
network: {
|
|
dangerouslyAllowPrivateNetwork: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps top-level block streaming as a shared default when named accounts already exist", () => {
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@default:example.org",
|
|
accessToken: "default-token",
|
|
blockStreaming: true,
|
|
accounts: {
|
|
support: {
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@support:example.org",
|
|
accessToken: "support-token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as CoreConfig;
|
|
|
|
const next = matrixSetupAdapter.applyAccountConfig({
|
|
cfg,
|
|
accountId: "ops",
|
|
input: {
|
|
name: "Ops",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@ops:example.org",
|
|
accessToken: "ops-token",
|
|
},
|
|
}) as CoreConfig;
|
|
|
|
expect(next.channels?.matrix?.blockStreaming).toBe(true);
|
|
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
|
name: "Ops",
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@ops:example.org",
|
|
accessToken: "ops-token",
|
|
});
|
|
expect(next.channels?.matrix?.accounts?.ops?.blockStreaming).toBeUndefined();
|
|
});
|
|
});
|