Files
openclaw/extensions/clickclack/src/setup-core.test.ts
Peter Steinberger 9e4381eb1c refactor(channels): unify bundled channel setup contracts (#117106)
* refactor(channels): canonicalize bundled setup contracts

* test(matrix): use scoped environment fixtures
2026-07-31 16:57:29 -07:00

647 lines
19 KiB
TypeScript

// ClickClack tests cover non-interactive setup validation and config writes.
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
const claimClickClackSetupCode = vi.hoisted(() => vi.fn());
const verifyClickClackAccountAfterSetup = vi.hoisted(() => vi.fn());
vi.mock("./setup-claim.js", async (importOriginal) => ({
...(await importOriginal<typeof import("./setup-claim.js")>()),
claimClickClackSetupCode,
}));
vi.mock("./setup-verify.js", () => ({
verifyClickClackAccountAfterSetup,
}));
import {
applyClickClackCredentialConfig,
clickClackSetupContract,
normalizeClickClackBaseUrl,
} from "./setup-core.js";
type ClickClackSetupInput = ChannelSetupInput & {
baseUrl?: string;
code?: string;
workspace?: string;
agentActivity?: boolean;
};
// Structural stand-in for the internal claim error: the setup formatter
// duck-types on a numeric `status`, so tests need only that shape.
function makeClaimError(status: number, detail: string): Error {
return Object.assign(new Error(`claim failed (${status}): ${detail}`), { status });
}
function validate(params: {
cfg?: OpenClawConfig;
accountId?: string;
input: ClickClackSetupInput;
}) {
return clickClackSetupContract.validateInput?.({
cfg: params.cfg ?? {},
accountId: params.accountId ?? DEFAULT_ACCOUNT_ID,
input: params.input,
});
}
async function prepare(input: ClickClackSetupInput, cfg: OpenClawConfig = {}) {
return await clickClackSetupContract.prepareAccountConfigInput?.({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
input,
runtime: createNonExitingRuntimeEnv(),
});
}
describe("ClickClack setup adapter", () => {
beforeEach(() => {
claimClickClackSetupCode.mockReset();
});
it("normalizes http(s) base URLs and rejects other schemes", () => {
expect(normalizeClickClackBaseUrl(" https://clickclack.example.com/// ")).toBe(
"https://clickclack.example.com",
);
expect(normalizeClickClackBaseUrl("http://localhost:3000/")).toBe("http://localhost:3000");
expect(normalizeClickClackBaseUrl("ssh://clickclack.example.com")).toBeUndefined();
expect(normalizeClickClackBaseUrl("not-a-url")).toBeUndefined();
});
it("claims a full setup URL and prepares the token, workspace, and defaults", async () => {
claimClickClackSetupCode.mockResolvedValue({
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {
defaultTo: "channel:general",
allowFrom: ["*"],
agentActivity: true,
},
});
await expect(
prepare({
code: "https://clickclack.example/#abcd-efgh-jkmn",
name: "Primary",
}),
).resolves.toEqual({
name: "Primary",
baseUrl: "https://clickclack.example",
token: "test-token",
workspace: "wsp_1",
defaultTo: "channel:general",
allowFrom: ["*"],
agentActivity: true,
});
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: "https://clickclack.example/api/bot-setup-codes/claim",
code: "ABCDEFGHJKMN",
});
});
it("claims an exact v1 endpoint and keeps the returned API base path", async () => {
claimClickClackSetupCode.mockResolvedValue({
contract_version: 1,
api_base_url: "https://api.clickclack.example/services/clickclack",
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {},
});
const exactClaimUrl =
"https://api.clickclack.example/services/clickclack/api/bot-setup-codes/claim";
await expect(
prepare({
code: `${exactClaimUrl}#abcd-efgh-jkmn`,
}),
).resolves.toMatchObject({
baseUrl: "https://api.clickclack.example/services/clickclack",
token: "test-token",
workspace: "wsp_1",
});
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: exactClaimUrl,
expectedClaimUrl: exactClaimUrl,
code: "ABCDEFGHJKMN",
});
});
it("claims a bare setup code with an explicit HTTPS base URL", async () => {
claimClickClackSetupCode.mockResolvedValue({
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {},
});
await expect(
prepare({
code: "abcd efgh jkmn",
baseUrl: "https://clickclack.example/",
}),
).resolves.toMatchObject({
baseUrl: "https://clickclack.example",
token: "test-token",
workspace: "wsp_1",
});
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: "https://clickclack.example/api/bot-setup-codes/claim",
code: "ABCDEFGHJKMN",
});
});
it("claims setup codes through an existing private API base", async () => {
claimClickClackSetupCode.mockResolvedValue({
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {},
});
await prepare(
{
code: "ABCD-EFGH-JKMN",
baseUrl: "https://clack.openclaw.ai",
},
{
channels: {
clickclack: {
apiBaseUrl: "http://127.0.0.1:8484",
},
},
} as OpenClawConfig,
);
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: "http://127.0.0.1:8484/api/bot-setup-codes/claim",
code: "ABCDEFGHJKMN",
});
});
it("uses a private API transport while validating the public exact endpoint", async () => {
claimClickClackSetupCode.mockResolvedValue({
contract_version: 1,
api_base_url: "https://api.clickclack.example/services/clickclack",
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {},
});
const exactClaimUrl =
"https://api.clickclack.example/services/clickclack/api/bot-setup-codes/claim";
await expect(
prepare({ code: `${exactClaimUrl}#ABCD-EFGH-JKMN` }, {
channels: {
clickclack: {
apiBaseUrl: "http://127.0.0.1:8484",
},
},
} as OpenClawConfig),
).resolves.toMatchObject({
baseUrl: "https://api.clickclack.example/services/clickclack",
});
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: "http://127.0.0.1:8484/api/bot-setup-codes/claim",
expectedClaimUrl: exactClaimUrl,
code: "ABCDEFGHJKMN",
});
});
it("accepts setup-code URLs for local HTTP installations", async () => {
claimClickClackSetupCode.mockResolvedValue({
token: "test-token",
bot: { id: "usr_bot", handle: "openclaw", display_name: "OpenClaw" },
workspace: {
id: "wsp_1",
route_id: "clickclack",
slug: "default",
name: "ClickClack",
},
defaults: {},
});
await expect(
prepare({
code: "http://localhost:3000/#abcd-efgh-jkmn",
}),
).resolves.toMatchObject({
baseUrl: "http://localhost:3000",
token: "test-token",
workspace: "wsp_1",
});
expect(claimClickClackSetupCode).toHaveBeenCalledWith({
claimUrl: "http://localhost:3000/api/bot-setup-codes/claim",
code: "ABCDEFGHJKMN",
});
});
it("rejects conflicting credentials before claiming a setup code", async () => {
for (const input of [
{ code: "ABCD-EFGH-JKMN", baseUrl: "https://clickclack.example", token: "test-token" },
{
code: "ABCD-EFGH-JKMN",
baseUrl: "https://clickclack.example",
tokenFile: "test-token-file",
},
{ code: "ABCD-EFGH-JKMN", baseUrl: "https://clickclack.example", useEnv: true },
]) {
await expect(prepare(input)).rejects.toThrow(
"ClickClack --code cannot be combined with --token, --token-file, or --use-env.",
);
}
expect(claimClickClackSetupCode).not.toHaveBeenCalled();
});
it("rejects mismatched and malformed setup-code inputs before claiming", async () => {
await expect(prepare({ code: "ftp://clickclack.example/#ABCD-EFGH-JKMN" })).rejects.toThrow(
"HTTP(S)",
);
await expect(
prepare({
code: "https://clickclack.example/#ABCD-EFGH-JKMN",
baseUrl: "https://other.example",
}),
).rejects.toThrow("does not match");
await expect(
prepare({ code: "not-a-code", baseUrl: "https://clickclack.example" }),
).rejects.toThrow("12 valid base32 characters");
await expect(
prepare({
code: "https://clickclack.example/?next=api#ABCD-EFGH-JKMN",
}),
).rejects.toThrow("must not include a query");
await expect(
prepare({
code:
"https://api.clickclack.example/services/clickclack/api/bot-setup-codes/claim" +
"#ABCD-EFGH-JKMN",
baseUrl: "https://api.clickclack.example",
}),
).rejects.toThrow("does not match");
expect(claimClickClackSetupCode).not.toHaveBeenCalled();
});
it("maps invalid and rate-limited claims to actionable errors", async () => {
claimClickClackSetupCode.mockRejectedValueOnce(makeClaimError(404, "not found"));
await expect(
prepare({ code: "ABCD-EFGH-JKMN", baseUrl: "https://clickclack.example" }),
).rejects.toThrow("invalid, expired, or already used");
claimClickClackSetupCode.mockRejectedValueOnce(makeClaimError(429, "retry later"));
await expect(
prepare({ code: "ABCD-EFGH-JKMN", baseUrl: "https://clickclack.example" }),
).rejects.toThrow("Too many ClickClack setup code attempts");
});
it("writes setup-code defaults through the existing account patch", () => {
expect(
clickClackSetupContract.applyAccountConfig({
cfg: {},
accountId: DEFAULT_ACCOUNT_ID,
input: {
token: "test-token",
baseUrl: "https://clickclack.example",
workspace: "wsp_1",
defaultTo: " channel:general ",
allowFrom: ["*"],
agentActivity: true,
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
clickclack: {
enabled: true,
token: "test-token",
baseUrl: "https://clickclack.example",
workspace: "wsp_1",
defaultTo: "channel:general",
allowFrom: ["*"],
agentActivity: true,
},
},
});
});
it("requires token, base URL, and workspace for explicit setup", () => {
const message = "ClickClack requires --token, --base-url, and --workspace (or --use-env).";
expect(
validate({ input: { baseUrl: "https://clickclack.example", workspace: "default" } }),
).toBe(message);
expect(validate({ input: { token: "ccb_test", workspace: "default" } })).toBe(message);
expect(validate({ input: { token: "ccb_test", baseUrl: "https://clickclack.example" } })).toBe(
message,
);
expect(
validate({
input: {
token: "ccb_test",
baseUrl: "https://clickclack.example",
workspace: "default",
},
}),
).toBeNull();
});
it("limits --use-env to the default account and requires URL and workspace config", () => {
expect(
validate({
accountId: "work",
input: {
useEnv: true,
baseUrl: "https://clickclack.example",
workspace: "default",
},
}),
).toBe("CLICKCLACK_BOT_TOKEN can only be used for the default account.");
expect(validate({ input: { useEnv: true } })).toBe(
"ClickClack requires --token, --base-url, and --workspace (or --use-env).",
);
expect(
validate({
cfg: {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
workspace: "default",
},
},
} as OpenClawConfig,
input: { useEnv: true },
}),
).toBeNull();
expect(
validate({
cfg: {
channels: {
clickclack: {
baseUrl: "ssh://clickclack.example",
workspace: "default",
},
},
} as OpenClawConfig,
input: { useEnv: true },
}),
).toBe("ClickClack base URL must be a valid http(s) URL.");
});
it("rejects malformed base URLs before writing config", () => {
expect(
validate({
input: {
token: "ccb_test",
baseUrl: "clickclack.example",
workspace: "default",
},
}),
).toBe("ClickClack base URL must be a valid http(s) URL.");
});
it("writes normalized default and named account config", () => {
expect(
clickClackSetupContract.applyAccountConfig({
cfg: {},
accountId: DEFAULT_ACCOUNT_ID,
input: {
name: "Primary",
token: "ccb_default",
baseUrl: "https://clickclack.example/",
workspace: " default ",
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
clickclack: {
enabled: true,
name: "Primary",
token: "ccb_default",
baseUrl: "https://clickclack.example",
workspace: "default",
},
},
});
expect(
clickClackSetupContract.applyAccountConfig({
cfg: { channels: { clickclack: { name: "Legacy" } } } as OpenClawConfig,
accountId: "Work Team",
input: {
name: "Work",
tokenFile: "/run/secrets/clickclack",
baseUrl: "https://work.clickclack.example/",
workspace: "wsp_work",
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
clickclack: {
enabled: true,
accounts: {
default: { name: "Legacy" },
"work-team": {
enabled: true,
name: "Work",
tokenFile: "/run/secrets/clickclack",
baseUrl: "https://work.clickclack.example",
workspace: "wsp_work",
},
},
},
},
});
});
it("keeps --use-env config free of token fields", () => {
expect(
clickClackSetupContract.applyAccountConfig({
cfg: {},
accountId: DEFAULT_ACCOUNT_ID,
input: {
useEnv: true,
baseUrl: "https://clickclack.example/",
workspace: "default",
} as ClickClackSetupInput,
}),
).toEqual({
channels: {
clickclack: {
enabled: true,
baseUrl: "https://clickclack.example",
workspace: "default",
},
},
});
});
it("clears stale competing credentials when the auth mode changes", () => {
const base = {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
workspace: "default",
},
},
} as OpenClawConfig;
const withToken = clickClackSetupContract.applyAccountConfig({
cfg: {
channels: {
clickclack: {
...base.channels?.clickclack,
tokenFile: "/run/secrets/old-token",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: {
token: "ccb_new",
baseUrl: "https://clickclack.example",
workspace: "default",
} as ClickClackSetupInput,
});
expect(withToken.channels?.clickclack).toMatchObject({ token: "ccb_new" });
expect(withToken.channels?.clickclack).not.toHaveProperty("tokenFile");
const withFile = clickClackSetupContract.applyAccountConfig({
cfg: {
channels: {
clickclack: {
...base.channels?.clickclack,
token: "ccb_old",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: {
tokenFile: "/run/secrets/new-token",
baseUrl: "https://clickclack.example",
workspace: "default",
} as ClickClackSetupInput,
});
expect(withFile.channels?.clickclack).toMatchObject({
tokenFile: "/run/secrets/new-token",
});
expect(withFile.channels?.clickclack).not.toHaveProperty("token");
const withEnv = clickClackSetupContract.applyAccountConfig({
cfg: {
channels: {
clickclack: {
baseUrl: "https://clickclack.example/",
workspace: " default ",
token: "ccb_old",
tokenFile: "/run/secrets/old-token",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: { useEnv: true },
});
expect(withEnv.channels?.clickclack).not.toHaveProperty("token");
expect(withEnv.channels?.clickclack).not.toHaveProperty("tokenFile");
expect(withEnv.channels?.clickclack).toMatchObject({
baseUrl: "https://clickclack.example",
workspace: "default",
});
const namedWithToken = clickClackSetupContract.applyAccountConfig({
cfg: {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
workspace: "default",
tokenFile: "/run/secrets/default-token",
},
},
} as OpenClawConfig,
accountId: "work",
input: {
token: "ccb_work",
baseUrl: "https://clickclack.example",
workspace: "work",
} as ClickClackSetupInput,
});
expect(namedWithToken.channels?.clickclack).not.toHaveProperty("tokenFile");
expect(namedWithToken.channels?.clickclack?.accounts).toMatchObject({
default: { tokenFile: "/run/secrets/default-token" },
work: { token: "ccb_work" },
});
expect(namedWithToken.channels?.clickclack?.accounts?.work).not.toHaveProperty("tokenFile");
});
it("preserves credentials when a partial patch does not select an auth mode", () => {
const cfg = {
channels: {
clickclack: {
tokenFile: "/run/secrets/clickclack",
},
},
} as OpenClawConfig;
expect(
applyClickClackCredentialConfig({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
}).channels?.clickclack,
).toMatchObject({
enabled: true,
tokenFile: "/run/secrets/clickclack",
});
});
it("runs post-write verification with the saved account config", async () => {
const cfg = {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
token: "ccb_test",
workspace: "default",
},
},
} as OpenClawConfig;
const runtime = createNonExitingRuntimeEnv();
await clickClackSetupContract.afterAccountConfigWritten?.({
previousCfg: {},
cfg,
accountId: DEFAULT_ACCOUNT_ID,
input: {},
runtime,
});
expect(verifyClickClackAccountAfterSetup).toHaveBeenCalledWith({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
runtime,
});
});
});