diff --git a/extensions/twitch/src/setup-surface.test.ts b/extensions/twitch/src/setup-surface.test.ts index 4336782eb9a..967eb3cfba2 100644 --- a/extensions/twitch/src/setup-surface.test.ts +++ b/extensions/twitch/src/setup-surface.test.ts @@ -313,20 +313,40 @@ describe("setup surface helpers", () => { }); describe("setup wizard account routing", () => { - it("normalizes account ids before using them as config keys", () => { - const cfg = setTwitchAccount( - {} as Parameters[0], - { - username: "normalized-bot", - accessToken: "oauth:normalized", - clientId: "normalized-client", - channel: "#normalized", - }, - "__proto__", - ); + it("rejects reserved account ids before using them as config keys", () => { + expect(() => + setTwitchAccount( + {} as Parameters[0], + { + username: "reserved-bot", + accessToken: "oauth:reserved", + clientId: "reserved-client", + channel: "#reserved", + }, + "__proto__", + ), + ).toThrow("Invalid Twitch account id"); - expect(cfg.channels?.twitch?.accounts?.default?.username).toBe("normalized-bot"); expect(Object.prototype).not.toHaveProperty("username"); + }); + + it("rejects reserved account ids before env-token writes", async () => { + await expect( + configureWithEnvToken( + {} as Parameters[0], + mockPrompter, + null, + "oauth:fromenv", + false, + {} as Parameters[5], + "__proto__", + ), + ).rejects.toThrow("Invalid Twitch account id"); + + expect(mockPromptConfirm).not.toHaveBeenCalled(); + }); + + it("normalizes account ids before rendering status lines", () => { expect( twitchSetupWizard.status?.resolveStatusLines?.({ cfg: {}, diff --git a/extensions/twitch/src/setup-surface.ts b/extensions/twitch/src/setup-surface.ts index 2ad4f540989..a464929c4a7 100644 --- a/extensions/twitch/src/setup-surface.ts +++ b/extensions/twitch/src/setup-surface.ts @@ -2,6 +2,7 @@ * Twitch setup wizard surface for CLI setup. */ +import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id"; import { getChatChannelMeta, type ChannelPlugin } from "openclaw/plugin-sdk/core"; import { formatDocsLink, @@ -23,11 +24,20 @@ import type { TwitchAccountConfig, TwitchRole } from "./types.js"; import { isAccountConfigured } from "./utils/twitch.js"; const channel = "twitch" as const; +const INVALID_ACCOUNT_ID_MESSAGE = "Invalid Twitch account id"; + +function normalizeRequestedSetupAccountId(accountId: string): string { + const normalized = normalizeOptionalAccountId(accountId); + if (!normalized) { + throw new Error(INVALID_ACCOUNT_ID_MESSAGE); + } + return normalized; +} function resolveSetupAccountId(cfg: OpenClawConfig, requestedAccountId?: string): string { const requested = requestedAccountId?.trim(); if (requested) { - return normalizeAccountId(requested); + return normalizeRequestedSetupAccountId(requested); } const preferred = cfg.channels?.twitch?.defaultAccount?.trim(); @@ -39,7 +49,9 @@ export function setTwitchAccount( account: Partial, accountId: string = resolveSetupAccountId(cfg), ): OpenClawConfig { - const resolvedAccountId = normalizeAccountId(accountId); + const resolvedAccountId = accountId.trim() + ? normalizeRequestedSetupAccountId(accountId) + : resolveSetupAccountId(cfg); const existing = getAccountConfig(cfg, resolvedAccountId); const merged: TwitchAccountConfig = { username: account.username ?? existing?.username ?? "", @@ -208,7 +220,9 @@ export async function configureWithEnvToken( dmPolicy: ChannelSetupDmPolicy, accountId: string = resolveSetupAccountId(cfg), ): Promise<{ cfg: OpenClawConfig } | null> { - const resolvedAccountId = normalizeAccountId(accountId); + const resolvedAccountId = accountId.trim() + ? normalizeRequestedSetupAccountId(accountId) + : resolveSetupAccountId(cfg); if (resolvedAccountId !== DEFAULT_ACCOUNT_ID) { return null; }