mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-24 23:21:20 +00:00
309 lines
9.1 KiB
TypeScript
309 lines
9.1 KiB
TypeScript
// ClickClack tests cover guided setup prompts and nonfatal live validation.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import {
|
|
createPluginSetupWizardConfigure,
|
|
createQueuedWizardPrompter,
|
|
createTestWizardPrompter,
|
|
runSetupWizardConfigure,
|
|
runSetupWizardFinalize,
|
|
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { withTempDir } from "openclaw/plugin-sdk/test-env";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
createClient: vi.fn(),
|
|
me: vi.fn(),
|
|
resolveWorkspaceId: vi.fn(),
|
|
workspaces: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./http-client.js", () => ({
|
|
createClickClackClient: (options: unknown) => {
|
|
mocks.createClient(options);
|
|
return {
|
|
me: mocks.me,
|
|
workspaces: mocks.workspaces,
|
|
};
|
|
},
|
|
}));
|
|
|
|
vi.mock("./resolve.js", () => ({
|
|
resolveWorkspaceId: mocks.resolveWorkspaceId,
|
|
}));
|
|
|
|
import { clickClackSetupPlugin } from "./channel.setup.js";
|
|
import { clickClackSetupWizard } from "./setup-surface.js";
|
|
import type { CoreConfig } from "./types.js";
|
|
|
|
const configuredAccount = {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
token: "ccb_test",
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} satisfies CoreConfig;
|
|
|
|
function tokenCredential() {
|
|
const credential = clickClackSetupWizard.credentials[0];
|
|
if (!credential) {
|
|
throw new Error("expected ClickClack token credential");
|
|
}
|
|
return credential;
|
|
}
|
|
|
|
describe("ClickClack setup wizard", () => {
|
|
beforeEach(() => {
|
|
mocks.createClient.mockReset();
|
|
mocks.me.mockReset();
|
|
mocks.resolveWorkspaceId.mockReset();
|
|
mocks.workspaces.mockReset();
|
|
mocks.me.mockResolvedValue({ id: "usr_bot", handle: "openclaw" });
|
|
mocks.resolveWorkspaceId.mockResolvedValue("wsp_default");
|
|
mocks.workspaces.mockResolvedValue([
|
|
{ id: "wsp_default", name: "Default", slug: "default", created_at: "2026-01-01" },
|
|
]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("detects configured accounts and inspects plain, env, and file credentials", async () => {
|
|
expect(await clickClackSetupWizard.status.resolveConfigured({ cfg: configuredAccount })).toBe(
|
|
true,
|
|
);
|
|
|
|
const credential = tokenCredential();
|
|
expect(
|
|
credential.inspect({
|
|
cfg: configuredAccount,
|
|
accountId: "default",
|
|
}),
|
|
).toMatchObject({
|
|
accountConfigured: true,
|
|
hasConfiguredValue: true,
|
|
resolvedValue: "ccb_test",
|
|
});
|
|
|
|
vi.stubEnv("CLICKCLACK_BOT_TOKEN", "ccb_env");
|
|
expect(
|
|
credential.inspect({
|
|
cfg: {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} as CoreConfig,
|
|
accountId: "default",
|
|
}),
|
|
).toMatchObject({
|
|
accountConfigured: true,
|
|
hasConfiguredValue: false,
|
|
resolvedValue: "ccb_env",
|
|
envValue: "ccb_env",
|
|
});
|
|
|
|
await withTempDir("clickclack-setup-token-", async (tempDir) => {
|
|
const tokenFile = path.join(tempDir, "token");
|
|
fs.writeFileSync(tokenFile, "ccb_file\n", "utf8");
|
|
expect(
|
|
credential.inspect({
|
|
cfg: {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
tokenFile,
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} as CoreConfig,
|
|
accountId: "default",
|
|
}),
|
|
).toMatchObject({
|
|
accountConfigured: true,
|
|
hasConfiguredValue: true,
|
|
resolvedValue: "ccb_file",
|
|
});
|
|
});
|
|
|
|
const secretRefAccount = {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
token: { source: "file", provider: "vault", id: "/clickclack/token" },
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} satisfies CoreConfig;
|
|
expect(await clickClackSetupWizard.status.resolveConfigured({ cfg: secretRefAccount })).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
credential.inspect({
|
|
cfg: secretRefAccount,
|
|
accountId: "default",
|
|
}),
|
|
).toMatchObject({
|
|
accountConfigured: true,
|
|
hasConfiguredValue: true,
|
|
resolvedValue: undefined,
|
|
});
|
|
expect(
|
|
clickClackSetupWizard.introNote?.shouldShow?.({
|
|
cfg: secretRefAccount,
|
|
accountId: "default",
|
|
credentialValues: {},
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("switches the default account to env auth before URL and workspace prompts", async () => {
|
|
const credential = tokenCredential();
|
|
const next = await credential.applyUseEnv?.({
|
|
cfg: {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
token: "ccb_stale",
|
|
tokenFile: "/run/secrets/stale",
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} as CoreConfig,
|
|
accountId: "default",
|
|
});
|
|
|
|
expect(next?.channels?.clickclack).not.toHaveProperty("token");
|
|
expect(next?.channels?.clickclack).not.toHaveProperty("tokenFile");
|
|
expect(next?.channels?.clickclack).toMatchObject({
|
|
baseUrl: "https://clickclack.example",
|
|
workspace: "default",
|
|
});
|
|
});
|
|
|
|
it("saves URL, token, and workspace through the guided flow", async () => {
|
|
const queued = createQueuedWizardPrompter({
|
|
textValues: ["ccb_guided", "https://clickclack.example/", "default"],
|
|
});
|
|
|
|
const result = await runSetupWizardConfigure({
|
|
configure: createPluginSetupWizardConfigure(clickClackSetupPlugin),
|
|
cfg: {} as CoreConfig,
|
|
prompter: queued.prompter,
|
|
options: { secretInputMode: "plaintext" as const },
|
|
});
|
|
|
|
expect(result.cfg.channels?.clickclack).toMatchObject({
|
|
enabled: true,
|
|
token: "ccb_guided",
|
|
baseUrl: "https://clickclack.example",
|
|
workspace: "default",
|
|
});
|
|
expect(mocks.me).toHaveBeenCalledTimes(1);
|
|
expect(mocks.resolveWorkspaceId).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("reports the resolved bot and workspace after live validation", async () => {
|
|
const note = vi.fn(async () => undefined);
|
|
|
|
await runSetupWizardFinalize({
|
|
finalize: clickClackSetupWizard.finalize,
|
|
cfg: configuredAccount,
|
|
prompter: createTestWizardPrompter({ note }),
|
|
});
|
|
|
|
expect(mocks.me.mock.invocationCallOrder[0]).toBeLessThan(
|
|
mocks.resolveWorkspaceId.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
|
|
);
|
|
expect(note).toHaveBeenCalledWith(
|
|
"Connected as @openclaw — workspace Default resolved.",
|
|
"ClickClack connection",
|
|
);
|
|
});
|
|
|
|
it("uses the resolved setup credential for live validation", async () => {
|
|
await runSetupWizardFinalize({
|
|
finalize: clickClackSetupWizard.finalize,
|
|
cfg: {
|
|
channels: {
|
|
clickclack: {
|
|
baseUrl: "https://clickclack.example",
|
|
token: { source: "file", provider: "vault", id: "/clickclack/token" },
|
|
workspace: "default",
|
|
},
|
|
},
|
|
} satisfies CoreConfig,
|
|
credentialValues: { token: "ccb_resolved" },
|
|
});
|
|
|
|
expect(mocks.createClient).toHaveBeenCalledWith({
|
|
baseUrl: "https://clickclack.example",
|
|
token: "ccb_resolved",
|
|
});
|
|
});
|
|
|
|
it("keeps setup saved when the token is rejected", async () => {
|
|
mocks.me.mockRejectedValue({ status: 401 });
|
|
const note = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
runSetupWizardFinalize({
|
|
finalize: clickClackSetupWizard.finalize,
|
|
cfg: configuredAccount,
|
|
prompter: createTestWizardPrompter({ note }),
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
expect(note).toHaveBeenCalledWith(
|
|
"ClickClack rejected the bot token (401). Copy a current token and rerun setup.",
|
|
"ClickClack connection check",
|
|
);
|
|
});
|
|
|
|
it("keeps setup saved when workspace resolution fails", async () => {
|
|
mocks.resolveWorkspaceId.mockResolvedValue("wsp_missing");
|
|
mocks.workspaces.mockResolvedValue([]);
|
|
const note = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
runSetupWizardFinalize({
|
|
finalize: clickClackSetupWizard.finalize,
|
|
cfg: {
|
|
channels: {
|
|
clickclack: {
|
|
...configuredAccount.channels.clickclack,
|
|
workspace: "wsp_missing",
|
|
},
|
|
},
|
|
},
|
|
prompter: createTestWizardPrompter({ note }),
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
expect(note).toHaveBeenCalledWith(
|
|
'Workspace "wsp_missing" was not found. Check the id, slug, or name, list available workspaces, and rerun setup.',
|
|
"ClickClack connection check",
|
|
);
|
|
});
|
|
|
|
it("keeps setup saved when the server is unreachable", async () => {
|
|
mocks.me.mockRejectedValue(new Error("network unavailable"));
|
|
const note = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
runSetupWizardFinalize({
|
|
finalize: clickClackSetupWizard.finalize,
|
|
cfg: configuredAccount,
|
|
prompter: createTestWizardPrompter({ note }),
|
|
}),
|
|
).resolves.toBeUndefined();
|
|
expect(note).toHaveBeenCalledWith(
|
|
"Connection check failed: network unavailable. Setup was saved; fix the connection and rerun setup.",
|
|
"ClickClack connection check",
|
|
);
|
|
});
|
|
});
|