mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:30:47 +00:00
436 lines
14 KiB
TypeScript
436 lines
14 KiB
TypeScript
/**
|
|
* Tests for setup-surface.ts helpers.
|
|
*
|
|
* Tests cover:
|
|
* - promptToken helper
|
|
* - promptUsername helper
|
|
* - promptClientId helper
|
|
* - promptChannelName helper
|
|
* - promptRefreshTokenSetup helper
|
|
* - configureWithEnvToken helper
|
|
* - setTwitchAccount config updates
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { WizardPrompter } from "../api.js";
|
|
import {
|
|
configureWithEnvToken,
|
|
promptChannelName,
|
|
promptClientId,
|
|
promptRefreshTokenSetup,
|
|
promptToken,
|
|
promptUsername,
|
|
setTwitchAccount,
|
|
twitchSetupPlugin,
|
|
twitchSetupWizard,
|
|
} from "./setup-surface.js";
|
|
import type { TwitchAccountConfig } from "./types.js";
|
|
|
|
// Mock the helpers we're testing
|
|
const mockPromptText = vi.fn();
|
|
const mockPromptConfirm = vi.fn();
|
|
const mockPromptNote = vi.fn();
|
|
const mockPrompter: WizardPrompter = {
|
|
text: mockPromptText,
|
|
confirm: mockPromptConfirm,
|
|
note: mockPromptNote,
|
|
} as unknown as WizardPrompter;
|
|
const originalEnvToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN;
|
|
|
|
const mockAccount: TwitchAccountConfig = {
|
|
username: "testbot",
|
|
accessToken: "oauth:test123",
|
|
clientId: "test-client-id",
|
|
channel: "#testchannel",
|
|
};
|
|
|
|
describe("setup surface helpers", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalEnvToken === undefined) {
|
|
delete process.env.OPENCLAW_TWITCH_ACCESS_TOKEN;
|
|
} else {
|
|
process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = originalEnvToken;
|
|
}
|
|
// Don't restoreAllMocks as it breaks module-level mocks
|
|
});
|
|
|
|
describe("promptToken", () => {
|
|
it("should return existing token when user confirms to keep it", async () => {
|
|
mockPromptConfirm.mockResolvedValue(true);
|
|
|
|
const result = await promptToken(mockPrompter, mockAccount, undefined);
|
|
|
|
expect(result).toBe("oauth:test123");
|
|
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
|
message: "Access token already configured. Keep it?",
|
|
initialValue: true,
|
|
});
|
|
expect(mockPromptText).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should validate token format", async () => {
|
|
// Set up mocks - user doesn't want to keep existing token
|
|
mockPromptConfirm.mockResolvedValueOnce(false);
|
|
|
|
// Track how many times promptText is called
|
|
let promptTextCallCount = 0;
|
|
let capturedValidate: ((value: string) => string | undefined) | undefined;
|
|
|
|
mockPromptText.mockImplementationOnce((_args) => {
|
|
promptTextCallCount++;
|
|
// Capture the validate function from the first argument
|
|
if (_args?.validate) {
|
|
capturedValidate = _args.validate;
|
|
}
|
|
return Promise.resolve("oauth:test123");
|
|
});
|
|
|
|
// Call promptToken
|
|
const result = await promptToken(mockPrompter, mockAccount, undefined);
|
|
|
|
// Verify promptText was called
|
|
expect(promptTextCallCount).toBe(1);
|
|
expect(result).toBe("oauth:test123");
|
|
|
|
// Test the validate function
|
|
if (!capturedValidate) {
|
|
throw new Error("promptToken validate callback was not captured");
|
|
}
|
|
expect(capturedValidate("")).toBe("Required");
|
|
expect(capturedValidate("notoauth")).toBe("Token should start with 'oauth:'");
|
|
expect(capturedValidate("oauth:goodtoken")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("promptUsername", () => {
|
|
it("should prompt for username with validation", async () => {
|
|
mockPromptText.mockResolvedValue("mybot");
|
|
|
|
const result = await promptUsername(mockPrompter, null);
|
|
|
|
expect(result).toBe("mybot");
|
|
expect(mockPromptText).toHaveBeenCalledWith({
|
|
message: "Twitch bot username",
|
|
initialValue: "",
|
|
validate: expect.any(Function),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("promptClientId", () => {
|
|
it("should prompt for client ID with validation", async () => {
|
|
mockPromptText.mockResolvedValue("abc123xyz");
|
|
|
|
const result = await promptClientId(mockPrompter, null);
|
|
|
|
expect(result).toBe("abc123xyz");
|
|
expect(mockPromptText).toHaveBeenCalledWith({
|
|
message: "Twitch Client ID",
|
|
initialValue: "",
|
|
validate: expect.any(Function),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("promptChannelName", () => {
|
|
it("should require a non-empty channel name", async () => {
|
|
mockPromptText.mockResolvedValue("");
|
|
|
|
await promptChannelName(mockPrompter, null);
|
|
|
|
const { validate } = mockPromptText.mock.calls[0]?.[0] ?? {};
|
|
expect(validate?.("")).toBe("Required");
|
|
expect(validate?.(" ")).toBe("Required");
|
|
expect(validate?.("#chan")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("promptRefreshTokenSetup", () => {
|
|
it("should return empty object when user declines", async () => {
|
|
mockPromptConfirm.mockResolvedValue(false);
|
|
|
|
const result = await promptRefreshTokenSetup(mockPrompter, mockAccount);
|
|
|
|
expect(result).toEqual({});
|
|
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
|
message: "Enable automatic token refresh (requires client secret and refresh token)?",
|
|
initialValue: false,
|
|
});
|
|
});
|
|
|
|
it("should prompt for credentials when user accepts", async () => {
|
|
mockPromptConfirm
|
|
.mockResolvedValueOnce(true) // First call: useRefresh
|
|
.mockResolvedValueOnce("secret123") // clientSecret
|
|
.mockResolvedValueOnce("refresh123"); // refreshToken
|
|
|
|
mockPromptText.mockResolvedValueOnce("secret123").mockResolvedValueOnce("refresh123");
|
|
|
|
const result = await promptRefreshTokenSetup(mockPrompter, null);
|
|
|
|
expect(result).toEqual({
|
|
clientSecret: "secret123",
|
|
refreshToken: "refresh123",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("configureWithEnvToken", () => {
|
|
it("should prompt for username and clientId when using env token", async () => {
|
|
// Reset and set up mocks - user accepts env token
|
|
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
|
|
|
// Set up mocks for username and clientId prompts
|
|
mockPromptText
|
|
.mockReset()
|
|
.mockResolvedValueOnce("testbot" as never)
|
|
.mockResolvedValueOnce("test-client-id" as never);
|
|
|
|
const result = await configureWithEnvToken(
|
|
{} as Parameters<typeof configureWithEnvToken>[0],
|
|
mockPrompter,
|
|
null,
|
|
"oauth:fromenv",
|
|
false,
|
|
{} as Parameters<typeof configureWithEnvToken>[5],
|
|
);
|
|
|
|
// Should return config with username and clientId
|
|
expect(result).not.toBeNull();
|
|
const defaultAccount = result?.cfg.channels?.twitch?.accounts?.default as
|
|
| { username?: string; clientId?: string }
|
|
| undefined;
|
|
expect(defaultAccount?.username).toBe("testbot");
|
|
expect(defaultAccount?.clientId).toBe("test-client-id");
|
|
});
|
|
|
|
it("writes env-token setup to the configured default account", async () => {
|
|
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
|
mockPromptText
|
|
.mockReset()
|
|
.mockResolvedValueOnce("secondary-bot" as never)
|
|
.mockResolvedValueOnce("secondary-client" as never);
|
|
|
|
const result = await configureWithEnvToken(
|
|
{
|
|
channels: {
|
|
twitch: {
|
|
defaultAccount: "secondary",
|
|
},
|
|
},
|
|
} as Parameters<typeof configureWithEnvToken>[0],
|
|
mockPrompter,
|
|
null,
|
|
"oauth:fromenv",
|
|
false,
|
|
{} as Parameters<typeof configureWithEnvToken>[5],
|
|
);
|
|
|
|
const secondaryAccount = result?.cfg.channels?.twitch?.accounts?.secondary as
|
|
| { username?: string; clientId?: string }
|
|
| undefined;
|
|
expect(secondaryAccount?.username).toBe("secondary-bot");
|
|
expect(secondaryAccount?.clientId).toBe("secondary-client");
|
|
expect(result?.cfg.channels?.twitch?.accounts?.default).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("defaultAccount setup resolution", () => {
|
|
it("reports status for the configured default account", async () => {
|
|
const lines = twitchSetupWizard.status?.resolveStatusLines?.({
|
|
cfg: {
|
|
channels: {
|
|
twitch: {
|
|
defaultAccount: "secondary",
|
|
accounts: {
|
|
secondary: {
|
|
username: "secondary-bot",
|
|
accessToken: "oauth:secondary",
|
|
clientId: "secondary-client",
|
|
channel: "#secondary",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never);
|
|
|
|
expect(lines).toEqual(["Twitch (secondary): configured"]);
|
|
});
|
|
|
|
it("reports status for the requested account override", async () => {
|
|
const lines = twitchSetupWizard.status?.resolveStatusLines?.({
|
|
cfg: {
|
|
channels: {
|
|
twitch: {
|
|
accounts: {
|
|
default: {
|
|
username: "default-bot",
|
|
accessToken: "oauth:default",
|
|
clientId: "default-client",
|
|
channel: "#default",
|
|
},
|
|
secondary: {
|
|
username: "secondary-bot",
|
|
accessToken: "oauth:secondary",
|
|
clientId: "secondary-client",
|
|
channel: "#secondary",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
accountId: "secondary",
|
|
configured: true,
|
|
} as never);
|
|
|
|
expect(lines).toEqual(["Twitch (secondary): configured"]);
|
|
});
|
|
|
|
it("reports env-token default account setup as configured", async () => {
|
|
process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:fromenv";
|
|
|
|
const cfg = {
|
|
channels: {
|
|
twitch: {
|
|
accounts: {
|
|
default: {
|
|
username: "env-bot",
|
|
accessToken: "",
|
|
clientId: "env-client",
|
|
channel: "#env",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as Parameters<NonNullable<typeof twitchSetupWizard.status>["resolveConfigured"]>[0]["cfg"];
|
|
|
|
expect(twitchSetupWizard.status?.resolveConfigured({ cfg })).toBe(true);
|
|
const account = twitchSetupPlugin.config.resolveAccount(cfg, "default");
|
|
expect(await twitchSetupPlugin.config.isConfigured?.(account, cfg)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("setup wizard account routing", () => {
|
|
it("normalizes account ids before using them as config keys", () => {
|
|
const cfg = setTwitchAccount(
|
|
{} as Parameters<typeof setTwitchAccount>[0],
|
|
{
|
|
username: "normalized-bot",
|
|
accessToken: "oauth:normalized",
|
|
clientId: "normalized-client",
|
|
channel: "#normalized",
|
|
},
|
|
"__proto__",
|
|
);
|
|
|
|
expect(cfg.channels?.twitch?.accounts?.default?.username).toBe("normalized-bot");
|
|
expect(Object.prototype).not.toHaveProperty("username");
|
|
expect(
|
|
twitchSetupWizard.status?.resolveStatusLines?.({
|
|
cfg: {},
|
|
accountId: "Alerts\r\n\u001b[31m",
|
|
configured: false,
|
|
} as never),
|
|
).toEqual(["Twitch (alerts-31m): needs username, token, and clientId"]);
|
|
});
|
|
|
|
it("reports account-scoped DM policy config keys", () => {
|
|
expect(
|
|
twitchSetupWizard.dmPolicy?.resolveConfigKeys?.(
|
|
{
|
|
channels: {
|
|
twitch: {
|
|
defaultAccount: "secondary",
|
|
},
|
|
},
|
|
} as Parameters<
|
|
NonNullable<NonNullable<typeof twitchSetupWizard.dmPolicy>["resolveConfigKeys"]>
|
|
>[0],
|
|
undefined,
|
|
),
|
|
).toEqual({
|
|
policyKey: "channels.twitch.accounts.secondary.allowedRoles",
|
|
allowFromKey: "channels.twitch.accounts.secondary.allowFrom",
|
|
});
|
|
|
|
expect(twitchSetupWizard.dmPolicy?.resolveConfigKeys?.({} as never, "alerts")).toEqual({
|
|
policyKey: "channels.twitch.accounts.alerts.allowedRoles",
|
|
allowFromKey: "channels.twitch.accounts.alerts.allowFrom",
|
|
});
|
|
});
|
|
|
|
it("writes to the requested account when defaultAccount is not created yet", async () => {
|
|
mockPromptText
|
|
.mockReset()
|
|
.mockResolvedValueOnce("secondary-bot" as never)
|
|
.mockResolvedValueOnce("oauth:secondary" as never)
|
|
.mockResolvedValueOnce("secondary-client" as never)
|
|
.mockResolvedValueOnce("#secondary" as never);
|
|
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
|
|
|
|
const result = await twitchSetupWizard.finalize?.({
|
|
cfg: {
|
|
channels: {
|
|
twitch: {
|
|
defaultAccount: "secondary",
|
|
accounts: {
|
|
default: {
|
|
username: "default-bot",
|
|
accessToken: "oauth:default",
|
|
clientId: "default-client",
|
|
channel: "#default",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as Parameters<NonNullable<typeof twitchSetupWizard.finalize>>[0]["cfg"],
|
|
accountId: "secondary",
|
|
credentialValues: {},
|
|
runtime: {} as Parameters<NonNullable<typeof twitchSetupWizard.finalize>>[0]["runtime"],
|
|
prompter: mockPrompter,
|
|
options: {},
|
|
forceAllowFrom: false,
|
|
});
|
|
|
|
const twitch = result?.cfg?.channels?.twitch;
|
|
expect(twitch?.accounts?.secondary?.username).toBe("secondary-bot");
|
|
expect(twitch?.accounts?.secondary?.accessToken).toBe("oauth:secondary");
|
|
expect(twitch?.accounts?.default?.username).toBe("default-bot");
|
|
});
|
|
});
|
|
|
|
describe("setup-only plugin config", () => {
|
|
it("lists all configured Twitch accounts", () => {
|
|
const cfg = {
|
|
channels: {
|
|
twitch: {
|
|
defaultAccount: "secondary",
|
|
accounts: {
|
|
default: {
|
|
username: "default-bot",
|
|
accessToken: "oauth:default",
|
|
clientId: "default-client",
|
|
channel: "#default",
|
|
},
|
|
secondary: {
|
|
username: "secondary-bot",
|
|
accessToken: "oauth:secondary",
|
|
clientId: "secondary-client",
|
|
channel: "#secondary",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as Parameters<typeof twitchSetupPlugin.config.listAccountIds>[0];
|
|
|
|
expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["default", "secondary"]);
|
|
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
|
|
});
|
|
});
|
|
});
|