diff --git a/extensions/bluebubbles/src/setup-surface.test.ts b/extensions/bluebubbles/src/setup-surface.test.ts index f731ee8469a..d941705dd56 100644 --- a/extensions/bluebubbles/src/setup-surface.test.ts +++ b/extensions/bluebubbles/src/setup-surface.test.ts @@ -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>[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>[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>[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>[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; diff --git a/extensions/feishu/src/setup-surface.test.ts b/extensions/feishu/src/setup-surface.test.ts index 33ab8ad7989..bf93b2f9d2f 100644 --- a/extensions/feishu/src/setup-surface.test.ts +++ b/extensions/feishu/src/setup-surface.test.ts @@ -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(); }); diff --git a/extensions/googlechat/src/setup-surface.test.ts b/extensions/googlechat/src/setup-surface.test.ts index 9570bb1848b..66260de8e26 100644 --- a/extensions/googlechat/src/setup-surface.test.ts +++ b/extensions/googlechat/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/irc/src/setup-surface.test.ts b/extensions/irc/src/setup-surface.test.ts index 56b9687f593..8ac569678b1 100644 --- a/extensions/irc/src/setup-surface.test.ts +++ b/extensions/irc/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index b613a16bba4..c13f1dda09c 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/matrix/src/matrix/client/file-sync-store.test.ts b/extensions/matrix/src/matrix/client/file-sync-store.test.ts index 56c88433d9c..5d064e30c0f 100644 --- a/extensions/matrix/src/matrix/client/file-sync-store.test.ts +++ b/extensions/matrix/src/matrix/client/file-sync-store.test.ts @@ -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, diff --git a/extensions/nostr/src/setup-surface.test.ts b/extensions/nostr/src/setup-surface.test.ts index c1cd3802c5e..a4769960b7a 100644 --- a/extensions/nostr/src/setup-surface.test.ts +++ b/extensions/nostr/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/synology-chat/src/setup-surface.test.ts b/extensions/synology-chat/src/setup-surface.test.ts index 5b30c747813..5adbe42a451 100644 --- a/extensions/synology-chat/src/setup-surface.test.ts +++ b/extensions/synology-chat/src/setup-surface.test.ts @@ -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, }); diff --git a/extensions/tlon/src/setup-surface.test.ts b/extensions/tlon/src/setup-surface.test.ts index a193f9ca800..c62eaf3e05b 100644 --- a/extensions/tlon/src/setup-surface.test.ts +++ b/extensions/tlon/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/whatsapp/src/setup-surface.test.ts b/extensions/whatsapp/src/setup-surface.test.ts index f1e05360fb5..f3c2dc154c9 100644 --- a/extensions/whatsapp/src/setup-surface.test.ts +++ b/extensions/whatsapp/src/setup-surface.test.ts @@ -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; + harness: ReturnType; cfg?: Parameters[0]["cfg"]; runtime?: RuntimeEnv; options?: Parameters[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"], }); diff --git a/extensions/zalo/src/setup-surface.test.ts b/extensions/zalo/src/setup-surface.test.ts index 16e6e46d8b8..2862d0eebfd 100644 --- a/extensions/zalo/src/setup-surface.test.ts +++ b/extensions/zalo/src/setup-surface.test.ts @@ -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"); diff --git a/extensions/zalouser/src/setup-surface.test.ts b/extensions/zalouser/src/setup-surface.test.ts index 14030a60936..2cfd00f13a0 100644 --- a/extensions/zalouser/src/setup-surface.test.ts +++ b/extensions/zalouser/src/setup-surface.test.ts @@ -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; + options?: Record; + 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["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["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["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["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); diff --git a/test/helpers/extensions/setup-wizard.ts b/test/helpers/extensions/setup-wizard.ts index 109394ee886..3d9fdffea4e 100644 --- a/test/helpers/extensions/setup-wizard.ts +++ b/test/helpers/extensions/setup-wizard.ts @@ -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 = {} ...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[0]; +type SetupWizardPlugin = SetupWizardAdapterParams["plugin"]; +type SetupWizard = NonNullable; + +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, + TAccountOverrides extends Record, + TRuntime, + TResult, +>(params: { + configure: (args: { + cfg: TCfg; + runtime: TRuntime; + prompter: WizardPrompter; + options: TOptions; + accountOverrides: TAccountOverrides; + shouldPromptAccountIds: boolean; + forceAllowFrom: boolean; + }) => Promise; + cfg?: TCfg; + runtime?: TRuntime; + prompter: WizardPrompter; + options?: TOptions; + accountOverrides?: TAccountOverrides; + shouldPromptAccountIds?: boolean; + forceAllowFrom?: boolean; +}): Promise { + 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, + }); +}