Twitch: align setup entry config

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 02:35:06 -04:00
parent 57bb717daf
commit 729c7b8613
4 changed files with 105 additions and 19 deletions

View File

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

View File

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

View File

@@ -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<NonNullable<typeof twitchSetupWizard.status>["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<typeof twitchSetupPlugin.config.listAccountIds>[0];
expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["default", "secondary"]);
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
});
});
});

View File

@@ -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<ResolvedTwitchAccount> = {
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,