426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
555 lines
17 KiB
TypeScript
555 lines
17 KiB
TypeScript
/**
|
|
* Exercises the Twitch plugin surface and its chat-formatting helpers (channel
|
|
* normalization, message splitting/length caps, markdown stripping) against a
|
|
* mocked runtime — no live Twitch/IRC connection.
|
|
*/
|
|
import type {
|
|
Content,
|
|
IAgentRuntime,
|
|
Memory,
|
|
MessageConnectorRegistration,
|
|
State,
|
|
} from "@elizaos/core";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
import twitchPlugin, {
|
|
formatChannelForDisplay,
|
|
getTwitchUserDisplayName,
|
|
MAX_TWITCH_MESSAGE_LENGTH,
|
|
normalizeChannel,
|
|
splitMessageForTwitch,
|
|
stripMarkdownForTwitch,
|
|
TwitchApiError,
|
|
TwitchConfigurationError,
|
|
type TwitchMessage,
|
|
type TwitchMessageSendOptions,
|
|
TwitchNotConnectedError,
|
|
TwitchPluginError,
|
|
type TwitchSendResult,
|
|
TwitchService,
|
|
TwitchServiceNotInitializedError,
|
|
type TwitchSettings,
|
|
type TwitchUserInfo,
|
|
} from "../src/index.ts";
|
|
import { TwitchWorkflowCredentialProvider } from "../src/workflow-credential-provider.ts";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers: mock runtime, memory, state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type RegisterMessageConnectorMock = ReturnType<
|
|
typeof vi.fn<(registration: MessageConnectorRegistration) => void>
|
|
>;
|
|
|
|
type MockRuntime = IAgentRuntime & {
|
|
registerMessageConnector: RegisterMessageConnectorMock;
|
|
registerSendHandler: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
type MockRuntimeOverrides = Partial<IAgentRuntime> &
|
|
Record<string, unknown> & {
|
|
modelResponse?: string;
|
|
service?: unknown;
|
|
registerMessageConnector?: RegisterMessageConnectorMock;
|
|
registerSendHandler?: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
type TwitchServiceHarnessFields = {
|
|
settings?: TwitchSettings;
|
|
joinedChannels?: Set<string>;
|
|
accountServices?: Map<string, TwitchService>;
|
|
connected?: boolean;
|
|
client?: unknown;
|
|
};
|
|
|
|
function makeMockRuntime(overrides: MockRuntimeOverrides = {}): MockRuntime {
|
|
return {
|
|
agentId: "agent-1",
|
|
getSetting: (key: string) =>
|
|
(overrides as Record<string, string>)[key] ?? null,
|
|
getService: (_name: string) => overrides.service ?? null,
|
|
composeState: async (_msg: unknown) => ({ recentMessages: "" }),
|
|
useModel: async (_type: string, _opts: unknown) =>
|
|
overrides.modelResponse ?? "{}",
|
|
emitEvent: async () => {},
|
|
registerMessageConnector:
|
|
vi.fn<(registration: MessageConnectorRegistration) => void>(),
|
|
registerSendHandler: vi.fn(),
|
|
...overrides,
|
|
} as MockRuntime;
|
|
}
|
|
|
|
function _makeMemory(
|
|
source: string = "twitch",
|
|
text: string = "hello",
|
|
): Partial<Memory> {
|
|
return {
|
|
content: { text, source },
|
|
userId: "user-1",
|
|
roomId: "room-1",
|
|
};
|
|
}
|
|
|
|
function _makeState(extra: Record<string, unknown> = {}): Partial<State> {
|
|
return {
|
|
agentName: "TestBot",
|
|
recentMessages: "",
|
|
data: {},
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function makeTwitchSettings(
|
|
overrides: Partial<TwitchSettings>,
|
|
): TwitchSettings {
|
|
return {
|
|
username: "testbot",
|
|
clientId: "client-id",
|
|
accessToken: "access-token",
|
|
channel: "mainchannel",
|
|
additionalChannels: [],
|
|
requireMention: false,
|
|
allowedRoles: ["all"],
|
|
allowedUserIds: [],
|
|
enabled: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeTwitchServiceHarness(
|
|
fields: TwitchServiceHarnessFields = {},
|
|
): TwitchService & TwitchServiceHarnessFields {
|
|
return Object.assign(
|
|
Object.create(TwitchService.prototype),
|
|
fields,
|
|
) as TwitchService & TwitchServiceHarnessFields;
|
|
}
|
|
|
|
function _makeUser(overrides: Partial<TwitchUserInfo> = {}): TwitchUserInfo {
|
|
return {
|
|
userId: "1",
|
|
username: "alice",
|
|
displayName: "Alice",
|
|
isModerator: false,
|
|
isBroadcaster: false,
|
|
isVip: false,
|
|
isSubscriber: false,
|
|
badges: new Map(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function _makeMockTwitchService(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
isConnected: () => overrides.connected ?? true,
|
|
getBotUsername: () => overrides.botUsername ?? "testbot",
|
|
getPrimaryChannel: () => overrides.primaryChannel ?? "mainchannel",
|
|
getJoinedChannels: () =>
|
|
(overrides.joinedChannels as string[]) ?? ["mainchannel"],
|
|
sendMessage: async (_text: string, _opts?: TwitchMessageSendOptions) =>
|
|
(overrides.sendResult as TwitchSendResult) ?? {
|
|
success: true,
|
|
messageId: "msg-123",
|
|
},
|
|
joinChannel: overrides.joinChannel ?? (async () => {}),
|
|
leaveChannel: overrides.leaveChannel ?? (async () => {}),
|
|
};
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 1. Plugin Metadata
|
|
// ===========================================================================
|
|
|
|
describe("Plugin metadata", () => {
|
|
test("does not register platform-specific chat actions", () => {
|
|
expect(twitchPlugin.actions).toHaveLength(0);
|
|
});
|
|
|
|
test("does not register platform-specific context providers", () => {
|
|
expect(twitchPlugin.providers).toHaveLength(0);
|
|
});
|
|
|
|
test("registers Twitch and workflow credential services", () => {
|
|
expect(twitchPlugin.services).toHaveLength(2);
|
|
expect(twitchPlugin.services).toContain(TwitchService);
|
|
expect(twitchPlugin.services).toContain(TwitchWorkflowCredentialProvider);
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// 4. Utility Functions
|
|
// ===========================================================================
|
|
|
|
describe("normalizeChannel", () => {
|
|
test.each([
|
|
["#mychannel", "mychannel"],
|
|
["mychannel", "mychannel"],
|
|
["", ""],
|
|
["##double", "#double"],
|
|
])("normalizeChannel(%s) → %s", (input, expected) => {
|
|
expect(normalizeChannel(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("formatChannelForDisplay", () => {
|
|
test.each([
|
|
["mychannel", "#mychannel"],
|
|
["#mychannel", "#mychannel"],
|
|
])("formatChannelForDisplay(%s) → %s", (input, expected) => {
|
|
expect(formatChannelForDisplay(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("getTwitchUserDisplayName", () => {
|
|
test("returns displayName when set", () => {
|
|
const user = {
|
|
userId: "1",
|
|
username: "alice",
|
|
displayName: "Alice_Cool",
|
|
isModerator: false,
|
|
isBroadcaster: false,
|
|
isVip: false,
|
|
isSubscriber: false,
|
|
badges: new Map(),
|
|
} as TwitchUserInfo;
|
|
expect(getTwitchUserDisplayName(user)).toBe("Alice_Cool");
|
|
});
|
|
|
|
test("falls back to username when displayName is empty", () => {
|
|
const user = {
|
|
userId: "1",
|
|
username: "bob",
|
|
displayName: "",
|
|
isModerator: false,
|
|
isBroadcaster: false,
|
|
isVip: false,
|
|
isSubscriber: false,
|
|
badges: new Map(),
|
|
} as TwitchUserInfo;
|
|
expect(getTwitchUserDisplayName(user)).toBe("bob");
|
|
});
|
|
});
|
|
|
|
describe("stripMarkdownForTwitch", () => {
|
|
test.each([
|
|
["**bold text**", "bold text"],
|
|
["__bold text__", "bold text"],
|
|
["*italic text*", "italic text"],
|
|
["_italic text_", "italic text"],
|
|
["~~strikethrough~~", "strikethrough"],
|
|
["`some code`", "some code"],
|
|
["[click here](https://example.com)", "click here"],
|
|
["## My Header", "My Header"],
|
|
["> quoted text", "quoted text"],
|
|
["- item one", "• item one"],
|
|
["1. item one", "• item one"],
|
|
["a\n\n\n\nb", "a\n\nb"],
|
|
["plain text", "plain text"],
|
|
[" hello ", "hello"],
|
|
])("stripMarkdownForTwitch(%j) → %j", (input, expected) => {
|
|
expect(stripMarkdownForTwitch(input)).toBe(expected);
|
|
});
|
|
|
|
test("processes code blocks", () => {
|
|
const result = stripMarkdownForTwitch("```js\nconsole.log('hi');\n```");
|
|
expect(result.length).toBeGreaterThan(0);
|
|
const result2 = stripMarkdownForTwitch("before ```code``` after");
|
|
expect(result2).toContain("code");
|
|
});
|
|
});
|
|
|
|
describe("splitMessageForTwitch", () => {
|
|
test("returns single chunk for short messages", () => {
|
|
const result = splitMessageForTwitch("Hello world");
|
|
expect(result).toEqual(["Hello world"]);
|
|
});
|
|
|
|
test("splits long messages into multiple chunks", () => {
|
|
const longMessage = "A".repeat(600);
|
|
const result = splitMessageForTwitch(longMessage);
|
|
expect(result.length).toBeGreaterThan(1);
|
|
for (const chunk of result) {
|
|
expect(chunk.length).toBeLessThanOrEqual(MAX_TWITCH_MESSAGE_LENGTH);
|
|
}
|
|
});
|
|
|
|
test("prefers splitting at sentence boundaries", () => {
|
|
// The ". " must appear past halfway of maxLength to be selected.
|
|
// lastIndexOf(". ", maxLength) returns the index of the ".", so the
|
|
// split point is at that index — everything before goes to chunk 0.
|
|
const prefix = "A".repeat(300);
|
|
const text = `${prefix}. ${"B".repeat(250)}`; // total 552
|
|
const result = splitMessageForTwitch(text, 500);
|
|
expect(result.length).toBe(2);
|
|
expect(result[0]).toBe(prefix);
|
|
expect(result[1]).toContain("B");
|
|
expect(result[1].length).toBeLessThan(text.length);
|
|
});
|
|
|
|
test("falls back to word boundary when no sentence break", () => {
|
|
const words = Array(60).fill("word").join(" "); // 60*5-1 = 299 chars
|
|
const result = splitMessageForTwitch(words, 50);
|
|
expect(result.length).toBeGreaterThan(1);
|
|
// Every chunk should be within the max limit
|
|
for (const chunk of result) {
|
|
expect(chunk.length).toBeLessThanOrEqual(50);
|
|
}
|
|
// Reassembled text should contain all original words
|
|
const reassembled = result.join(" ");
|
|
expect(reassembled.replace(/\s+/g, " ")).toContain("word");
|
|
});
|
|
|
|
test("respects custom maxLength", () => {
|
|
const text = "A".repeat(30);
|
|
const result = splitMessageForTwitch(text, 10);
|
|
expect(result.length).toBe(3);
|
|
});
|
|
|
|
test("returns empty for single-word exact match", () => {
|
|
const text = "A".repeat(500);
|
|
const result = splitMessageForTwitch(text, 500);
|
|
expect(result).toEqual([text]);
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// 5. Error Classes
|
|
// ===========================================================================
|
|
|
|
describe("Custom Errors", () => {
|
|
test("TwitchPluginError is an Error with correct name", () => {
|
|
const err = new TwitchPluginError("oops");
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err.name).toBe("TwitchPluginError");
|
|
expect(err.message).toBe("oops");
|
|
});
|
|
|
|
test("TwitchServiceNotInitializedError has default message", () => {
|
|
const err = new TwitchServiceNotInitializedError();
|
|
expect(err.message).toBe("Twitch service is not initialized");
|
|
expect(err.name).toBe("TwitchServiceNotInitializedError");
|
|
expect(err).toBeInstanceOf(TwitchPluginError);
|
|
});
|
|
|
|
test("TwitchNotConnectedError has default message", () => {
|
|
const err = new TwitchNotConnectedError();
|
|
expect(err.message).toBe("Twitch client is not connected");
|
|
expect(err.name).toBe("TwitchNotConnectedError");
|
|
expect(err).toBeInstanceOf(TwitchPluginError);
|
|
});
|
|
|
|
test("TwitchConfigurationError stores settingName", () => {
|
|
const err = new TwitchConfigurationError("bad config", "MY_SETTING");
|
|
expect(err.message).toBe("bad config");
|
|
expect(err.settingName).toBe("MY_SETTING");
|
|
expect(err.name).toBe("TwitchConfigurationError");
|
|
expect(err).toBeInstanceOf(TwitchPluginError);
|
|
});
|
|
|
|
test("TwitchApiError stores statusCode", () => {
|
|
const err = new TwitchApiError("api fail", 401);
|
|
expect(err.message).toBe("api fail");
|
|
expect(err.statusCode).toBe(401);
|
|
expect(err.name).toBe("TwitchApiError");
|
|
expect(err).toBeInstanceOf(TwitchPluginError);
|
|
});
|
|
});
|
|
|
|
describe("Twitch message connector accounts", () => {
|
|
test("registers one connector for each started account", () => {
|
|
const runtime = makeMockRuntime({
|
|
registerMessageConnector:
|
|
vi.fn<(registration: MessageConnectorRegistration) => void>(),
|
|
registerSendHandler: vi.fn(),
|
|
logger: { info: vi.fn() },
|
|
});
|
|
const primary = makeTwitchServiceHarness({
|
|
settings: makeTwitchSettings({
|
|
accountId: "primary",
|
|
username: "bot_one",
|
|
channel: "one",
|
|
}),
|
|
joinedChannels: new Set(["one"]),
|
|
});
|
|
const secondary = makeTwitchServiceHarness({
|
|
settings: makeTwitchSettings({
|
|
accountId: "secondary",
|
|
username: "bot_two",
|
|
channel: "two",
|
|
}),
|
|
joinedChannels: new Set(["two"]),
|
|
});
|
|
const service = makeTwitchServiceHarness({
|
|
accountServices: new Map([
|
|
["primary", primary],
|
|
["secondary", secondary],
|
|
]),
|
|
});
|
|
|
|
TwitchService.registerSendHandlers(runtime, service);
|
|
|
|
expect(runtime.registerMessageConnector).toHaveBeenCalledTimes(2);
|
|
expect(
|
|
runtime.registerMessageConnector.mock.calls.map(
|
|
([registration]) => registration.accountId,
|
|
),
|
|
).toEqual(["primary", "secondary"]);
|
|
});
|
|
|
|
test("registers accountId and routes sends through that account", async () => {
|
|
const runtime = makeMockRuntime({
|
|
registerMessageConnector:
|
|
vi.fn<(registration: MessageConnectorRegistration) => void>(),
|
|
registerSendHandler: vi.fn(),
|
|
getRoom: vi.fn(),
|
|
logger: { info: vi.fn() },
|
|
TWITCH_DEFAULT_ACCOUNT_ID: "streamer",
|
|
});
|
|
const service = makeTwitchServiceHarness({
|
|
settings: makeTwitchSettings({
|
|
accountId: "streamer",
|
|
username: "testbot",
|
|
channel: "mainchannel",
|
|
}),
|
|
});
|
|
const sendMessage = vi
|
|
.spyOn(service, "sendMessage")
|
|
.mockResolvedValue({ success: true, messageId: "msg-1" });
|
|
|
|
TwitchService.registerSendHandlers(runtime, service);
|
|
|
|
expect(runtime.registerMessageConnector).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
source: "twitch",
|
|
accountId: "streamer",
|
|
joinHandler: expect.any(Function),
|
|
leaveHandler: expect.any(Function),
|
|
}),
|
|
);
|
|
|
|
const registration = runtime.registerMessageConnector.mock.calls[0][0];
|
|
await registration.sendHandler?.(
|
|
runtime,
|
|
{
|
|
source: "twitch",
|
|
accountId: "streamer",
|
|
channelId: "mainchannel",
|
|
},
|
|
{ text: "hello" } satisfies Content,
|
|
);
|
|
|
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
"hello",
|
|
expect.objectContaining({ channel: "mainchannel" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// 7. sendMessage path
|
|
// ===========================================================================
|
|
// The Twitch MessageConnector (registered by TwitchService.registerSendHandlers)
|
|
// is the canonical send path, via MESSAGE operation=send.
|
|
|
|
// ===========================================================================
|
|
// 12. Type Construction
|
|
// ===========================================================================
|
|
|
|
describe("Type construction and shapes", () => {
|
|
test("TwitchUserInfo can be constructed with all fields", () => {
|
|
const user: TwitchUserInfo = {
|
|
userId: "123",
|
|
username: "testuser",
|
|
displayName: "TestUser",
|
|
isModerator: true,
|
|
isBroadcaster: false,
|
|
isVip: true,
|
|
isSubscriber: false,
|
|
color: "#FF0000",
|
|
badges: new Map([["moderator", "1"]]),
|
|
};
|
|
expect(user.userId).toBe("123");
|
|
expect(user.color).toBe("#FF0000");
|
|
expect(user.badges.get("moderator")).toBe("1");
|
|
});
|
|
|
|
test("TwitchMessage can be constructed with reply info", () => {
|
|
const msg: TwitchMessage = {
|
|
id: "msg-1",
|
|
channel: "test",
|
|
text: "hello",
|
|
user: {
|
|
userId: "1",
|
|
username: "user1",
|
|
displayName: "User1",
|
|
isModerator: false,
|
|
isBroadcaster: false,
|
|
isVip: false,
|
|
isSubscriber: false,
|
|
badges: new Map(),
|
|
},
|
|
timestamp: new Date(),
|
|
isAction: false,
|
|
isHighlighted: true,
|
|
replyTo: {
|
|
messageId: "parent-1",
|
|
userId: "2",
|
|
username: "user2",
|
|
text: "original",
|
|
},
|
|
};
|
|
expect(msg.replyTo?.messageId).toBe("parent-1");
|
|
expect(msg.isHighlighted).toBe(true);
|
|
});
|
|
|
|
test("TwitchSendResult success shape", () => {
|
|
const res: TwitchSendResult = {
|
|
success: true,
|
|
messageId: "abc-123",
|
|
};
|
|
expect(res.success).toBe(true);
|
|
expect(res.messageId).toBe("abc-123");
|
|
expect(res.error).toBeUndefined();
|
|
});
|
|
|
|
test("TwitchSendResult failure shape", () => {
|
|
const res: TwitchSendResult = {
|
|
success: false,
|
|
error: "not connected",
|
|
};
|
|
expect(res.success).toBe(false);
|
|
expect(res.error).toBe("not connected");
|
|
expect(res.messageId).toBeUndefined();
|
|
});
|
|
|
|
test("TwitchMessageSendOptions is optional fields", () => {
|
|
const opts: TwitchMessageSendOptions = {};
|
|
expect(opts.channel).toBeUndefined();
|
|
expect(opts.replyTo).toBeUndefined();
|
|
|
|
const opts2: TwitchMessageSendOptions = {
|
|
channel: "test",
|
|
replyTo: "msg-1",
|
|
};
|
|
expect(opts2.channel).toBe("test");
|
|
expect(opts2.replyTo).toBe("msg-1");
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// 13. TwitchService Static Properties
|
|
// ===========================================================================
|
|
|
|
describe("TwitchService class", () => {
|
|
test("has correct serviceType", () => {
|
|
expect(TwitchService.serviceType).toBe("twitch");
|
|
});
|
|
|
|
test("has static start method", () => {
|
|
expect(typeof TwitchService.start).toBe("function");
|
|
});
|
|
|
|
test("has static stopRuntime method", () => {
|
|
expect(typeof TwitchService.stopRuntime).toBe("function");
|
|
});
|
|
});
|