refactor(test): dedupe setup wizard test helpers

This commit is contained in:
Peter Steinberger
2026-03-21 23:29:02 +00:00
parent 6266b842d4
commit 57fa59ab92
13 changed files with 222 additions and 271 deletions

View File

@@ -1,7 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import {
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";
@@ -34,10 +38,19 @@ async function createBlueBubblesConfigureAdapter() {
});
}
async function runBlueBubblesConfigure(params: { cfg: unknown; prompter: WizardPrompter }) {
const adapter = await createBlueBubblesConfigureAdapter();
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
return await runSetupWizardConfigure({
configure: adapter.configure,
cfg: params.cfg as ConfigureContext["cfg"],
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
prompter: params.prompter,
});
}
describe("bluebubbles setup surface", () => {
it("preserves existing password SecretRef and keeps default webhook path", async () => {
const adapter = await createBlueBubblesConfigureAdapter();
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
const passwordRef = { source: "env", provider: "default", id: "BLUEBUBBLES_PASSWORD" };
const confirm = vi
.fn()
@@ -45,10 +58,8 @@ describe("bluebubbles setup surface", () => {
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
const text = vi.fn();
const note = vi.fn();
const prompter = { confirm, text, note } as unknown as WizardPrompter;
const context = {
const result = await runBlueBubblesConfigure({
cfg: {
channels: {
bluebubbles: {
@@ -58,14 +69,8 @@ describe("bluebubbles setup surface", () => {
},
},
},
prompter,
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
forceAllowFrom: false,
accountOverrides: {},
shouldPromptAccountIds: false,
} satisfies ConfigureContext;
const result = await adapter.configure(context);
prompter: createTestWizardPrompter({ confirm, text }),
});
expect(result.cfg.channels?.bluebubbles?.password).toEqual(passwordRef);
expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe(DEFAULT_WEBHOOK_PATH);
@@ -73,18 +78,14 @@ describe("bluebubbles setup surface", () => {
});
it("applies a custom webhook path when requested", async () => {
const adapter = await createBlueBubblesConfigureAdapter();
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
const confirm = vi
.fn()
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
const text = vi.fn().mockResolvedValueOnce("/custom-bluebubbles");
const note = vi.fn();
const prompter = { confirm, text, note } as unknown as WizardPrompter;
const context = {
const result = await runBlueBubblesConfigure({
cfg: {
channels: {
bluebubbles: {
@@ -94,14 +95,8 @@ describe("bluebubbles setup surface", () => {
},
},
},
prompter,
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
forceAllowFrom: false,
accountOverrides: {},
shouldPromptAccountIds: false,
} satisfies ConfigureContext;
const result = await adapter.configure(context);
prompter: createTestWizardPrompter({ confirm, text }),
});
expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe("/custom-bluebubbles");
expect(text).toHaveBeenCalledWith(
@@ -113,23 +108,13 @@ describe("bluebubbles setup surface", () => {
});
it("validates server URLs before accepting input", async () => {
const adapter = await createBlueBubblesConfigureAdapter();
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
const confirm = vi.fn().mockResolvedValueOnce(false);
const text = vi.fn().mockResolvedValueOnce("127.0.0.1:1234").mockResolvedValueOnce("secret");
const note = vi.fn();
const prompter = { confirm, text, note } as unknown as WizardPrompter;
const context = {
await runBlueBubblesConfigure({
cfg: { channels: { bluebubbles: {} } },
prompter,
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
forceAllowFrom: false,
accountOverrides: {},
shouldPromptAccountIds: false,
} satisfies ConfigureContext;
await adapter.configure(context);
prompter: createTestWizardPrompter({ confirm, text }),
});
const serverUrlPrompt = text.mock.calls[0]?.[0] as {
validate?: (value: string) => string | undefined;

View File

@@ -1,5 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
} from "../../../test/helpers/extensions/setup-wizard.js";
vi.mock("./probe.js", () => ({
probeFeishu: vi.fn(async () => ({ ok: false, error: "mocked" })),
@@ -7,13 +12,6 @@ vi.mock("./probe.js", () => ({
import { feishuPlugin } from "./channel.js";
const baseConfigureContext = {
runtime: {} as never,
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
};
const baseStatusContext = {
accountOverrides: {},
};
@@ -56,10 +54,7 @@ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: st
});
}
const feishuConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: feishuPlugin,
wizard: feishuPlugin.setupWizard!,
});
const feishuConfigureAdapter = createPluginSetupWizardAdapter(feishuPlugin);
describe("feishu setup wizard", () => {
it("does not throw when config appId/appSecret are SecretRef objects", async () => {
@@ -68,18 +63,17 @@ describe("feishu setup wizard", () => {
.mockResolvedValueOnce("cli_from_prompt")
.mockResolvedValueOnce("secret_from_prompt")
.mockResolvedValueOnce("oc_group_1");
const prompter = {
note: vi.fn(async () => undefined),
const prompter = createTestWizardPrompter({
text,
confirm: vi.fn(async () => true),
select: vi.fn(
async ({ initialValue }: { initialValue?: string }) => initialValue ?? "allowlist",
),
} as never;
) as never,
});
await expect(
feishuConfigureAdapter.configure({
runSetupWizardConfigure({
configure: feishuConfigureAdapter.configure,
cfg: {
channels: {
feishu: {
@@ -89,7 +83,7 @@ describe("feishu setup wizard", () => {
},
} as never,
prompter,
...baseConfigureContext,
runtime: createRuntimeEnv({ throwOnExit: false }) as never,
}),
).resolves.toBeTruthy();
});

View File

@@ -1,17 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { googlechatPlugin } from "./channel.js";
const googlechatConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: googlechatPlugin,
wizard: googlechatPlugin.setupWizard!,
});
const googlechatConfigureAdapter = createPluginSetupWizardAdapter(googlechatPlugin);
describe("googlechat setup wizard", () => {
it("configures service-account auth and webhook audience", async () => {
@@ -27,16 +24,11 @@ describe("googlechat setup wizard", () => {
}) as WizardPrompter["text"],
});
const runtime = createRuntimeEnv();
const result = await googlechatConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: googlechatConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");

View File

@@ -1,18 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { ircPlugin } from "./channel.js";
import type { RuntimeEnv } from "./runtime-api.js";
import type { CoreConfig } from "./types.js";
const ircConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: ircPlugin,
wizard: ircPlugin.setupWizard!,
});
const ircConfigureAdapter = createPluginSetupWizardAdapter(ircPlugin);
describe("irc setup wizard", () => {
it("configures host and nick via setup prompts", async () => {
@@ -52,16 +48,11 @@ describe("irc setup wizard", () => {
}),
});
const runtime: RuntimeEnv = createRuntimeEnv();
const result = await ircConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: ircConfigureAdapter.configure,
cfg: {} as CoreConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");

View File

@@ -5,9 +5,9 @@ import {
resolveDefaultLineAccountId,
resolveLineAccount,
} from "../../../src/line/accounts.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../api.js";
@@ -42,14 +42,11 @@ describe("line setup wizard", () => {
}) as WizardPrompter["text"],
});
const result = await lineConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: lineConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime: createRuntimeEnv(),
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");

View File

@@ -62,6 +62,12 @@ function createDeferred() {
describe("FileBackedMatrixSyncStore", () => {
const tempDirs: string[] = [];
function createStoragePath(): string {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
return path.join(tempDir, "bot-storage.json");
}
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
@@ -71,9 +77,7 @@ describe("FileBackedMatrixSyncStore", () => {
});
it("persists sync data so restart resumes from the saved cursor", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const firstStore = new FileBackedMatrixSyncStore(storagePath);
expect(firstStore.hasSavedSync()).toBe(false);
@@ -97,9 +101,7 @@ describe("FileBackedMatrixSyncStore", () => {
});
it("only treats sync state as restart-safe after a clean shutdown persist", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const firstStore = new FileBackedMatrixSyncStore(storagePath);
await firstStore.setSyncData(createSyncResponse("s123"));
@@ -118,9 +120,7 @@ describe("FileBackedMatrixSyncStore", () => {
});
it("clears the clean-shutdown marker once fresh sync data arrives", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const firstStore = new FileBackedMatrixSyncStore(storagePath);
await firstStore.setSyncData(createSyncResponse("s123"));
@@ -141,9 +141,7 @@ describe("FileBackedMatrixSyncStore", () => {
it("coalesces background persistence until the debounce window elapses", async () => {
vi.useFakeTimers();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const writeSpy = vi.spyOn(jsonFiles, "writeJsonAtomic").mockResolvedValue();
const store = new FileBackedMatrixSyncStore(storagePath);
@@ -174,9 +172,7 @@ describe("FileBackedMatrixSyncStore", () => {
it("waits for an in-flight persist when shutdown flush runs", async () => {
vi.useFakeTimers();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const writeDeferred = createDeferred();
const writeSpy = vi
.spyOn(jsonFiles, "writeJsonAtomic")
@@ -201,9 +197,7 @@ describe("FileBackedMatrixSyncStore", () => {
});
it("persists client options alongside sync state", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
const firstStore = new FileBackedMatrixSyncStore(storagePath);
await firstStore.storeClientOptions({ lazyLoadMembers: true });
@@ -214,9 +208,7 @@ describe("FileBackedMatrixSyncStore", () => {
});
it("loads legacy raw sync payloads from bot-storage.json", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-sync-store-"));
tempDirs.push(tempDir);
const storagePath = path.join(tempDir, "bot-storage.json");
const storagePath = createStoragePath();
fs.writeFileSync(
storagePath,

View File

@@ -1,17 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { nostrPlugin } from "./channel.js";
const nostrConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: nostrPlugin,
wizard: nostrPlugin.setupWizard!,
});
const nostrConfigureAdapter = createPluginSetupWizardAdapter(nostrPlugin);
describe("nostr setup wizard", () => {
it("configures a private key and relay URLs", async () => {
@@ -27,14 +24,11 @@ describe("nostr setup wizard", () => {
}) as WizardPrompter["text"],
});
const result = await nostrConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: nostrConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime: createRuntimeEnv(),
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");

View File

@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { synologyChatPlugin } from "./channel.js";
@@ -31,14 +31,11 @@ describe("synology-chat setup wizard", () => {
}) as WizardPrompter["text"],
});
const result = await synologyChatConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: synologyChatConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime: createRuntimeEnv(),
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");
@@ -68,13 +65,11 @@ describe("synology-chat setup wizard", () => {
}) as WizardPrompter["text"],
});
const result = await synologyChatConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: synologyChatConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime: createRuntimeEnv(),
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: true,
});

View File

@@ -1,17 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig, RuntimeEnv } from "../api.js";
import type { OpenClawConfig } from "../api.js";
import { tlonPlugin } from "./channel.js";
const tlonConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: tlonPlugin,
wizard: tlonPlugin.setupWizard!,
});
const tlonConfigureAdapter = createPluginSetupWizardAdapter(tlonPlugin);
describe("tlon setup wizard", () => {
it("configures ship, auth, and discovery settings", async () => {
@@ -48,16 +45,11 @@ describe("tlon setup wizard", () => {
}),
});
const runtime: RuntimeEnv = createRuntimeEnv();
const result = await tlonConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: tlonConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");

View File

@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import {
createQueuedWizardPrompter,
runSetupWizardConfigure,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { whatsappPlugin } from "./channel.js";
const loginWebMock = vi.hoisted(() => vi.fn(async () => {}));
@@ -34,49 +37,6 @@ vi.mock("./accounts.js", () => ({
resolveWhatsAppAuthDir: resolveWhatsAppAuthDirMock,
}));
function createPrompterHarness(params?: {
selectValues?: string[];
textValues?: string[];
confirmValues?: boolean[];
}) {
const selectValues = [...(params?.selectValues ?? [])];
const textValues = [...(params?.textValues ?? [])];
const confirmValues = [...(params?.confirmValues ?? [])];
const intro = vi.fn(async () => undefined);
const outro = vi.fn(async () => undefined);
const note = vi.fn(async () => undefined);
const select = vi.fn(async () => selectValues.shift() ?? "");
const multiselect = vi.fn(async () => [] as string[]);
const text = vi.fn(async () => textValues.shift() ?? "");
const confirm = vi.fn(async () => confirmValues.shift() ?? false);
const progress = vi.fn(() => ({
update: vi.fn(),
stop: vi.fn(),
}));
return {
intro,
outro,
note,
select,
multiselect,
text,
confirm,
progress,
prompter: {
intro,
outro,
note,
select,
multiselect,
text,
confirm,
progress,
} as WizardPrompter,
};
}
function createRuntime(): RuntimeEnv {
return {
error: vi.fn(),
@@ -89,7 +49,7 @@ const whatsappConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
});
async function runConfigureWithHarness(params: {
harness: ReturnType<typeof createPrompterHarness>;
harness: ReturnType<typeof createQueuedWizardPrompter>;
cfg?: Parameters<typeof whatsappConfigureAdapter.configure>[0]["cfg"];
runtime?: RuntimeEnv;
options?: Parameters<typeof whatsappConfigureAdapter.configure>[0]["options"];
@@ -97,7 +57,8 @@ async function runConfigureWithHarness(params: {
shouldPromptAccountIds?: boolean;
forceAllowFrom?: boolean;
}) {
return await whatsappConfigureAdapter.configure({
return await runSetupWizardConfigure({
configure: whatsappConfigureAdapter.configure,
cfg: params.cfg ?? {},
runtime: params.runtime ?? createRuntime(),
prompter: params.harness.prompter,
@@ -109,7 +70,7 @@ async function runConfigureWithHarness(params: {
}
function createSeparatePhoneHarness(params: { selectValues: string[]; textValues?: string[] }) {
return createPrompterHarness({
return createQueuedWizardPrompter({
confirmValues: [false],
selectValues: params.selectValues,
textValues: params.textValues,
@@ -138,7 +99,7 @@ describe("whatsapp setup wizard", () => {
});
it("applies owner allowlist when forceAllowFrom is enabled", async () => {
const harness = createPrompterHarness({
const harness = createQueuedWizardPrompter({
confirmValues: [false],
textValues: ["+1 (555) 555-0123"],
});
@@ -184,7 +145,7 @@ describe("whatsapp setup wizard", () => {
it("enables allowlist self-chat mode for personal-phone setup", async () => {
pathExistsMock.mockResolvedValue(true);
const harness = createPrompterHarness({
const harness = createQueuedWizardPrompter({
confirmValues: [false],
selectValues: ["personal"],
textValues: ["+1 (555) 111-2222"],
@@ -225,7 +186,7 @@ describe("whatsapp setup wizard", () => {
it("runs WhatsApp login when not linked and user confirms linking", async () => {
pathExistsMock.mockResolvedValue(false);
const harness = createPrompterHarness({
const harness = createQueuedWizardPrompter({
confirmValues: [true],
selectValues: ["separate", "disabled"],
});

View File

@@ -1,17 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { zaloPlugin } from "./channel.js";
const zaloConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: zaloPlugin,
wizard: zaloPlugin.setupWizard!,
});
const zaloConfigureAdapter = createPluginSetupWizardAdapter(zaloPlugin);
describe("zalo setup wizard", () => {
it("configures a polling token flow", async () => {
@@ -31,16 +28,11 @@ describe("zalo setup wizard", () => {
}),
});
const runtime: RuntimeEnv = createRuntimeEnv();
const result = await zaloConfigureAdapter.configure({
const result = await runSetupWizardConfigure({
configure: zaloConfigureAdapter.configure,
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: { secretInputMode: "plaintext" },
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
options: { secretInputMode: "plaintext" as const },
});
expect(result.accountId).toBe("default");

View File

@@ -1,19 +1,32 @@
import { describe, expect, it, vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
import { createTestWizardPrompter } from "../../../test/helpers/extensions/setup-wizard.js";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
runSetupWizardConfigure,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../runtime-api.js";
import "./zalo-js.test-mocks.js";
import { zalouserPlugin } from "./channel.js";
const zalouserConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({
plugin: zalouserPlugin,
wizard: zalouserPlugin.setupWizard!,
});
const zalouserConfigureAdapter = createPluginSetupWizardAdapter(zalouserPlugin);
async function runSetup(params: {
cfg?: OpenClawConfig;
prompter: ReturnType<typeof createTestWizardPrompter>;
options?: Record<string, unknown>;
forceAllowFrom?: boolean;
}) {
return await runSetupWizardConfigure({
configure: zalouserConfigureAdapter.configure,
cfg: params.cfg as OpenClawConfig | undefined,
prompter: params.prompter,
options: params.options,
forceAllowFrom: params.forceAllowFrom,
});
}
describe("zalouser setup wizard", () => {
it("enables the account without forcing QR login", async () => {
const runtime = createRuntimeEnv();
const prompter = createTestWizardPrompter({
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Login via QR code now?") {
@@ -26,15 +39,7 @@ describe("zalouser setup wizard", () => {
}),
});
const result = await zalouserConfigureAdapter.configure({
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
const result = await runSetup({ prompter });
expect(result.accountId).toBe("default");
expect(result.cfg.channels?.zalouser?.enabled).toBe(true);
@@ -42,7 +47,6 @@ describe("zalouser setup wizard", () => {
});
it("prompts DM policy before group access in quickstart", async () => {
const runtime = createRuntimeEnv();
const seen: string[] = [];
const prompter = createTestWizardPrompter({
confirm: vi.fn(async ({ message }: { message: string }) => {
@@ -70,14 +74,9 @@ describe("zalouser setup wizard", () => {
) as ReturnType<typeof createTestWizardPrompter>["select"],
});
const result = await zalouserConfigureAdapter.configure({
cfg: {} as OpenClawConfig,
runtime,
const result = await runSetup({
prompter,
options: { quickstartDefaults: true },
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");
@@ -92,7 +91,6 @@ describe("zalouser setup wizard", () => {
});
it("allows an empty quickstart DM allowlist with a warning", async () => {
const runtime = createRuntimeEnv();
const note = vi.fn(async (_message: string, _title?: string) => {});
const prompter = createTestWizardPrompter({
note,
@@ -125,14 +123,9 @@ describe("zalouser setup wizard", () => {
}) as ReturnType<typeof createTestWizardPrompter>["text"],
});
const result = await zalouserConfigureAdapter.configure({
cfg: {} as OpenClawConfig,
runtime,
const result = await runSetup({
prompter,
options: { quickstartDefaults: true },
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.accountId).toBe("default");
@@ -148,7 +141,6 @@ describe("zalouser setup wizard", () => {
});
it("allows an empty group allowlist with a warning", async () => {
const runtime = createRuntimeEnv();
const note = vi.fn(async (_message: string, _title?: string) => {});
const prompter = createTestWizardPrompter({
note,
@@ -181,15 +173,7 @@ describe("zalouser setup wizard", () => {
}) as ReturnType<typeof createTestWizardPrompter>["text"],
});
const result = await zalouserConfigureAdapter.configure({
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
const result = await runSetup({ prompter });
expect(result.cfg.channels?.zalouser?.groupPolicy).toBe("allowlist");
expect(result.cfg.channels?.zalouser?.groups).toEqual({});
@@ -201,7 +185,6 @@ describe("zalouser setup wizard", () => {
});
it("preserves non-quickstart forceAllowFrom behavior", async () => {
const runtime = createRuntimeEnv();
const note = vi.fn(async (_message: string, _title?: string) => {});
const seen: string[] = [];
const prompter = createTestWizardPrompter({
@@ -225,15 +208,7 @@ describe("zalouser setup wizard", () => {
}) as ReturnType<typeof createTestWizardPrompter>["text"],
});
const result = await zalouserConfigureAdapter.configure({
cfg: {} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: true,
});
const result = await runSetup({ prompter, forceAllowFrom: true });
expect(result.cfg.channels?.zalouser?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.zalouser?.allowFrom).toEqual([]);
@@ -247,7 +222,6 @@ describe("zalouser setup wizard", () => {
});
it("allowlists the plugin when a plugin allowlist already exists", async () => {
const runtime = createRuntimeEnv();
const prompter = createTestWizardPrompter({
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Login via QR code now?") {
@@ -260,18 +234,13 @@ describe("zalouser setup wizard", () => {
}),
});
const result = await zalouserConfigureAdapter.configure({
const result = await runSetup({
cfg: {
plugins: {
allow: ["telegram"],
},
} as OpenClawConfig,
runtime,
prompter,
options: {},
accountOverrides: {},
shouldPromptAccountIds: false,
forceAllowFrom: false,
});
expect(result.cfg.plugins?.entries?.zalouser?.enabled).toBe(true);

View File

@@ -1,5 +1,7 @@
import { vi } from "vitest";
import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
import { createRuntimeEnv } from "./runtime-env.js";
export type { WizardPrompter } from "../../../src/wizard/prompts.js";
@@ -26,3 +28,98 @@ export function createTestWizardPrompter(overrides: Partial<WizardPrompter> = {}
...overrides,
};
}
export function createQueuedWizardPrompter(params?: {
selectValues?: string[];
textValues?: string[];
confirmValues?: boolean[];
}) {
const selectValues = [...(params?.selectValues ?? [])];
const textValues = [...(params?.textValues ?? [])];
const confirmValues = [...(params?.confirmValues ?? [])];
const intro = vi.fn(async () => undefined);
const outro = vi.fn(async () => undefined);
const note = vi.fn(async () => undefined);
const select = vi.fn(async () => selectValues.shift() ?? "");
const multiselect = vi.fn(async () => [] as string[]);
const text = vi.fn(async () => textValues.shift() ?? "");
const confirm = vi.fn(async () => confirmValues.shift() ?? false);
const progress = vi.fn(() => ({
update: vi.fn(),
stop: vi.fn(),
}));
return {
intro,
outro,
note,
select,
multiselect,
text,
confirm,
progress,
prompter: createTestWizardPrompter({
intro,
outro,
note,
select: select as WizardPrompter["select"],
multiselect: multiselect as WizardPrompter["multiselect"],
text: text as WizardPrompter["text"],
confirm,
progress,
}),
};
}
type SetupWizardAdapterParams = Parameters<typeof buildChannelSetupWizardAdapterFromSetupWizard>[0];
type SetupWizardPlugin = SetupWizardAdapterParams["plugin"];
type SetupWizard = NonNullable<SetupWizardAdapterParams["wizard"]>;
export function createPluginSetupWizardAdapter<
TPlugin extends SetupWizardPlugin & { setupWizard?: SetupWizard },
>(plugin: TPlugin) {
const wizard = plugin.setupWizard;
if (!wizard) {
throw new Error(`${plugin.id} is missing setupWizard`);
}
return buildChannelSetupWizardAdapterFromSetupWizard({
plugin,
wizard,
});
}
export async function runSetupWizardConfigure<
TCfg,
TOptions extends Record<string, unknown>,
TAccountOverrides extends Record<string, string | undefined>,
TRuntime,
TResult,
>(params: {
configure: (args: {
cfg: TCfg;
runtime: TRuntime;
prompter: WizardPrompter;
options: TOptions;
accountOverrides: TAccountOverrides;
shouldPromptAccountIds: boolean;
forceAllowFrom: boolean;
}) => Promise<TResult>;
cfg?: TCfg;
runtime?: TRuntime;
prompter: WizardPrompter;
options?: TOptions;
accountOverrides?: TAccountOverrides;
shouldPromptAccountIds?: boolean;
forceAllowFrom?: boolean;
}): Promise<TResult> {
return await params.configure({
cfg: (params.cfg ?? {}) as TCfg,
runtime: (params.runtime ?? createRuntimeEnv()) as TRuntime,
prompter: params.prompter,
options: (params.options ?? {}) as TOptions,
accountOverrides: (params.accountOverrides ?? {}) as TAccountOverrides,
shouldPromptAccountIds: params.shouldPromptAccountIds ?? false,
forceAllowFrom: params.forceAllowFrom ?? false,
});
}