diff --git a/CHANGELOG.md b/CHANGELOG.md index eaaa2f8ba79..684d19f11c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - OpenAI Codex/OAuth: keep OpenClaw as the canonical owner for imported Codex CLI OAuth sessions, stop writing refreshed credentials back into `.codex`, and prefer fresher OpenClaw credentials over stale imported CLI state so refresh recovery stays stable. Thanks @vincentkoc. - OpenAI Codex/OAuth: treat the OpenAI TLS prerequisites probe as advisory instead of a hard blocker, so Codex sign-in can still proceed when the speculative Node/OpenSSL precheck fails but the real OAuth flow still works. Thanks @vincentkoc. - Models status/OAuth health: align OAuth health reporting with the same effective credential view runtime uses, so expired refreshable sessions stop showing healthy by default and fresher imported Codex CLI credentials surface correctly in `models status`, doctor, and gateway auth status. Thanks @vincentkoc. +- Twitch/setup: load Twitch through the bundled setup-entry discovery path and keep setup/status account detection aligned with runtime config. (#68008) Thanks @gumadeiras. ## 2026.4.15 diff --git a/extensions/twitch/index.test.ts b/extensions/twitch/index.test.ts new file mode 100644 index 00000000000..251bc173382 --- /dev/null +++ b/extensions/twitch/index.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { assertBundledChannelEntries } from "../../test/helpers/bundled-channel-entry.ts"; +import entry from "./index.js"; +import setupEntry from "./setup-entry.js"; + +describe("twitch bundled entries", () => { + assertBundledChannelEntries({ + entry, + expectedId: "twitch", + expectedName: "Twitch", + setupEntry, + }); + + it("loads the setup-only channel plugin", () => { + const plugin = setupEntry.loadSetupPlugin?.(); + + expect(plugin?.id).toBe("twitch"); + expect(plugin?.setupWizard).toBeDefined(); + }); +}); diff --git a/extensions/twitch/src/setup-surface.test.ts b/extensions/twitch/src/setup-surface.test.ts index 3d35b8325bd..106d9104cef 100644 --- a/extensions/twitch/src/setup-surface.test.ts +++ b/extensions/twitch/src/setup-surface.test.ts @@ -20,6 +20,7 @@ import { promptRefreshTokenSetup, promptToken, promptUsername, + twitchSetupPlugin, twitchSetupWizard, } from "./setup-surface.js"; import type { TwitchAccountConfig } from "./types.js"; @@ -31,6 +32,7 @@ const mockPrompter: WizardPrompter = { text: mockPromptText, confirm: mockPromptConfirm, } as unknown as WizardPrompter; +const originalEnvToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN; const mockAccount: TwitchAccountConfig = { username: "testbot", @@ -45,6 +47,11 @@ describe("setup surface helpers", () => { }); afterEach(() => { + if (originalEnvToken === undefined) { + delete process.env.OPENCLAW_TWITCH_ACCESS_TOKEN; + } else { + process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = originalEnvToken; + } // Don't restoreAllMocks as it breaks module-level mocks }); @@ -251,5 +258,57 @@ describe("setup surface helpers", () => { expect(lines).toEqual(["Twitch (secondary): configured"]); }); + + it("reports env-token default account setup as configured", async () => { + process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:fromenv"; + + const cfg = { + channels: { + twitch: { + accounts: { + default: { + username: "env-bot", + accessToken: "", + clientId: "env-client", + channel: "#env", + }, + }, + }, + }, + } as Parameters["resolveConfigured"]>[0]["cfg"]; + + expect(twitchSetupWizard.status?.resolveConfigured({ cfg })).toBe(true); + const account = twitchSetupPlugin.config.resolveAccount(cfg, "default"); + expect(await twitchSetupPlugin.config.isConfigured?.(account, cfg)).toBe(true); + }); + }); + + describe("setup-only plugin config", () => { + it("lists all configured Twitch accounts", () => { + const cfg = { + channels: { + twitch: { + defaultAccount: "secondary", + accounts: { + default: { + username: "default-bot", + accessToken: "oauth:default", + clientId: "default-client", + channel: "#default", + }, + secondary: { + username: "secondary-bot", + accessToken: "oauth:secondary", + clientId: "secondary-client", + channel: "#secondary", + }, + }, + }, + }, + } as Parameters[0]; + + expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["default", "secondary"]); + expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary"); + }); }); }); diff --git a/extensions/twitch/src/setup-surface.ts b/extensions/twitch/src/setup-surface.ts index d432666fc7a..8a03416de61 100644 --- a/extensions/twitch/src/setup-surface.ts +++ b/extensions/twitch/src/setup-surface.ts @@ -11,7 +11,13 @@ import { type OpenClawConfig, type WizardPrompter, } from "openclaw/plugin-sdk/setup"; -import { DEFAULT_ACCOUNT_ID, getAccountConfig, resolveDefaultTwitchAccountId } from "./config.js"; +import { + DEFAULT_ACCOUNT_ID, + getAccountConfig, + listAccountIds, + resolveDefaultTwitchAccountId, + resolveTwitchAccountContext, +} from "./config.js"; import type { TwitchAccountConfig, TwitchRole } from "./types.js"; import { isAccountConfigured } from "./utils/twitch.js"; @@ -348,13 +354,11 @@ export const twitchSetupWizard: ChannelSetupWizard = { configuredHint: "configured", unconfiguredHint: "needs setup", resolveConfigured: ({ cfg }) => { - const account = getAccountConfig(cfg, resolveSetupAccountId(cfg)); - return account ? isAccountConfigured(account) : false; + return resolveTwitchAccountContext(cfg, resolveSetupAccountId(cfg)).configured; }, resolveStatusLines: ({ cfg }) => { const accountId = resolveSetupAccountId(cfg); - const account = getAccountConfig(cfg, accountId); - const configured = account ? isAccountConfigured(account) : false; + const configured = resolveTwitchAccountContext(cfg, accountId).configured; return [ `Twitch${accountId !== DEFAULT_ACCOUNT_ID ? ` (${accountId})` : ""}: ${configured ? "configured" : "needs username, token, and clientId"}`, ]; @@ -437,25 +441,27 @@ export const twitchSetupPlugin: ChannelPlugin = { chatTypes: ["group"], }, config: { - listAccountIds: (cfg) => { - const accountId = resolveSetupAccountId(cfg); - return getAccountConfig(cfg, accountId) ? [accountId] : []; - }, + listAccountIds: (cfg) => listAccountIds(cfg), resolveAccount: (cfg, accountId) => { - const resolvedAccountId = accountId ?? resolveSetupAccountId(cfg); + const resolvedAccountId = accountId ?? resolveDefaultTwitchAccountId(cfg); const account = getAccountConfig(cfg, resolvedAccountId); - const fallback = { + if (!account) { + return { + accountId: resolvedAccountId, + username: "", + accessToken: "", + clientId: "", + channel: "", + enabled: false, + }; + } + return { accountId: resolvedAccountId, - username: "", - accessToken: "", - clientId: "", - channel: "", - enabled: false, + ...account, }; - return account ? { ...fallback, ...account } : fallback; }, - defaultAccountId: (cfg) => resolveSetupAccountId(cfg), - isConfigured: (account) => isAccountConfigured(account), + defaultAccountId: (cfg) => resolveDefaultTwitchAccountId(cfg), + isConfigured: (account, cfg) => resolveTwitchAccountContext(cfg, account?.accountId).configured, isEnabled: (account) => account.enabled !== false, }, setup: twitchSetupAdapter,