mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
Twitch: tighten setup account handling
This commit is contained in:
@@ -54,6 +54,30 @@ describe("getAccountConfig", () => {
|
||||
expect(result?.username).toBe("secondbot");
|
||||
});
|
||||
|
||||
it("normalizes account ids without reading inherited account properties", () => {
|
||||
const accounts = Object.create({
|
||||
inherited: {
|
||||
username: "inherited-bot",
|
||||
accessToken: "oauth:inherited",
|
||||
},
|
||||
}) as Record<string, unknown>;
|
||||
accounts.Secondary = {
|
||||
username: "secondbot",
|
||||
accessToken: "oauth:secondary",
|
||||
};
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getAccountConfig(cfg, "SECONDARY\r\n")).toMatchObject({ username: "secondbot" });
|
||||
expect(getAccountConfig(cfg, "inherited")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for non-existent account ID", () => {
|
||||
const result = getAccountConfig(mockMultiAccountConfig, "nonexistent");
|
||||
|
||||
@@ -120,6 +144,21 @@ describe("listAccountIds", () => {
|
||||
} as Parameters<typeof listAccountIds>[0]),
|
||||
).toEqual(["default", "secondary"]);
|
||||
});
|
||||
|
||||
it("normalizes configured account ids", () => {
|
||||
expect(
|
||||
listAccountIds({
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
Secondary: { username: "secondbot" },
|
||||
"Alerts\r\n\u001b[31m": { username: "alerts" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof listAccountIds>[0]),
|
||||
).toEqual(["alerts-31m", "secondary"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDefaultTwitchAccountId", () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { listCombinedAccountIds } from "openclaw/plugin-sdk/account-resolution";
|
||||
import {
|
||||
listCombinedAccountIds,
|
||||
normalizeAccountId,
|
||||
resolveNormalizedAccountEntry,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveTwitchToken, type TwitchTokenResolution } from "./token.js";
|
||||
import type { TwitchAccountConfig } from "./types.js";
|
||||
@@ -36,14 +40,19 @@ export function getAccountConfig(
|
||||
}
|
||||
|
||||
const cfg = coreConfig as OpenClawConfig;
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const twitch = cfg.channels?.twitch;
|
||||
// Access accounts via unknown to handle union type (single-account vs multi-account)
|
||||
const twitchRaw = twitch as Record<string, unknown> | undefined;
|
||||
const accounts = twitchRaw?.accounts as Record<string, TwitchAccountConfig> | undefined;
|
||||
|
||||
// For default account, check base-level config first
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
const accountFromAccounts = accounts?.[DEFAULT_ACCOUNT_ID];
|
||||
if (normalizedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||
const accountFromAccounts = resolveNormalizedAccountEntry(
|
||||
accounts,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
);
|
||||
|
||||
// Base-level properties that can form an implicit default account
|
||||
const baseLevel = {
|
||||
@@ -87,11 +96,12 @@ export function getAccountConfig(
|
||||
}
|
||||
|
||||
// For non-default accounts, only check accounts object
|
||||
if (!accounts || !accounts[accountId]) {
|
||||
const account = resolveNormalizedAccountEntry(accounts, normalizedAccountId, normalizeAccountId);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return accounts[accountId] as TwitchAccountConfig | null;
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +123,19 @@ export function listAccountIds(cfg: OpenClawConfig): string[] {
|
||||
typeof twitchRaw.channel === "string");
|
||||
|
||||
return listCombinedAccountIds({
|
||||
configuredAccountIds: Object.keys(accountMap ?? {}),
|
||||
configuredAccountIds: Object.keys(accountMap ?? {}).map((accountId) =>
|
||||
normalizeAccountId(accountId),
|
||||
),
|
||||
implicitAccountId: hasBaseLevelConfig ? DEFAULT_ACCOUNT_ID : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDefaultTwitchAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred =
|
||||
const preferredRaw =
|
||||
typeof cfg.channels?.twitch?.defaultAccount === "string"
|
||||
? cfg.channels.twitch.defaultAccount.trim()
|
||||
: "";
|
||||
const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : "";
|
||||
const ids = listAccountIds(cfg);
|
||||
if (preferred && ids.includes(preferred)) {
|
||||
return preferred;
|
||||
@@ -137,7 +150,9 @@ export function resolveTwitchAccountContext(
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string | null,
|
||||
): ResolvedTwitchAccountContext {
|
||||
const resolvedAccountId = accountId?.trim() || resolveDefaultTwitchAccountId(cfg);
|
||||
const resolvedAccountId = accountId?.trim()
|
||||
? normalizeAccountId(accountId)
|
||||
: resolveDefaultTwitchAccountId(cfg);
|
||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||
const tokenResolution = resolveTwitchToken(cfg, { accountId: resolvedAccountId });
|
||||
return {
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("setup surface helpers", () => {
|
||||
expect(defaultAccount?.clientId).toBe("test-client-id");
|
||||
});
|
||||
|
||||
it("writes env-token setup to the configured default account", async () => {
|
||||
it("skips env-token shortcut for non-default accounts", async () => {
|
||||
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
||||
mockPromptText
|
||||
.mockReset()
|
||||
@@ -230,12 +230,9 @@ describe("setup surface helpers", () => {
|
||||
{} as Parameters<typeof configureWithEnvToken>[5],
|
||||
);
|
||||
|
||||
const secondaryAccount = result?.cfg.channels?.twitch?.accounts?.secondary as
|
||||
| { username?: string; clientId?: string }
|
||||
| undefined;
|
||||
expect(secondaryAccount?.username).toBe("secondary-bot");
|
||||
expect(secondaryAccount?.clientId).toBe("secondary-client");
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.default).toBeUndefined();
|
||||
expect(result).toBeNull();
|
||||
expect(mockPromptConfirm).not.toHaveBeenCalled();
|
||||
expect(mockPromptText).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -402,6 +399,41 @@ describe("setup surface helpers", () => {
|
||||
expect(twitch?.accounts?.secondary?.accessToken).toBe("oauth:secondary");
|
||||
expect(twitch?.accounts?.default?.username).toBe("default-bot");
|
||||
});
|
||||
|
||||
it("persists a token instead of using env-token shortcut for non-default finalize", async () => {
|
||||
process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:fromenv";
|
||||
mockPromptText
|
||||
.mockReset()
|
||||
.mockResolvedValueOnce("secondary-bot" as never)
|
||||
.mockResolvedValueOnce("oauth:persisted" as never)
|
||||
.mockResolvedValueOnce("secondary-client" as never)
|
||||
.mockResolvedValueOnce("#secondary" as never);
|
||||
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
|
||||
|
||||
const result = await twitchSetupWizard.finalize?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {},
|
||||
},
|
||||
},
|
||||
} as Parameters<NonNullable<typeof twitchSetupWizard.finalize>>[0]["cfg"],
|
||||
accountId: "secondary",
|
||||
credentialValues: {},
|
||||
runtime: {} as Parameters<NonNullable<typeof twitchSetupWizard.finalize>>[0]["runtime"],
|
||||
prompter: mockPrompter,
|
||||
options: {},
|
||||
forceAllowFrom: false,
|
||||
});
|
||||
|
||||
const twitch = result?.cfg?.channels?.twitch;
|
||||
expect(twitch?.accounts?.secondary?.accessToken).toBe("oauth:persisted");
|
||||
expect(mockPromptConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
||||
message: "Enable automatic token refresh (requires client secret and refresh token)?",
|
||||
initialValue: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setup-only plugin config", () => {
|
||||
@@ -431,5 +463,31 @@ describe("setup surface helpers", () => {
|
||||
expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["default", "secondary"]);
|
||||
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
|
||||
});
|
||||
|
||||
it("normalizes exposed account ids", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
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(["secondary"]);
|
||||
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
|
||||
expect(twitchSetupPlugin.config.resolveAccount(cfg, "SECONDARY\r\n").accountId).toBe(
|
||||
"secondary",
|
||||
);
|
||||
expect(twitchSetupPlugin.config.resolveAccount(cfg, "SECONDARY\r\n").username).toBe(
|
||||
"secondary-bot",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,6 +208,11 @@ export async function configureWithEnvToken(
|
||||
dmPolicy: ChannelSetupDmPolicy,
|
||||
accountId: string = resolveSetupAccountId(cfg),
|
||||
): Promise<{ cfg: OpenClawConfig } | null> {
|
||||
const resolvedAccountId = normalizeAccountId(accountId);
|
||||
if (resolvedAccountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const useEnv = await prompter.confirm({
|
||||
message: "Twitch env var OPENCLAW_TWITCH_ACCESS_TOKEN detected. Use env token?",
|
||||
initialValue: true,
|
||||
@@ -227,11 +232,17 @@ export async function configureWithEnvToken(
|
||||
accessToken: "",
|
||||
enabled: true,
|
||||
},
|
||||
accountId,
|
||||
resolvedAccountId,
|
||||
);
|
||||
|
||||
if (forceAllowFrom && dmPolicy.promptAllowFrom) {
|
||||
return { cfg: await dmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter, accountId }) };
|
||||
return {
|
||||
cfg: await dmPolicy.promptAllowFrom({
|
||||
cfg: cfgWithAccount,
|
||||
prompter,
|
||||
accountId: resolvedAccountId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return { cfg: cfgWithAccount };
|
||||
@@ -400,7 +411,7 @@ export const twitchSetupWizard: ChannelSetupWizard = {
|
||||
|
||||
const envToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN?.trim();
|
||||
|
||||
if (envToken && !account?.accessToken) {
|
||||
if (accountId === DEFAULT_ACCOUNT_ID && envToken && !account?.accessToken) {
|
||||
const envResult = await configureWithEnvToken(
|
||||
cfg,
|
||||
prompter,
|
||||
@@ -469,7 +480,7 @@ export const twitchSetupPlugin: ChannelPlugin<ResolvedTwitchAccount> = {
|
||||
config: {
|
||||
listAccountIds: (cfg) => listAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => {
|
||||
const resolvedAccountId = accountId ?? resolveDefaultTwitchAccountId(cfg);
|
||||
const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultTwitchAccountId(cfg));
|
||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||
if (!account) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user