From 30ad059da88ceebafa3fbf38cef6c77c78fbdafd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 00:15:51 +0000 Subject: [PATCH] refactor(test): dedupe setup wizard helpers --- .../bluebubbles/src/setup-surface.test.ts | 6 +- .../src/monitor/provider.allowlist.test.ts | 6 +- ...tor.broadcast.reply-once.lifecycle.test.ts | 4 +- .../feishu/src/monitor.reaction.test.ts | 11 +- extensions/feishu/src/monitor.startup.test.ts | 4 +- extensions/feishu/src/setup-status.test.ts | 9 +- extensions/feishu/src/setup-surface.test.ts | 17 +- .../googlechat/src/setup-surface.test.ts | 6 +- extensions/irc/src/setup-surface.test.ts | 4 +- extensions/line/src/setup-surface.test.ts | 26 +-- extensions/matrix/src/channel.resolve.test.ts | 4 +- .../matrix/src/onboarding.resolve.test.ts | 4 +- extensions/matrix/src/onboarding.test.ts | 12 +- extensions/nostr/src/setup-surface.test.ts | 6 +- .../synology-chat/src/setup-surface.test.ts | 12 +- extensions/telegram/src/setup-surface.test.ts | 25 ++- extensions/tlon/src/setup-surface.test.ts | 6 +- extensions/whatsapp/src/setup-surface.test.ts | 15 +- extensions/zalo/src/setup-status.test.ts | 9 +- extensions/zalo/src/setup-surface.test.ts | 6 +- extensions/zalouser/src/channel.setup.test.ts | 9 +- extensions/zalouser/src/setup-surface.test.ts | 6 +- .../plugins/setup-wizard-helpers.test.ts | 19 +- .../plugins/setup-wizard-proxy.test.ts | 68 ++----- src/plugin-sdk/channel-setup.test.ts | 9 +- test/helpers/extensions/runtime-env.ts | 16 +- test/helpers/extensions/setup-wizard.ts | 189 +++++++++++++++++- 27 files changed, 322 insertions(+), 186 deletions(-) diff --git a/extensions/bluebubbles/src/setup-surface.test.ts b/extensions/bluebubbles/src/setup-surface.test.ts index d941705dd56..3e39e7dfb79 100644 --- a/extensions/bluebubbles/src/setup-surface.test.ts +++ b/extensions/bluebubbles/src/setup-surface.test.ts @@ -1,7 +1,7 @@ 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 { + createSetupWizardAdapter, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, @@ -31,8 +31,8 @@ async function createBlueBubblesConfigureAdapter() { }).config.allowFrom ?? [], }, setup: blueBubblesSetupAdapter, - } as Parameters[0]["plugin"]; - return buildChannelSetupWizardAdapterFromSetupWizard({ + } as Parameters[0]["plugin"]; + return createSetupWizardAdapter({ plugin, wizard: blueBubblesSetupWizard, }); diff --git a/extensions/discord/src/monitor/provider.allowlist.test.ts b/extensions/discord/src/monitor/provider.allowlist.test.ts index d478e39abcf..70129615bea 100644 --- a/extensions/discord/src/monitor/provider.allowlist.test.ts +++ b/extensions/discord/src/monitor/provider.allowlist.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../../../../src/runtime.js"; -import { createRuntimeEnv } from "../../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingTypedRuntimeEnv } from "../../../../test/helpers/extensions/runtime-env.js"; const { resolveDiscordChannelAllowlistMock, resolveDiscordUserAllowlistMock } = vi.hoisted(() => ({ resolveDiscordChannelAllowlistMock: vi.fn( @@ -36,7 +36,7 @@ import { resolveDiscordAllowlistConfig } from "./provider.allowlist.js"; describe("resolveDiscordAllowlistConfig", () => { it("canonicalizes resolved user names to ids in runtime config", async () => { - const runtime = createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv; + const runtime = createNonExitingTypedRuntimeEnv(); const result = await resolveDiscordAllowlistConfig({ token: "token", allowFrom: ["Alice", "111", "*"], @@ -70,7 +70,7 @@ describe("resolveDiscordAllowlistConfig", () => { channelName: "missing-room", }, ]); - const runtime = createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv; + const runtime = createNonExitingTypedRuntimeEnv(); await resolveDiscordAllowlistConfig({ token: "token", diff --git a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts index 04554eebe53..8fd2614aa95 100644 --- a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; import { monitorSingleAccount } from "./monitor.account.js"; import { setFeishuRuntime } from "./runtime.js"; @@ -183,7 +183,7 @@ async function setupLifecycleMonitor(accountId: "account-A" | "account-B") { }); createEventDispatcherMock.mockReturnValueOnce({ register }); - const runtime = createRuntimeEnv({ throwOnExit: false }); + const runtime = createNonExitingRuntimeEnv(); runtimesByAccount.set(accountId, runtime); await monitorSingleAccount({ diff --git a/extensions/feishu/src/monitor.reaction.test.ts b/extensions/feishu/src/monitor.reaction.test.ts index c13dfa38360..238d8a060db 100644 --- a/extensions/feishu/src/monitor.reaction.test.ts +++ b/extensions/feishu/src/monitor.reaction.test.ts @@ -5,7 +5,10 @@ import { resolveInboundDebounceMs, } from "../../../src/auto-reply/inbound-debounce.js"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { + createNonExitingTypedRuntimeEnv, + createRuntimeEnv, +} from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js"; import * as dedup from "./dedup.js"; @@ -173,7 +176,7 @@ async function setupDebounceMonitor(params?: { await monitorSingleAccount({ cfg: buildDebounceConfig(), account: buildDebounceAccount(), - runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), botOpenIdSource: { kind: "prefetched", botOpenId: params?.botOpenId ?? "ou_bot", @@ -449,7 +452,7 @@ describe("monitorSingleAccount lifecycle", () => { await monitorSingleAccount({ cfg: buildDebounceConfig(), account: buildDebounceAccount(), - runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot", @@ -486,7 +489,7 @@ describe("monitorSingleAccount lifecycle", () => { monitorSingleAccount({ cfg: buildDebounceConfig(), account: buildDebounceAccount(), - runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot", diff --git a/extensions/feishu/src/monitor.startup.test.ts b/extensions/feishu/src/monitor.startup.test.ts index 1ee572cf890..cb85ad859ad 100644 --- a/extensions/feishu/src/monitor.startup.test.ts +++ b/extensions/feishu/src/monitor.startup.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig } from "../runtime-api.js"; import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js"; @@ -135,7 +135,7 @@ describe("Feishu monitor startup preflight", () => { }); const abortController = new AbortController(); - const runtime = createRuntimeEnv({ throwOnExit: false }); + const runtime = createNonExitingRuntimeEnv(); const monitorPromise = monitorFeishuProvider({ config: buildMultiAccountWebsocketConfig(["alpha", "beta"]), runtime, diff --git a/extensions/feishu/src/setup-status.test.ts b/extensions/feishu/src/setup-status.test.ts index 6f1a877814e..3287b620123 100644 --- a/extensions/feishu/src/setup-status.test.ts +++ b/extensions/feishu/src/setup-status.test.ts @@ -1,16 +1,13 @@ import { describe, expect, it } from "vitest"; -import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; +import { createPluginSetupWizardStatus } from "../../../test/helpers/extensions/setup-wizard.js"; import type { OpenClawConfig } from "../runtime-api.js"; import { feishuPlugin } from "./channel.js"; -const feishuConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: feishuPlugin, - wizard: feishuPlugin.setupWizard!, -}); +const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin); describe("feishu setup wizard status", () => { it("treats SecretRef appSecret as configured when appId is present", async () => { - const status = await feishuConfigureAdapter.getStatus({ + const status = await feishuGetStatus({ cfg: { channels: { feishu: { diff --git a/extensions/feishu/src/setup-surface.test.ts b/extensions/feishu/src/setup-surface.test.ts index bf93b2f9d2f..91ab0614bb1 100644 --- a/extensions/feishu/src/setup-surface.test.ts +++ b/extensions/feishu/src/setup-surface.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from "vitest"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingTypedRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, + createPluginSetupWizardStatus, createTestWizardPrompter, runSetupWizardConfigure, } from "../../../test/helpers/extensions/setup-wizard.js"; @@ -41,7 +42,7 @@ async function withEnvVars(values: Record, run: () = } async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: string }) { - return await feishuConfigureAdapter.getStatus({ + return await feishuGetStatus({ cfg: { channels: { feishu: { @@ -54,7 +55,9 @@ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: st }); } -const feishuConfigureAdapter = createPluginSetupWizardAdapter(feishuPlugin); +const feishuConfigure = createPluginSetupWizardConfigure(feishuPlugin); +const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin); +type FeishuConfigureRuntime = Parameters[0]["runtime"]; describe("feishu setup wizard", () => { it("does not throw when config appId/appSecret are SecretRef objects", async () => { @@ -73,7 +76,7 @@ describe("feishu setup wizard", () => { await expect( runSetupWizardConfigure({ - configure: feishuConfigureAdapter.configure, + configure: feishuConfigure, cfg: { channels: { feishu: { @@ -83,7 +86,7 @@ describe("feishu setup wizard", () => { }, } as never, prompter, - runtime: createRuntimeEnv({ throwOnExit: false }) as never, + runtime: createNonExitingTypedRuntimeEnv(), }), ).resolves.toBeTruthy(); }); @@ -91,7 +94,7 @@ describe("feishu setup wizard", () => { describe("feishu setup wizard status", () => { it("does not fallback to top-level appId when account explicitly sets empty appId", async () => { - const status = await feishuConfigureAdapter.getStatus({ + const status = await feishuGetStatus({ cfg: { channels: { feishu: { diff --git a/extensions/googlechat/src/setup-surface.test.ts b/extensions/googlechat/src/setup-surface.test.ts index 66260de8e26..750bef39a71 100644 --- a/extensions/googlechat/src/setup-surface.test.ts +++ b/extensions/googlechat/src/setup-surface.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, @@ -8,7 +8,7 @@ import { import type { OpenClawConfig } from "../runtime-api.js"; import { googlechatPlugin } from "./channel.js"; -const googlechatConfigureAdapter = createPluginSetupWizardAdapter(googlechatPlugin); +const googlechatConfigure = createPluginSetupWizardConfigure(googlechatPlugin); describe("googlechat setup wizard", () => { it("configures service-account auth and webhook audience", async () => { @@ -25,7 +25,7 @@ describe("googlechat setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: googlechatConfigureAdapter.configure, + configure: googlechatConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, diff --git a/extensions/irc/src/setup-surface.test.ts b/extensions/irc/src/setup-surface.test.ts index 8ac569678b1..4ae2a0a5476 100644 --- a/extensions/irc/src/setup-surface.test.ts +++ b/extensions/irc/src/setup-surface.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { createPluginSetupWizardAdapter, createTestWizardPrompter, + promptSetupWizardAllowFrom, runSetupWizardConfigure, type WizardPrompter, } from "../../../test/helpers/extensions/setup-wizard.js"; @@ -92,7 +93,8 @@ describe("irc setup wizard", () => { }, }; - const updated = (await promptAllowFrom?.({ + const updated = (await promptSetupWizardAllowFrom({ + promptAllowFrom, cfg, prompter, accountId: "work", diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index c13f1dda09c..0eeb85f9eb5 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -1,32 +1,14 @@ import { describe, expect, it, vi } from "vitest"; -import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; -import { - listLineAccountIds, - resolveDefaultLineAccountId, - resolveLineAccount, -} from "../../../src/line/accounts.js"; import { + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, } from "../../../test/helpers/extensions/setup-wizard.js"; import type { OpenClawConfig } from "../api.js"; -import { lineSetupAdapter, lineSetupWizard } from "./setup-surface.js"; +import { linePlugin } from "./channel.js"; -const lineConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: { - id: "line", - meta: { label: "LINE" }, - config: { - listAccountIds: listLineAccountIds, - defaultAccountId: resolveDefaultLineAccountId, - resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => - resolveLineAccount({ cfg, accountId: accountId ?? undefined }).config.allowFrom, - }, - setup: lineSetupAdapter, - } as Parameters[0]["plugin"], - wizard: lineSetupWizard, -}); +const lineConfigure = createPluginSetupWizardConfigure(linePlugin); describe("line setup wizard", () => { it("configures token and secret for the default account", async () => { @@ -43,7 +25,7 @@ describe("line setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: lineConfigureAdapter.configure, + configure: lineConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, diff --git a/extensions/matrix/src/channel.resolve.test.ts b/extensions/matrix/src/channel.resolve.test.ts index b39c754c07e..3d8e5a24154 100644 --- a/extensions/matrix/src/channel.resolve.test.ts +++ b/extensions/matrix/src/channel.resolve.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; const resolveMatrixTargetsMock = vi.hoisted(() => vi.fn(async () => [])); @@ -20,7 +20,7 @@ describe("matrix resolver adapter", () => { accountId: "ops", inputs: ["Alice"], kind: "user", - runtime: createRuntimeEnv({ throwOnExit: false }), + runtime: createNonExitingRuntimeEnv(), }); expect(resolveMatrixTargetsMock).toHaveBeenCalledWith({ diff --git a/extensions/matrix/src/onboarding.resolve.test.ts b/extensions/matrix/src/onboarding.resolve.test.ts index 0b9ef1f645c..dffed4a5fd1 100644 --- a/extensions/matrix/src/onboarding.resolve.test.ts +++ b/extensions/matrix/src/onboarding.resolve.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingTypedRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { RuntimeEnv, WizardPrompter } from "../runtime-api.js"; import { matrixOnboardingAdapter } from "./onboarding.js"; import { installMatrixTestRuntime } from "./test-runtime.js"; @@ -83,7 +83,7 @@ describe("matrix onboarding account-scoped resolution", () => { }, }, } as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, diff --git a/extensions/matrix/src/onboarding.test.ts b/extensions/matrix/src/onboarding.test.ts index f49596bcb53..7860ef93245 100644 --- a/extensions/matrix/src/onboarding.test.ts +++ b/extensions/matrix/src/onboarding.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; +import { createNonExitingTypedRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { RuntimeEnv, WizardPrompter } from "../runtime-api.js"; import { matrixOnboardingAdapter } from "./onboarding.js"; import { installMatrixTestRuntime } from "./test-runtime.js"; @@ -82,7 +82,7 @@ describe("matrix onboarding", () => { }, }, } as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, @@ -152,7 +152,7 @@ describe("matrix onboarding", () => { }, }, } as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, @@ -199,7 +199,7 @@ describe("matrix onboarding", () => { await expect( matrixOnboardingAdapter.configureInteractive!({ cfg: { channels: {} } as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, @@ -253,7 +253,7 @@ describe("matrix onboarding", () => { const result = await matrixOnboardingAdapter.configureInteractive!({ cfg: {} as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, @@ -366,7 +366,7 @@ describe("matrix onboarding", () => { }, }, } as CoreConfig, - runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv, + runtime: createNonExitingTypedRuntimeEnv(), prompter, options: undefined, accountOverrides: {}, diff --git a/extensions/nostr/src/setup-surface.test.ts b/extensions/nostr/src/setup-surface.test.ts index a4769960b7a..4f3b521c8fd 100644 --- a/extensions/nostr/src/setup-surface.test.ts +++ b/extensions/nostr/src/setup-surface.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, @@ -8,7 +8,7 @@ import { import type { OpenClawConfig } from "../runtime-api.js"; import { nostrPlugin } from "./channel.js"; -const nostrConfigureAdapter = createPluginSetupWizardAdapter(nostrPlugin); +const nostrConfigure = createPluginSetupWizardConfigure(nostrPlugin); describe("nostr setup wizard", () => { it("configures a private key and relay URLs", async () => { @@ -25,7 +25,7 @@ describe("nostr setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: nostrConfigureAdapter.configure, + configure: nostrConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, diff --git a/extensions/synology-chat/src/setup-surface.test.ts b/extensions/synology-chat/src/setup-surface.test.ts index 5adbe42a451..49b16a17dc2 100644 --- a/extensions/synology-chat/src/setup-surface.test.ts +++ b/extensions/synology-chat/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 type { OpenClawConfig } from "../../../src/config/config.js"; import { + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, } from "../../../test/helpers/extensions/setup-wizard.js"; import { synologyChatPlugin } from "./channel.js"; -import { synologyChatSetupWizard } from "./setup-surface.js"; -const synologyChatConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: synologyChatPlugin, - wizard: synologyChatSetupWizard, -}); +const synologyChatConfigure = createPluginSetupWizardConfigure(synologyChatPlugin); describe("synology-chat setup wizard", () => { it("configures token and incoming webhook for the default account", async () => { @@ -32,7 +28,7 @@ describe("synology-chat setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: synologyChatConfigureAdapter.configure, + configure: synologyChatConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, @@ -66,7 +62,7 @@ describe("synology-chat setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: synologyChatConfigureAdapter.configure, + configure: synologyChatConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, diff --git a/extensions/telegram/src/setup-surface.test.ts b/extensions/telegram/src/setup-surface.test.ts index 66de1d61bf0..722f5cedca2 100644 --- a/extensions/telegram/src/setup-surface.test.ts +++ b/extensions/telegram/src/setup-surface.test.ts @@ -1,34 +1,33 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; +import { + createTestWizardPrompter, + runSetupWizardFinalize, + runSetupWizardPrepare, +} from "../../../test/helpers/extensions/setup-wizard.js"; import { telegramSetupWizard } from "./setup-surface.js"; async function runPrepare(cfg: OpenClawConfig, accountId: string) { - return await telegramSetupWizard.prepare?.({ + return await runSetupWizardPrepare({ + prepare: telegramSetupWizard.prepare, cfg, accountId, - credentialValues: {}, - runtime: {} as never, - prompter: {} as never, options: {}, }); } async function runFinalize(cfg: OpenClawConfig, accountId: string) { - const prompter = { - note: vi.fn(async () => undefined), - }; + const note = vi.fn(async () => undefined); - await telegramSetupWizard.finalize?.({ + await runSetupWizardFinalize({ + finalize: telegramSetupWizard.finalize, cfg, accountId, - credentialValues: {}, - runtime: {} as never, - prompter: prompter as never, - forceAllowFrom: false, + prompter: createTestWizardPrompter({ note }), }); - return prompter.note; + return note; } function expectPreparedResult( diff --git a/extensions/tlon/src/setup-surface.test.ts b/extensions/tlon/src/setup-surface.test.ts index c62eaf3e05b..5257f0aa6ce 100644 --- a/extensions/tlon/src/setup-surface.test.ts +++ b/extensions/tlon/src/setup-surface.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, @@ -8,7 +8,7 @@ import { import type { OpenClawConfig } from "../api.js"; import { tlonPlugin } from "./channel.js"; -const tlonConfigureAdapter = createPluginSetupWizardAdapter(tlonPlugin); +const tlonConfigure = createPluginSetupWizardConfigure(tlonPlugin); describe("tlon setup wizard", () => { it("configures ship, auth, and discovery settings", async () => { @@ -46,7 +46,7 @@ describe("tlon setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: tlonConfigureAdapter.configure, + configure: tlonConfigure, cfg: {} as OpenClawConfig, prompter, options: {}, diff --git a/extensions/whatsapp/src/setup-surface.test.ts b/extensions/whatsapp/src/setup-surface.test.ts index f3c2dc154c9..697473033a6 100644 --- a/extensions/whatsapp/src/setup-surface.test.ts +++ b/extensions/whatsapp/src/setup-surface.test.ts @@ -1,8 +1,8 @@ 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 { + createPluginSetupWizardConfigure, createQueuedWizardPrompter, runSetupWizardConfigure, } from "../../../test/helpers/extensions/setup-wizard.js"; @@ -43,22 +43,19 @@ function createRuntime(): RuntimeEnv { } as unknown as RuntimeEnv; } -const whatsappConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: whatsappPlugin, - wizard: whatsappPlugin.setupWizard!, -}); +const whatsappConfigure = createPluginSetupWizardConfigure(whatsappPlugin); async function runConfigureWithHarness(params: { harness: ReturnType; - cfg?: Parameters[0]["cfg"]; + cfg?: Parameters[0]["cfg"]; runtime?: RuntimeEnv; - options?: Parameters[0]["options"]; - accountOverrides?: Parameters[0]["accountOverrides"]; + options?: Parameters[0]["options"]; + accountOverrides?: Parameters[0]["accountOverrides"]; shouldPromptAccountIds?: boolean; forceAllowFrom?: boolean; }) { return await runSetupWizardConfigure({ - configure: whatsappConfigureAdapter.configure, + configure: whatsappConfigure, cfg: params.cfg ?? {}, runtime: params.runtime ?? createRuntime(), prompter: params.harness.prompter, diff --git a/extensions/zalo/src/setup-status.test.ts b/extensions/zalo/src/setup-status.test.ts index 738b9436f14..93dd835dd7f 100644 --- a/extensions/zalo/src/setup-status.test.ts +++ b/extensions/zalo/src/setup-status.test.ts @@ -1,16 +1,13 @@ import { describe, expect, it } from "vitest"; -import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; +import { createPluginSetupWizardStatus } from "../../../test/helpers/extensions/setup-wizard.js"; import type { OpenClawConfig } from "../runtime-api.js"; import { zaloPlugin } from "./channel.js"; -const zaloConfigureAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: zaloPlugin, - wizard: zaloPlugin.setupWizard!, -}); +const zaloGetStatus = createPluginSetupWizardStatus(zaloPlugin); describe("zalo setup wizard status", () => { it("treats SecretRef botToken as configured", async () => { - const status = await zaloConfigureAdapter.getStatus({ + const status = await zaloGetStatus({ cfg: { channels: { zalo: { diff --git a/extensions/zalo/src/setup-surface.test.ts b/extensions/zalo/src/setup-surface.test.ts index 2862d0eebfd..ab5c1572e6c 100644 --- a/extensions/zalo/src/setup-surface.test.ts +++ b/extensions/zalo/src/setup-surface.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, type WizardPrompter, @@ -8,7 +8,7 @@ import { import type { OpenClawConfig } from "../runtime-api.js"; import { zaloPlugin } from "./channel.js"; -const zaloConfigureAdapter = createPluginSetupWizardAdapter(zaloPlugin); +const zaloConfigure = createPluginSetupWizardConfigure(zaloPlugin); describe("zalo setup wizard", () => { it("configures a polling token flow", async () => { @@ -29,7 +29,7 @@ describe("zalo setup wizard", () => { }); const result = await runSetupWizardConfigure({ - configure: zaloConfigureAdapter.configure, + configure: zaloConfigure, cfg: {} as OpenClawConfig, prompter, options: { secretInputMode: "plaintext" as const }, diff --git a/extensions/zalouser/src/channel.setup.test.ts b/extensions/zalouser/src/channel.setup.test.ts index 75aebe5e6be..915b236dcb2 100644 --- a/extensions/zalouser/src/channel.setup.test.ts +++ b/extensions/zalouser/src/channel.setup.test.ts @@ -2,15 +2,12 @@ import { mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { buildChannelSetupWizardAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; import { withEnvAsync } from "../../../test/helpers/extensions/env.js"; +import { createPluginSetupWizardStatus } from "../../../test/helpers/extensions/setup-wizard.js"; import "./zalo-js.test-mocks.js"; import { zalouserSetupPlugin } from "./channel.setup.js"; -const zalouserSetupAdapter = buildChannelSetupWizardAdapterFromSetupWizard({ - plugin: zalouserSetupPlugin, - wizard: zalouserSetupPlugin.setupWizard!, -}); +const zalouserSetupGetStatus = createPluginSetupWizardStatus(zalouserSetupPlugin); describe("zalouser setup plugin", () => { it("builds setup status without an initialized runtime", async () => { @@ -19,7 +16,7 @@ describe("zalouser setup plugin", () => { try { await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => { await expect( - zalouserSetupAdapter.getStatus({ + zalouserSetupGetStatus({ cfg: {}, accountOverrides: {}, }), diff --git a/extensions/zalouser/src/setup-surface.test.ts b/extensions/zalouser/src/setup-surface.test.ts index 2cfd00f13a0..222090d1a34 100644 --- a/extensions/zalouser/src/setup-surface.test.ts +++ b/extensions/zalouser/src/setup-surface.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { - createPluginSetupWizardAdapter, + createPluginSetupWizardConfigure, createTestWizardPrompter, runSetupWizardConfigure, } from "../../../test/helpers/extensions/setup-wizard.js"; @@ -8,7 +8,7 @@ import type { OpenClawConfig } from "../runtime-api.js"; import "./zalo-js.test-mocks.js"; import { zalouserPlugin } from "./channel.js"; -const zalouserConfigureAdapter = createPluginSetupWizardAdapter(zalouserPlugin); +const zalouserConfigure = createPluginSetupWizardConfigure(zalouserPlugin); async function runSetup(params: { cfg?: OpenClawConfig; @@ -17,7 +17,7 @@ async function runSetup(params: { forceAllowFrom?: boolean; }) { return await runSetupWizardConfigure({ - configure: zalouserConfigureAdapter.configure, + configure: zalouserConfigure, cfg: params.cfg as OpenClawConfig | undefined, prompter: params.prompter, options: params.options, diff --git a/src/channels/plugins/setup-wizard-helpers.test.ts b/src/channels/plugins/setup-wizard-helpers.test.ts index bcdc09917fb..6084c5a0f44 100644 --- a/src/channels/plugins/setup-wizard-helpers.test.ts +++ b/src/channels/plugins/setup-wizard-helpers.test.ts @@ -1,4 +1,8 @@ import { describe, expect, it, vi } from "vitest"; +import { + resolveSetupWizardAllowFromEntries, + resolveSetupWizardGroupAllowlist, +} from "../../../test/helpers/extensions/setup-wizard.js"; import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { @@ -1456,10 +1460,9 @@ describe("createAccountScopedAllowFromSection", () => { expect(section.credentialInputKey).toBe("token"); await expect( - section.resolveEntries({ - cfg: {}, + resolveSetupWizardAllowFromEntries({ + resolveEntries: section.resolveEntries, accountId: DEFAULT_ACCOUNT_ID, - credentialValues: {}, entries: ["alice"], }), ).resolves.toEqual([{ input: "alice", resolved: true, id: "ALICE" }]); @@ -1496,10 +1499,9 @@ describe("createAllowFromSection", () => { expect(section.helpTitle).toBe("LINE allowlist"); await expect( - section.resolveEntries({ - cfg: {}, + resolveSetupWizardAllowFromEntries({ + resolveEntries: section.resolveEntries, accountId: DEFAULT_ACCOUNT_ID, - credentialValues: {}, entries: ["u1"], }), ).resolves.toEqual([{ input: "u1", resolved: true, id: "U1" }]); @@ -1546,10 +1548,9 @@ describe("createAccountScopedGroupAccessSection", () => { expect(policyNext.channels?.slack?.groupPolicy).toBe("open"); await expect( - section.resolveAllowlist?.({ - cfg: {}, + resolveSetupWizardGroupAllowlist({ + resolveAllowlist: section.resolveAllowlist, accountId: DEFAULT_ACCOUNT_ID, - credentialValues: {}, entries: ["general"], prompter, }), diff --git a/src/channels/plugins/setup-wizard-proxy.test.ts b/src/channels/plugins/setup-wizard-proxy.test.ts index f4b3db0d725..480ecf5ceb3 100644 --- a/src/channels/plugins/setup-wizard-proxy.test.ts +++ b/src/channels/plugins/setup-wizard-proxy.test.ts @@ -1,4 +1,11 @@ import { describe, expect, it, vi } from "vitest"; +import { + promptSetupWizardAllowFrom, + resolveSetupWizardAllowFromEntries, + resolveSetupWizardGroupAllowlist, + runSetupWizardFinalize, + runSetupWizardPrepare, +} from "../../../test/helpers/extensions/setup-wizard.js"; import { createAllowlistSetupWizardProxy, createDelegatedFinalize, @@ -46,15 +53,7 @@ describe("createDelegatedPrepare", () => { const prepare = createDelegatedPrepare(loadWizard); - expect( - await prepare({ - cfg: {}, - accountId: "default", - credentialValues: {}, - runtime: {} as never, - prompter: {} as never, - }), - ).toEqual({ + expect(await runSetupWizardPrepare({ prepare })).toEqual({ cfg: { channels: { demo: { enabled: true }, @@ -88,16 +87,7 @@ describe("createDelegatedFinalize", () => { const finalize = createDelegatedFinalize(loadWizard); - expect( - await finalize({ - cfg: {}, - accountId: "default", - credentialValues: {}, - runtime: {} as never, - prompter: {} as never, - forceAllowFrom: true, - }), - ).toEqual({ + expect(await runSetupWizardFinalize({ finalize, forceAllowFrom: true })).toEqual({ cfg: { channels: { demo: { forceAllowFrom: true }, @@ -159,27 +149,18 @@ describe("createAllowlistSetupWizardProxy", () => { }); expect( - await wizard.dmPolicy?.promptAllowFrom?.({ - cfg: {}, - prompter: {} as never, - accountId: "default", - }), + await promptSetupWizardAllowFrom({ promptAllowFrom: wizard.dmPolicy?.promptAllowFrom }), ).toEqual({}); expect( - await wizard.allowFrom?.resolveEntries({ - cfg: {}, - accountId: "default", - credentialValues: {}, + await resolveSetupWizardAllowFromEntries({ + resolveEntries: wizard.allowFrom?.resolveEntries, entries: ["alice"], }), ).toEqual([{ input: "alice", resolved: false, id: null }]); expect( - await wizard.groupAccess?.resolveAllowlist?.({ - cfg: {}, - accountId: "default", - credentialValues: {}, + await resolveSetupWizardGroupAllowlist({ + resolveAllowlist: wizard.groupAccess?.resolveAllowlist, entries: ["general"], - prompter: {} as never, }), ).toEqual([{ input: "general" }]); }); @@ -231,31 +212,14 @@ describe("createDelegatedSetupWizardProxy", () => { expect(await wizard.status.resolveStatusLines?.({ cfg: {}, configured: false })).toEqual([ "line", ]); - expect( - await wizard.prepare?.({ - cfg: {}, - accountId: "default", - credentialValues: {}, - runtime: {} as never, - prompter: {} as never, - }), - ).toEqual({ + expect(await runSetupWizardPrepare({ prepare: wizard.prepare })).toEqual({ cfg: { channels: { demo: { prepared: true }, }, }, }); - expect( - await wizard.finalize?.({ - cfg: {}, - accountId: "default", - credentialValues: {}, - runtime: {} as never, - prompter: {} as never, - forceAllowFrom: false, - }), - ).toEqual({ + expect(await runSetupWizardFinalize({ finalize: wizard.finalize })).toEqual({ cfg: { channels: { demo: { finalized: true }, diff --git a/src/plugin-sdk/channel-setup.test.ts b/src/plugin-sdk/channel-setup.test.ts index 3890dfc803d..546077fa1f5 100644 --- a/src/plugin-sdk/channel-setup.test.ts +++ b/src/plugin-sdk/channel-setup.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { runSetupWizardFinalize } from "../../test/helpers/extensions/setup-wizard.js"; import { createOptionalChannelSetupSurface } from "./channel-setup.js"; describe("createOptionalChannelSetupSurface", () => { @@ -21,17 +22,13 @@ describe("createOptionalChannelSetupSurface", () => { expect(setup.setupWizard.channel).toBe("example"); expect(setup.setupWizard.status.unconfiguredHint).toContain("/channels/example"); await expect( - setup.setupWizard.finalize?.({ - cfg: {}, - accountId: "default", - credentialValues: {}, + runSetupWizardFinalize({ + finalize: setup.setupWizard.finalize, runtime: { log: () => {}, error: () => {}, exit: async () => {}, }, - prompter: {} as never, - forceAllowFrom: false, }), ).rejects.toThrow("@openclaw/example"); }); diff --git a/test/helpers/extensions/runtime-env.ts b/test/helpers/extensions/runtime-env.ts index 45035976760..2ad7f32a718 100644 --- a/test/helpers/extensions/runtime-env.ts +++ b/test/helpers/extensions/runtime-env.ts @@ -1,7 +1,9 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/testing"; import { vi } from "vitest"; -export function createRuntimeEnv(options?: { throwOnExit?: boolean }): RuntimeEnv { +export function createRuntimeEnv(options?: { + throwOnExit?: boolean; +}): RuntimeEnv { const throwOnExit = options?.throwOnExit ?? true; return { log: vi.fn(), @@ -13,3 +15,15 @@ export function createRuntimeEnv(options?: { throwOnExit?: boolean }): RuntimeEn : vi.fn(), }; } + +export function createTypedRuntimeEnv(options?: { throwOnExit?: boolean }): TRuntime { + return createRuntimeEnv(options) as TRuntime; +} + +export function createNonExitingRuntimeEnv(): RuntimeEnv { + return createRuntimeEnv({ throwOnExit: false }); +} + +export function createNonExitingTypedRuntimeEnv(): TRuntime { + return createTypedRuntimeEnv({ throwOnExit: false }); +} diff --git a/test/helpers/extensions/setup-wizard.ts b/test/helpers/extensions/setup-wizard.ts index 3d9fdffea4e..e957f1ffc6b 100644 --- a/test/helpers/extensions/setup-wizard.ts +++ b/test/helpers/extensions/setup-wizard.ts @@ -75,6 +75,40 @@ export function createQueuedWizardPrompter(params?: { type SetupWizardAdapterParams = Parameters[0]; type SetupWizardPlugin = SetupWizardAdapterParams["plugin"]; type SetupWizard = NonNullable; +type SetupWizardCredentialValues = Record; + +function resolveSetupWizardAccountContext(params: { + cfg?: TCfg; + accountId?: string; + credentialValues?: SetupWizardCredentialValues; +}) { + return { + cfg: (params.cfg ?? {}) as TCfg, + accountId: params.accountId ?? "default", + credentialValues: params.credentialValues ?? {}, + }; +} + +function resolveSetupWizardRuntime(runtime?: TRuntime): TRuntime { + return (runtime ?? createRuntimeEnv({ throwOnExit: false })) as TRuntime; +} + +function resolveSetupWizardPrompter(prompter?: WizardPrompter): WizardPrompter { + return prompter ?? createTestWizardPrompter(); +} + +function resolveSetupWizardNotePrompter(prompter?: Pick) { + return ( + prompter ?? + ({ + note: vi.fn(async () => undefined), + } satisfies Pick) + ); +} + +export function createSetupWizardAdapter(params: SetupWizardAdapterParams) { + return buildChannelSetupWizardAdapterFromSetupWizard(params); +} export function createPluginSetupWizardAdapter< TPlugin extends SetupWizardPlugin & { setupWizard?: SetupWizard }, @@ -83,12 +117,24 @@ export function createPluginSetupWizardAdapter< if (!wizard) { throw new Error(`${plugin.id} is missing setupWizard`); } - return buildChannelSetupWizardAdapterFromSetupWizard({ + return createSetupWizardAdapter({ plugin, wizard, }); } +export function createPluginSetupWizardConfigure< + TPlugin extends SetupWizardPlugin & { setupWizard?: SetupWizard }, +>(plugin: TPlugin) { + return createPluginSetupWizardAdapter(plugin).configure; +} + +export function createPluginSetupWizardStatus< + TPlugin extends SetupWizardPlugin & { setupWizard?: SetupWizard }, +>(plugin: TPlugin) { + return createPluginSetupWizardAdapter(plugin).getStatus; +} + export async function runSetupWizardConfigure< TCfg, TOptions extends Record, @@ -123,3 +169,144 @@ export async function runSetupWizardConfigure< forceAllowFrom: params.forceAllowFrom ?? false, }); } + +export async function runSetupWizardPrepare< + TCfg, + TOptions extends Record, + TRuntime, + TResult, +>(params: { + prepare?: (args: { + cfg: TCfg; + accountId: string; + credentialValues: Record; + runtime: TRuntime; + prompter: WizardPrompter; + options?: TOptions; + }) => Promise | TResult; + cfg?: TCfg; + accountId?: string; + credentialValues?: Record; + runtime?: TRuntime; + prompter?: WizardPrompter; + options?: TOptions; +}): Promise { + const context = resolveSetupWizardAccountContext({ + cfg: params.cfg, + accountId: params.accountId, + credentialValues: params.credentialValues, + }); + return await params.prepare?.({ + ...context, + runtime: resolveSetupWizardRuntime(params.runtime), + prompter: resolveSetupWizardPrompter(params.prompter), + options: params.options, + }); +} + +export async function runSetupWizardFinalize< + TCfg, + TOptions extends Record, + TRuntime, + TResult, +>(params: { + finalize?: (args: { + cfg: TCfg; + accountId: string; + credentialValues: Record; + runtime: TRuntime; + prompter: WizardPrompter; + options?: TOptions; + forceAllowFrom: boolean; + }) => Promise | TResult; + cfg?: TCfg; + accountId?: string; + credentialValues?: Record; + runtime?: TRuntime; + prompter?: WizardPrompter; + options?: TOptions; + forceAllowFrom?: boolean; +}): Promise { + const context = resolveSetupWizardAccountContext({ + cfg: params.cfg, + accountId: params.accountId, + credentialValues: params.credentialValues, + }); + return await params.finalize?.({ + ...context, + runtime: resolveSetupWizardRuntime(params.runtime), + prompter: resolveSetupWizardPrompter(params.prompter), + options: params.options, + forceAllowFrom: params.forceAllowFrom ?? false, + }); +} + +export async function promptSetupWizardAllowFrom(params: { + promptAllowFrom?: (args: { + cfg: TCfg; + prompter: WizardPrompter; + accountId: string; + }) => Promise | TResult; + cfg?: TCfg; + prompter?: WizardPrompter; + accountId?: string; +}): Promise { + const context = resolveSetupWizardAccountContext({ + cfg: params.cfg, + accountId: params.accountId, + }); + return await params.promptAllowFrom?.({ + cfg: context.cfg, + accountId: context.accountId, + prompter: resolveSetupWizardPrompter(params.prompter), + }); +} + +export async function resolveSetupWizardAllowFromEntries(params: { + resolveEntries?: (args: { + cfg: TCfg; + accountId: string; + credentialValues: Record; + entries: string[]; + }) => Promise | TResult; + entries: string[]; + cfg?: TCfg; + accountId?: string; + credentialValues?: SetupWizardCredentialValues; +}): Promise { + const context = resolveSetupWizardAccountContext({ + cfg: params.cfg, + accountId: params.accountId, + credentialValues: params.credentialValues, + }); + return await params.resolveEntries?.({ + ...context, + entries: params.entries, + }); +} + +export async function resolveSetupWizardGroupAllowlist(params: { + resolveAllowlist?: (args: { + cfg: TCfg; + accountId: string; + credentialValues: Record; + entries: string[]; + prompter: Pick; + }) => Promise | TResult; + entries: string[]; + cfg?: TCfg; + accountId?: string; + credentialValues?: SetupWizardCredentialValues; + prompter?: Pick; +}): Promise { + const context = resolveSetupWizardAccountContext({ + cfg: params.cfg, + accountId: params.accountId, + credentialValues: params.credentialValues, + }); + return await params.resolveAllowlist?.({ + ...context, + entries: params.entries, + prompter: resolveSetupWizardNotePrompter(params.prompter), + }); +}