feat: Twitch Plugin (#1612)
* wip * copy polugin files * wip type changes * refactor: improve Twitch plugin code quality and fix all tests - Extract client manager registry for centralized lifecycle management - Refactor to use early returns and reduce mutations - Fix status check logic for clientId detection - Add comprehensive test coverage for new modules - Remove tests for unimplemented features (index.test.ts, resolver.test.ts) - Fix mock setup issues in test suite (149 tests now passing) - Improve error handling with errorResponse helper in actions.ts - Normalize token handling to eliminate duplication Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * use accountId * delete md file * delte tsconfig * adjust log level * fix probe logic * format * fix monitor * code review fixes * format * no mutation * less mutation * chain debug log * await authProvider setup * use uuid * use spread * fix tests * update docs and remove bot channel fallback * more readme fixes * remove comments + fromat * fix tests * adjust access control logic * format * install * simplify config object * remove duplicate log tags + log received messages * update docs * update tests * format * strip markdown in monitor * remove strip markdown config, enabled by default * default requireMention to true * fix store path arg * fix multi account id + add unit test * fix multi account id + add unit test * make channel required and update docs * remove whisper functionality * remove duplicate connect log * update docs with convert twitch link * make twitch message processing non blocking * schema consistent casing * remove noisy ignore log * use coreLogger --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
270
extensions/twitch/src/status.test.ts
Normal file
270
extensions/twitch/src/status.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Tests for status.ts module
|
||||
*
|
||||
* Tests cover:
|
||||
* - Detection of unconfigured accounts
|
||||
* - Detection of disabled accounts
|
||||
* - Detection of missing clientId
|
||||
* - Token format warnings
|
||||
* - Access control warnings
|
||||
* - Runtime error detection
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectTwitchStatusIssues } from "./status.js";
|
||||
import type { ChannelAccountSnapshot } from "./types.js";
|
||||
|
||||
describe("status", () => {
|
||||
describe("collectTwitchStatusIssues", () => {
|
||||
it("should detect unconfigured accounts", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: false,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
expect(issues.length).toBeGreaterThan(0);
|
||||
expect(issues[0]?.kind).toBe("config");
|
||||
expect(issues[0]?.message).toContain("not properly configured");
|
||||
});
|
||||
|
||||
it("should detect disabled accounts", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: false,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
expect(issues.length).toBeGreaterThan(0);
|
||||
const disabledIssue = issues.find((i) => i.message.includes("disabled"));
|
||||
expect(disabledIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect missing clientId when account configured (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123",
|
||||
// clientId missing
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const clientIdIssue = issues.find((i) => i.message.includes("client ID"));
|
||||
expect(clientIdIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should warn about oauth: prefix in token (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123", // has prefix
|
||||
clientId: "test-id",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const prefixIssue = issues.find((i) => i.message.includes("oauth:"));
|
||||
expect(prefixIssue).toBeDefined();
|
||||
expect(prefixIssue?.kind).toBe("config");
|
||||
});
|
||||
|
||||
it("should detect clientSecret without refreshToken (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123",
|
||||
clientId: "test-id",
|
||||
clientSecret: "secret123",
|
||||
// refreshToken missing
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const secretIssue = issues.find((i) => i.message.includes("clientSecret"));
|
||||
expect(secretIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect empty allowFrom array (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "test123",
|
||||
clientId: "test-id",
|
||||
allowFrom: [], // empty array
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const allowFromIssue = issues.find((i) => i.message.includes("allowFrom"));
|
||||
expect(allowFromIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "test123",
|
||||
clientId: "test-id",
|
||||
allowedRoles: ["all"],
|
||||
allowFrom: ["123456"], // conflict!
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const conflictIssue = issues.find((i) => i.kind === "intent");
|
||||
expect(conflictIssue).toBeDefined();
|
||||
expect(conflictIssue?.message).toContain("allowedRoles is set to 'all'");
|
||||
});
|
||||
|
||||
it("should detect runtime errors", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
lastError: "Connection timeout",
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const runtimeIssue = issues.find((i) => i.kind === "runtime");
|
||||
expect(runtimeIssue).toBeDefined();
|
||||
expect(runtimeIssue?.message).toContain("Connection timeout");
|
||||
});
|
||||
|
||||
it("should detect accounts that never connected", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: false,
|
||||
lastStartAt: undefined,
|
||||
lastInboundAt: undefined,
|
||||
lastOutboundAt: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const neverConnectedIssue = issues.find((i) =>
|
||||
i.message.includes("never connected successfully"),
|
||||
);
|
||||
expect(neverConnectedIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect long-running connections", () => {
|
||||
const oldDate = Date.now() - 8 * 24 * 60 * 60 * 1000; // 8 days ago
|
||||
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
running: true,
|
||||
lastStartAt: oldDate,
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const uptimeIssue = issues.find((i) => i.message.includes("running for"));
|
||||
expect(uptimeIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle empty snapshots array", () => {
|
||||
const issues = collectTwitchStatusIssues([]);
|
||||
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
it("should skip non-Twitch accounts gracefully", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: undefined,
|
||||
configured: false,
|
||||
enabled: true,
|
||||
running: false,
|
||||
},
|
||||
];
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
// Should not crash, may return empty or minimal issues
|
||||
expect(Array.isArray(issues)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user