Files
openclaw/extensions/twitch/src/plugin.test.ts
Ayaan Zaidi 2c8ed54ddb feat(heartbeat): default delivery to the configured owner, never groups (#121988)
Unset heartbeat.target now resolves "owner": elected heartbeat notifications deliver to the operator's DM resolved from commands.ownerAllowFrom or the channel allowFrom (first concrete entry; wildcards and channel-scoped wildcards excluded; configured owners exhausted across channels before any channel-local fallback). Delivery requires the channel's own classifier to positively prove a direct destination — every bundled messaging plugin now ships an inferTargetChatType contract — and unproven or group-shaped destinations fail closed to the visible no-route state. The first implicitly-routed delivery carries a one-line self-explanation naming the target: "none" opt-out. Explicit target "last" remains as the follow-the-conversation opt-in. Refines the unreleased #121892 default before it ships; refs #121880.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:18:22 +00:00

188 lines
5.6 KiB
TypeScript

// Twitch tests cover plugin plugin behavior.
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { twitchPlugin } from "./plugin.js";
import { twitchSetupPlugin } from "./setup-surface.js";
describe("twitchPlugin pairing", () => {
it("classifies only channel targets as groups", () => {
expect(twitchPlugin.messaging?.inferTargetChatType?.({ to: "twitch:openclaw" })).toBe("group");
expect(
twitchPlugin.messaging?.inferTargetChatType?.({ to: "twitch:user:operator" }),
).toBeUndefined();
});
it("normalizes trimmed twitch user prefixes in allow entries", () => {
expect(twitchPlugin.pairing?.normalizeAllowEntry?.(" twitch:user:123456 ")).toBe("123456");
expect(twitchPlugin.pairing?.normalizeAllowEntry?.(" user789012 ")).toBe("789012");
});
});
describe("twitchPlugin outbound session routing", () => {
it("reproduces the canonical inbound channel session", async () => {
const route = await twitchPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},
agentId: "ops",
accountId: "stream",
target: "twitch:channel:OpenClaw",
});
expect(route).toMatchObject({
sessionKey: "agent:ops:twitch:group:openclaw",
baseSessionKey: "agent:ops:twitch:group:openclaw",
recipientSessionExact: true,
peer: { kind: "group", id: "openclaw" },
chatType: "group",
to: "openclaw",
});
});
it.each(["twitch:user:alice", "twitch:dm:alice"])(
"rejects unsupported direct target %s",
async (target) => {
const route = await twitchPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},
agentId: "ops",
target,
});
expect(route).toBeNull();
},
);
});
describe("twitchPlugin.status.buildAccountSnapshot", () => {
it("uses the resolved account ID for multi-account configs", async () => {
const secondary = {
channel: "secondary-channel",
username: "secondary",
accessToken: "oauth:secondary-token",
clientId: "secondary-client",
enabled: true,
};
const cfg = {
channels: {
twitch: {
accounts: {
default: {
channel: "default-channel",
username: "default",
accessToken: "oauth:default-token",
clientId: "default-client",
enabled: true,
},
secondary,
},
},
},
} as OpenClawConfig;
const snapshot = await twitchPlugin.status?.buildAccountSnapshot?.({
account: twitchPlugin.config.resolveAccount(cfg, "secondary"),
cfg,
});
expect(snapshot?.accountId).toBe("secondary");
});
});
describe("twitchPlugin.config", () => {
it("uses configured defaultAccount for omitted-account plugin resolution", () => {
const cfg = {
channels: {
twitch: {
defaultAccount: "secondary",
accounts: {
default: {
channel: "default-channel",
username: "default",
accessToken: "oauth:default-token",
clientId: "default-client",
enabled: true,
},
secondary: {
channel: "secondary-channel",
username: "secondary",
accessToken: "oauth:secondary-token",
clientId: "secondary-client",
enabled: true,
},
},
},
},
} as OpenClawConfig;
expect(twitchPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
expect(twitchPlugin.config.resolveAccount(cfg).accountId).toBe("secondary");
});
it.each([
{
name: "does not borrow the configured default account's token",
defaultToken: "oauth:default-token",
selectedToken: "",
expected: false,
},
{
name: "does not reject a configured named account because the default lacks a token",
defaultToken: "",
selectedToken: "oauth:secondary-token",
expected: true,
},
])("$name", ({ defaultToken, selectedToken, expected }) => {
const cfg = {
channels: {
twitch: {
accounts: {
default: {
username: "default",
accessToken: defaultToken,
clientId: "default-client",
channel: "default-channel",
},
secondary: {
username: "secondary",
accessToken: selectedToken,
clientId: "secondary-client",
channel: "secondary-channel",
},
},
},
},
} as OpenClawConfig;
for (const config of [twitchPlugin.config, twitchSetupPlugin.config]) {
const account = config.resolveAccount(cfg, "secondary");
expect(account.accountId).toBe("secondary");
expect(config.isConfigured?.(account, cfg)).toBe(expected);
}
});
it("normalizes named accounts consistently across runtime and setup", () => {
const cfg = {
channels: {
twitch: {
accounts: {
Secondary: {
username: "secondary",
accessToken: "oauth:secondary-token",
clientId: "secondary-client",
channel: "secondary-channel",
},
},
},
},
} as OpenClawConfig;
for (const config of [twitchPlugin.config, twitchSetupPlugin.config]) {
const account = config.resolveAccount(cfg, "SECONDARY\r\n");
expect(account).toMatchObject({
accountId: "secondary",
username: "secondary",
});
expect(config.isConfigured?.(account, cfg)).toBe(true);
}
});
});