chore: Fix type errors in extensions/twitch tests.

This commit is contained in:
cpojer
2026-02-17 10:11:34 +09:00
parent 889f221ed1
commit 0f8d1f175a
5 changed files with 76 additions and 49 deletions

View File

@@ -5,7 +5,9 @@ import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
describe("checkTwitchAccessControl", () => {
const mockAccount: TwitchAccountConfig = {
username: "testbot",
token: "oauth:test",
accessToken: "test",
clientId: "test-client-id",
channel: "testchannel",
};
const mockMessage: TwitchChatMessage = {

View File

@@ -34,14 +34,24 @@ vi.mock("./utils/markdown.js", () => ({
vi.mock("./utils/twitch.js", () => ({
normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""),
missingTargetError: (channel: string, hint: string) =>
`Missing target for ${channel}. Provide ${hint}`,
new Error(`Missing target for ${channel}. Provide ${hint}`),
}));
function assertResolvedTarget(
result: ReturnType<NonNullable<typeof twitchOutbound.resolveTarget>>,
): string {
if (!result.ok) {
throw result.error;
}
return result.to;
}
describe("outbound", () => {
const mockAccount = {
...BASE_TWITCH_TEST_ACCOUNT,
accessToken: "oauth:test123",
};
const resolveTarget = twitchOutbound.resolveTarget!;
const mockConfig = makeTwitchTestConfig(mockAccount);
installTwitchTestHooks();
@@ -63,106 +73,121 @@ describe("outbound", () => {
describe("resolveTarget", () => {
it("should normalize and return target in explicit mode", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#MyChannel",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
expect(result.to).toBe("mychannel");
expect(assertResolvedTarget(result)).toBe("mychannel");
});
it("should return target in implicit mode with wildcard allowlist", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#AnyChannel",
mode: "implicit",
allowFrom: ["*"],
});
expect(result.ok).toBe(true);
expect(result.to).toBe("anychannel");
expect(assertResolvedTarget(result)).toBe("anychannel");
});
it("should return target in implicit mode when in allowlist", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#allowed",
mode: "implicit",
allowFrom: ["#allowed", "#other"],
});
expect(result.ok).toBe(true);
expect(result.to).toBe("allowed");
expect(assertResolvedTarget(result)).toBe("allowed");
});
it("should error when target not in allowlist (implicit mode)", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#notallowed",
mode: "implicit",
allowFrom: ["#primary", "#secondary"],
});
expect(result.ok).toBe(false);
expect(result.error).toContain("Twitch");
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain("Twitch");
});
it("should accept any target when allowlist is empty", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#anychannel",
mode: "heartbeat",
allowFrom: [],
});
expect(result.ok).toBe(true);
expect(result.to).toBe("anychannel");
expect(assertResolvedTarget(result)).toBe("anychannel");
});
it("should error when no target provided with allowlist", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: undefined,
mode: "implicit",
allowFrom: ["#fallback", "#other"],
});
expect(result.ok).toBe(false);
expect(result.error).toContain("Twitch");
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain("Twitch");
});
it("should return error when no target and no allowlist", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: undefined,
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(false);
expect(result.error).toContain("Missing target");
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain("Missing target");
});
it("should handle whitespace-only target", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: " ",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(false);
expect(result.error).toContain("Missing target");
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain("Missing target");
});
it("should error when target normalizes to empty string", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(false);
expect(result.error).toContain("Twitch");
if (result.ok) {
throw new Error("expected resolveTarget to fail");
}
expect(result.error.message).toContain("Twitch");
});
it("should filter wildcard from allowlist when checking membership", () => {
const result = twitchOutbound.resolveTarget({
const result = resolveTarget({
to: "#mychannel",
mode: "implicit",
allowFrom: ["*", "#specific"],
@@ -170,7 +195,7 @@ describe("outbound", () => {
// With wildcard, any target is accepted
expect(result.ok).toBe(true);
expect(result.to).toBe("mychannel");
expect(assertResolvedTarget(result)).toBe("mychannel");
});
});
@@ -185,7 +210,7 @@ describe("outbound", () => {
messageId: "twitch-msg-123",
});
const result = await twitchOutbound.sendText({
const result = await twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello Twitch!",
@@ -211,7 +236,7 @@ describe("outbound", () => {
vi.mocked(getAccountConfig).mockReturnValue(null);
await expect(
twitchOutbound.sendText({
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
@@ -227,9 +252,9 @@ describe("outbound", () => {
vi.mocked(getAccountConfig).mockReturnValue(accountWithoutChannel);
await expect(
twitchOutbound.sendText({
twitchOutbound.sendText!({
cfg: mockConfig,
to: undefined,
to: "",
text: "Hello!",
accountId: "default",
}),
@@ -246,9 +271,9 @@ describe("outbound", () => {
messageId: "msg-456",
});
await twitchOutbound.sendText({
await twitchOutbound.sendText!({
cfg: mockConfig,
to: undefined,
to: "",
text: "Hello!",
accountId: "default",
});
@@ -268,13 +293,13 @@ describe("outbound", () => {
abortController.abort();
await expect(
twitchOutbound.sendText({
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
accountId: "default",
signal: abortController.signal,
}),
} as Parameters<NonNullable<typeof twitchOutbound.sendText>>[0]),
).rejects.toThrow("Outbound delivery aborted");
});
@@ -290,7 +315,7 @@ describe("outbound", () => {
});
await expect(
twitchOutbound.sendText({
twitchOutbound.sendText!({
cfg: mockConfig,
to: "#testchannel",
text: "Hello!",
@@ -311,7 +336,7 @@ describe("outbound", () => {
messageId: "media-msg-123",
});
const result = await twitchOutbound.sendMedia({
const result = await twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: "Check this:",
@@ -341,10 +366,10 @@ describe("outbound", () => {
messageId: "media-only-msg",
});
await twitchOutbound.sendMedia({
await twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: undefined,
text: "",
mediaUrl: "https://example.com/image.png",
accountId: "default",
});
@@ -364,14 +389,14 @@ describe("outbound", () => {
abortController.abort();
await expect(
twitchOutbound.sendMedia({
twitchOutbound.sendMedia!({
cfg: mockConfig,
to: "#testchannel",
text: "Check this:",
mediaUrl: "https://example.com/image.png",
accountId: "default",
signal: abortController.signal,
}),
} as Parameters<NonNullable<typeof twitchOutbound.sendMedia>>[0]),
).rejects.toThrow("Outbound delivery aborted");
});
});

View File

@@ -48,7 +48,7 @@ describe("send", () => {
const mockAccount = {
...BASE_TWITCH_TEST_ACCOUNT,
token: "oauth:test123",
accessToken: "test123",
};
const mockConfig = makeTwitchTestConfig(mockAccount);
@@ -66,7 +66,7 @@ describe("send", () => {
ok: true,
messageId: "twitch-msg-123",
}),
} as ReturnType<typeof getClientManager>);
} as unknown as ReturnType<typeof getClientManager>);
vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text);
const result = await sendMessageTwitchInternal(
@@ -93,7 +93,7 @@ describe("send", () => {
ok: true,
messageId: "twitch-msg-456",
}),
} as ReturnType<typeof getClientManager>);
} as unknown as ReturnType<typeof getClientManager>);
vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text.replace(/\*\*/g, ""));
await sendMessageTwitchInternal(
@@ -224,7 +224,7 @@ describe("send", () => {
vi.mocked(isAccountConfigured).mockReturnValue(true);
vi.mocked(getClientManager).mockReturnValue({
sendMessage: vi.fn().mockRejectedValue(new Error("Connection lost")),
} as ReturnType<typeof getClientManager>);
} as unknown as ReturnType<typeof getClientManager>);
const result = await sendMessageTwitchInternal(
"#testchannel",
@@ -253,7 +253,7 @@ describe("send", () => {
});
vi.mocked(getClientManager).mockReturnValue({
sendMessage: mockSend,
} as ReturnType<typeof getClientManager>);
} as unknown as ReturnType<typeof getClientManager>);
await sendMessageTwitchInternal(
"",

View File

@@ -254,7 +254,7 @@ describe("status", () => {
it("should skip non-Twitch accounts gracefully", () => {
const snapshots: ChannelAccountSnapshot[] = [
{
accountId: undefined,
accountId: "unknown",
configured: false,
enabled: true,
running: false,

View File

@@ -86,7 +86,7 @@ describe("TwitchClientManager", () => {
const testAccount: TwitchAccountConfig = {
username: "testbot",
token: "oauth:test123456",
accessToken: "test123456",
clientId: "test-client-id",
channel: "testchannel",
enabled: true,
@@ -94,7 +94,7 @@ describe("TwitchClientManager", () => {
const testAccount2: TwitchAccountConfig = {
username: "testbot2",
token: "oauth:test789",
accessToken: "test789",
clientId: "test-client-id-2",
channel: "testchannel2",
enabled: true,
@@ -145,8 +145,8 @@ describe("TwitchClientManager", () => {
it("should use account username as default channel when channel not specified", async () => {
const accountWithoutChannel: TwitchAccountConfig = {
...testAccount,
channel: undefined,
};
channel: "",
} as unknown as TwitchAccountConfig;
await manager.getClient(accountWithoutChannel);
@@ -172,7 +172,7 @@ describe("TwitchClientManager", () => {
it("should normalize token by removing oauth: prefix", async () => {
const accountWithPrefix: TwitchAccountConfig = {
...testAccount,
token: "oauth:actualtoken123",
accessToken: "oauth:actualtoken123",
};
// Override the mock to return a specific token for this test
@@ -207,8 +207,8 @@ describe("TwitchClientManager", () => {
it("should throw error when clientId is missing", async () => {
const accountWithoutClientId: TwitchAccountConfig = {
...testAccount,
clientId: undefined,
};
clientId: "" as unknown as string,
} as unknown as TwitchAccountConfig;
await expect(manager.getClient(accountWithoutClientId)).rejects.toThrow(
"Missing Twitch client ID",