refactor(test): dedupe setup wizard helpers

This commit is contained in:
Peter Steinberger
2026-03-22 00:15:51 +00:00
parent 85722d4cf2
commit 30ad059da8
27 changed files with 322 additions and 186 deletions

View File

@@ -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<typeof buildChannelSetupWizardAdapterFromSetupWizard>[0]["plugin"];
return buildChannelSetupWizardAdapterFromSetupWizard({
} as Parameters<typeof createSetupWizardAdapter>[0]["plugin"];
return createSetupWizardAdapter({
plugin,
wizard: blueBubblesSetupWizard,
});

View File

@@ -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<RuntimeEnv>();
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<RuntimeEnv>();
await resolveDiscordAllowlistConfig({
token: "token",

View File

@@ -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({

View File

@@ -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<RuntimeEnv>(),
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<RuntimeEnv>(),
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<RuntimeEnv>(),
botOpenIdSource: {
kind: "prefetched",
botOpenId: "ou_bot",

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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<string, string | undefined>, 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<typeof feishuConfigure>[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<FeishuConfigureRuntime>(),
}),
).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: {

View File

@@ -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: {},

View File

@@ -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",

View File

@@ -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<typeof buildChannelSetupWizardAdapterFromSetupWizard>[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: {},

View File

@@ -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({

View File

@@ -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<RuntimeEnv>(),
prompter,
options: undefined,
accountOverrides: {},

View File

@@ -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<RuntimeEnv>(),
prompter,
options: undefined,
accountOverrides: {},
@@ -152,7 +152,7 @@ describe("matrix onboarding", () => {
},
},
} as CoreConfig,
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
runtime: createNonExitingTypedRuntimeEnv<RuntimeEnv>(),
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<RuntimeEnv>(),
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<RuntimeEnv>(),
prompter,
options: undefined,
accountOverrides: {},
@@ -366,7 +366,7 @@ describe("matrix onboarding", () => {
},
},
} as CoreConfig,
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
runtime: createNonExitingTypedRuntimeEnv<RuntimeEnv>(),
prompter,
options: undefined,
accountOverrides: {},

View File

@@ -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: {},

View File

@@ -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: {},

View File

@@ -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(

View File

@@ -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: {},

View File

@@ -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<typeof createQueuedWizardPrompter>;
cfg?: Parameters<typeof whatsappConfigureAdapter.configure>[0]["cfg"];
cfg?: Parameters<typeof whatsappConfigure>[0]["cfg"];
runtime?: RuntimeEnv;
options?: Parameters<typeof whatsappConfigureAdapter.configure>[0]["options"];
accountOverrides?: Parameters<typeof whatsappConfigureAdapter.configure>[0]["accountOverrides"];
options?: Parameters<typeof whatsappConfigure>[0]["options"];
accountOverrides?: Parameters<typeof whatsappConfigure>[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,

View File

@@ -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: {

View File

@@ -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 },

View File

@@ -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: {},
}),

View File

@@ -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,

View File

@@ -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,
}),

View File

@@ -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 },

View File

@@ -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");
});

View File

@@ -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<TRuntime = RuntimeEnv>(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<TRuntime>(options?: { throwOnExit?: boolean }): TRuntime {
return createRuntimeEnv(options) as TRuntime;
}
export function createNonExitingRuntimeEnv(): RuntimeEnv {
return createRuntimeEnv({ throwOnExit: false });
}
export function createNonExitingTypedRuntimeEnv<TRuntime>(): TRuntime {
return createTypedRuntimeEnv<TRuntime>({ throwOnExit: false });
}

View File

@@ -75,6 +75,40 @@ export function createQueuedWizardPrompter(params?: {
type SetupWizardAdapterParams = Parameters<typeof buildChannelSetupWizardAdapterFromSetupWizard>[0];
type SetupWizardPlugin = SetupWizardAdapterParams["plugin"];
type SetupWizard = NonNullable<SetupWizardAdapterParams["wizard"]>;
type SetupWizardCredentialValues = Record<string, string>;
function resolveSetupWizardAccountContext<TCfg>(params: {
cfg?: TCfg;
accountId?: string;
credentialValues?: SetupWizardCredentialValues;
}) {
return {
cfg: (params.cfg ?? {}) as TCfg,
accountId: params.accountId ?? "default",
credentialValues: params.credentialValues ?? {},
};
}
function resolveSetupWizardRuntime<TRuntime>(runtime?: TRuntime): TRuntime {
return (runtime ?? createRuntimeEnv({ throwOnExit: false })) as TRuntime;
}
function resolveSetupWizardPrompter(prompter?: WizardPrompter): WizardPrompter {
return prompter ?? createTestWizardPrompter();
}
function resolveSetupWizardNotePrompter(prompter?: Pick<WizardPrompter, "note">) {
return (
prompter ??
({
note: vi.fn(async () => undefined),
} satisfies Pick<WizardPrompter, "note">)
);
}
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<string, unknown>,
@@ -123,3 +169,144 @@ export async function runSetupWizardConfigure<
forceAllowFrom: params.forceAllowFrom ?? false,
});
}
export async function runSetupWizardPrepare<
TCfg,
TOptions extends Record<string, unknown>,
TRuntime,
TResult,
>(params: {
prepare?: (args: {
cfg: TCfg;
accountId: string;
credentialValues: Record<string, string>;
runtime: TRuntime;
prompter: WizardPrompter;
options?: TOptions;
}) => Promise<TResult> | TResult;
cfg?: TCfg;
accountId?: string;
credentialValues?: Record<string, string>;
runtime?: TRuntime;
prompter?: WizardPrompter;
options?: TOptions;
}): Promise<TResult | undefined> {
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<string, unknown>,
TRuntime,
TResult,
>(params: {
finalize?: (args: {
cfg: TCfg;
accountId: string;
credentialValues: Record<string, string>;
runtime: TRuntime;
prompter: WizardPrompter;
options?: TOptions;
forceAllowFrom: boolean;
}) => Promise<TResult> | TResult;
cfg?: TCfg;
accountId?: string;
credentialValues?: Record<string, string>;
runtime?: TRuntime;
prompter?: WizardPrompter;
options?: TOptions;
forceAllowFrom?: boolean;
}): Promise<TResult | undefined> {
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<TCfg, TResult>(params: {
promptAllowFrom?: (args: {
cfg: TCfg;
prompter: WizardPrompter;
accountId: string;
}) => Promise<TResult> | TResult;
cfg?: TCfg;
prompter?: WizardPrompter;
accountId?: string;
}): Promise<TResult | undefined> {
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<TCfg, TResult>(params: {
resolveEntries?: (args: {
cfg: TCfg;
accountId: string;
credentialValues: Record<string, string>;
entries: string[];
}) => Promise<TResult> | TResult;
entries: string[];
cfg?: TCfg;
accountId?: string;
credentialValues?: SetupWizardCredentialValues;
}): Promise<TResult | undefined> {
const context = resolveSetupWizardAccountContext({
cfg: params.cfg,
accountId: params.accountId,
credentialValues: params.credentialValues,
});
return await params.resolveEntries?.({
...context,
entries: params.entries,
});
}
export async function resolveSetupWizardGroupAllowlist<TCfg, TResult>(params: {
resolveAllowlist?: (args: {
cfg: TCfg;
accountId: string;
credentialValues: Record<string, string>;
entries: string[];
prompter: Pick<WizardPrompter, "note">;
}) => Promise<TResult> | TResult;
entries: string[];
cfg?: TCfg;
accountId?: string;
credentialValues?: SetupWizardCredentialValues;
prompter?: Pick<WizardPrompter, "note">;
}): Promise<TResult | undefined> {
const context = resolveSetupWizardAccountContext({
cfg: params.cfg,
accountId: params.accountId,
credentialValues: params.credentialValues,
});
return await params.resolveAllowlist?.({
...context,
entries: params.entries,
prompter: resolveSetupWizardNotePrompter(params.prompter),
});
}