@kodelyth/twitch 2026.5.42 → 2026.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/klaw.plugin.json +219 -2
- package/package.json +18 -1
- package/api.ts +0 -21
- package/channel-plugin-api.ts +0 -1
- package/index.test.ts +0 -13
- package/index.ts +0 -16
- package/runtime-api.ts +0 -22
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/access-control.test.ts +0 -373
- package/src/access-control.ts +0 -195
- package/src/actions.test.ts +0 -75
- package/src/actions.ts +0 -175
- package/src/client-manager-registry.ts +0 -87
- package/src/config-schema.test.ts +0 -46
- package/src/config-schema.ts +0 -88
- package/src/config.test.ts +0 -233
- package/src/config.ts +0 -177
- package/src/monitor.ts +0 -311
- package/src/outbound.test.ts +0 -572
- package/src/outbound.ts +0 -242
- package/src/plugin.lifecycle.test.ts +0 -86
- package/src/plugin.live.test.ts +0 -120
- package/src/plugin.test.ts +0 -77
- package/src/plugin.ts +0 -220
- package/src/probe.test.ts +0 -196
- package/src/probe.ts +0 -130
- package/src/resolver.ts +0 -139
- package/src/runtime.ts +0 -9
- package/src/send.test.ts +0 -342
- package/src/send.ts +0 -191
- package/src/setup-surface.test.ts +0 -529
- package/src/setup-surface.ts +0 -526
- package/src/status.test.ts +0 -298
- package/src/status.ts +0 -179
- package/src/test-fixtures.ts +0 -30
- package/src/token.test.ts +0 -198
- package/src/token.ts +0 -93
- package/src/twitch-client.test.ts +0 -574
- package/src/twitch-client.ts +0 -276
- package/src/types.ts +0 -104
- package/src/utils/markdown.ts +0 -98
- package/src/utils/twitch.ts +0 -81
- package/test/setup.ts +0 -7
- package/tsconfig.json +0 -16
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for setup-surface.ts helpers.
|
|
3
|
-
*
|
|
4
|
-
* Tests cover:
|
|
5
|
-
* - promptToken helper
|
|
6
|
-
* - promptUsername helper
|
|
7
|
-
* - promptClientId helper
|
|
8
|
-
* - promptChannelName helper
|
|
9
|
-
* - promptRefreshTokenSetup helper
|
|
10
|
-
* - configureWithEnvToken helper
|
|
11
|
-
* - setTwitchAccount config updates
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
-
import type { WizardPrompter } from "../api.js";
|
|
16
|
-
import {
|
|
17
|
-
configureWithEnvToken,
|
|
18
|
-
promptChannelName,
|
|
19
|
-
promptClientId,
|
|
20
|
-
promptRefreshTokenSetup,
|
|
21
|
-
promptToken,
|
|
22
|
-
promptUsername,
|
|
23
|
-
setTwitchAccount,
|
|
24
|
-
twitchSetupPlugin,
|
|
25
|
-
twitchSetupWizard,
|
|
26
|
-
} from "./setup-surface.js";
|
|
27
|
-
import type { TwitchAccountConfig } from "./types.js";
|
|
28
|
-
|
|
29
|
-
// Mock the helpers we're testing
|
|
30
|
-
const mockPromptText = vi.fn();
|
|
31
|
-
const mockPromptConfirm = vi.fn();
|
|
32
|
-
const mockPromptNote = vi.fn();
|
|
33
|
-
const mockPrompter: WizardPrompter = {
|
|
34
|
-
text: mockPromptText,
|
|
35
|
-
confirm: mockPromptConfirm,
|
|
36
|
-
note: mockPromptNote,
|
|
37
|
-
} as unknown as WizardPrompter;
|
|
38
|
-
const originalEnvToken = process.env.KLAW_TWITCH_ACCESS_TOKEN;
|
|
39
|
-
|
|
40
|
-
const mockAccount: TwitchAccountConfig = {
|
|
41
|
-
username: "testbot",
|
|
42
|
-
accessToken: "oauth:test123",
|
|
43
|
-
clientId: "test-client-id",
|
|
44
|
-
channel: "#testchannel",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function requireFirstTextPromptArgs(): {
|
|
48
|
-
message?: string;
|
|
49
|
-
initialValue?: string;
|
|
50
|
-
validate?: (value: string) => string | undefined;
|
|
51
|
-
} {
|
|
52
|
-
const [call] = mockPromptText.mock.calls;
|
|
53
|
-
if (!call || typeof call[0] !== "object" || call[0] === null || Array.isArray(call[0])) {
|
|
54
|
-
throw new Error("expected Twitch text prompt args");
|
|
55
|
-
}
|
|
56
|
-
return call[0] as {
|
|
57
|
-
message?: string;
|
|
58
|
-
initialValue?: string;
|
|
59
|
-
validate?: (value: string) => string | undefined;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe("setup surface helpers", () => {
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
vi.clearAllMocks();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
if (originalEnvToken === undefined) {
|
|
70
|
-
delete process.env.KLAW_TWITCH_ACCESS_TOKEN;
|
|
71
|
-
} else {
|
|
72
|
-
process.env.KLAW_TWITCH_ACCESS_TOKEN = originalEnvToken;
|
|
73
|
-
}
|
|
74
|
-
// Don't restoreAllMocks as it breaks module-level mocks
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("promptToken", () => {
|
|
78
|
-
it("should return existing token when user confirms to keep it", async () => {
|
|
79
|
-
mockPromptConfirm.mockResolvedValue(true);
|
|
80
|
-
|
|
81
|
-
const result = await promptToken(mockPrompter, mockAccount, undefined);
|
|
82
|
-
|
|
83
|
-
expect(result).toBe("oauth:test123");
|
|
84
|
-
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
|
85
|
-
message: "Access token already configured. Keep it?",
|
|
86
|
-
initialValue: true,
|
|
87
|
-
});
|
|
88
|
-
expect(mockPromptText).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("should validate token format", async () => {
|
|
92
|
-
// Set up mocks - user doesn't want to keep existing token
|
|
93
|
-
mockPromptConfirm.mockResolvedValueOnce(false);
|
|
94
|
-
|
|
95
|
-
// Track how many times promptText is called
|
|
96
|
-
let promptTextCallCount = 0;
|
|
97
|
-
let capturedValidate: ((value: string) => string | undefined) | undefined;
|
|
98
|
-
|
|
99
|
-
mockPromptText.mockImplementationOnce((_args) => {
|
|
100
|
-
promptTextCallCount++;
|
|
101
|
-
// Capture the validate function from the first argument
|
|
102
|
-
if (_args?.validate) {
|
|
103
|
-
capturedValidate = _args.validate;
|
|
104
|
-
}
|
|
105
|
-
return Promise.resolve("oauth:test123");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Call promptToken
|
|
109
|
-
const result = await promptToken(mockPrompter, mockAccount, undefined);
|
|
110
|
-
|
|
111
|
-
// Verify promptText was called
|
|
112
|
-
expect(promptTextCallCount).toBe(1);
|
|
113
|
-
expect(result).toBe("oauth:test123");
|
|
114
|
-
|
|
115
|
-
// Test the validate function
|
|
116
|
-
if (!capturedValidate) {
|
|
117
|
-
throw new Error("promptToken validate callback was not captured");
|
|
118
|
-
}
|
|
119
|
-
expect(capturedValidate("")).toBe("Required");
|
|
120
|
-
expect(capturedValidate("notoauth")).toBe("Token should start with 'oauth:'");
|
|
121
|
-
expect(capturedValidate("oauth:goodtoken")).toBeUndefined();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("promptUsername", () => {
|
|
126
|
-
it("should prompt for username with validation", async () => {
|
|
127
|
-
mockPromptText.mockResolvedValue("mybot");
|
|
128
|
-
|
|
129
|
-
const result = await promptUsername(mockPrompter, null);
|
|
130
|
-
|
|
131
|
-
expect(result).toBe("mybot");
|
|
132
|
-
const promptArgs = requireFirstTextPromptArgs();
|
|
133
|
-
expect(promptArgs.message).toBe("Twitch bot username");
|
|
134
|
-
expect(promptArgs.initialValue).toBe("");
|
|
135
|
-
expect(promptArgs.validate?.("")).toBe("Required");
|
|
136
|
-
expect(promptArgs.validate?.("mybot")).toBeUndefined();
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("promptClientId", () => {
|
|
141
|
-
it("should prompt for client ID with validation", async () => {
|
|
142
|
-
mockPromptText.mockResolvedValue("abc123xyz");
|
|
143
|
-
|
|
144
|
-
const result = await promptClientId(mockPrompter, null);
|
|
145
|
-
|
|
146
|
-
expect(result).toBe("abc123xyz");
|
|
147
|
-
const promptArgs = requireFirstTextPromptArgs();
|
|
148
|
-
expect(promptArgs.message).toBe("Twitch Client ID");
|
|
149
|
-
expect(promptArgs.initialValue).toBe("");
|
|
150
|
-
expect(promptArgs.validate?.("")).toBe("Required");
|
|
151
|
-
expect(promptArgs.validate?.("abc123xyz")).toBeUndefined();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("promptChannelName", () => {
|
|
156
|
-
it("should require a non-empty channel name", async () => {
|
|
157
|
-
mockPromptText.mockResolvedValue("");
|
|
158
|
-
|
|
159
|
-
await promptChannelName(mockPrompter, null);
|
|
160
|
-
|
|
161
|
-
const { validate } = requireFirstTextPromptArgs();
|
|
162
|
-
expect(validate?.("")).toBe("Required");
|
|
163
|
-
expect(validate?.(" ")).toBe("Required");
|
|
164
|
-
expect(validate?.("#chan")).toBeUndefined();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("promptRefreshTokenSetup", () => {
|
|
169
|
-
it("should return empty object when user declines", async () => {
|
|
170
|
-
mockPromptConfirm.mockResolvedValue(false);
|
|
171
|
-
|
|
172
|
-
const result = await promptRefreshTokenSetup(mockPrompter, mockAccount);
|
|
173
|
-
|
|
174
|
-
expect(result).toStrictEqual({});
|
|
175
|
-
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
|
176
|
-
message: "Enable automatic token refresh (requires client secret and refresh token)?",
|
|
177
|
-
initialValue: false,
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("should prompt for credentials when user accepts", async () => {
|
|
182
|
-
mockPromptConfirm
|
|
183
|
-
.mockResolvedValueOnce(true) // First call: useRefresh
|
|
184
|
-
.mockResolvedValueOnce("secret123") // clientSecret
|
|
185
|
-
.mockResolvedValueOnce("refresh123"); // refreshToken
|
|
186
|
-
|
|
187
|
-
mockPromptText.mockResolvedValueOnce("secret123").mockResolvedValueOnce("refresh123");
|
|
188
|
-
|
|
189
|
-
const result = await promptRefreshTokenSetup(mockPrompter, null);
|
|
190
|
-
|
|
191
|
-
expect(result).toEqual({
|
|
192
|
-
clientSecret: "secret123",
|
|
193
|
-
refreshToken: "refresh123",
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("configureWithEnvToken", () => {
|
|
199
|
-
it("should prompt for username and clientId when using env token", async () => {
|
|
200
|
-
// Reset and set up mocks - user accepts env token
|
|
201
|
-
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
|
202
|
-
|
|
203
|
-
// Set up mocks for username and clientId prompts
|
|
204
|
-
mockPromptText
|
|
205
|
-
.mockReset()
|
|
206
|
-
.mockResolvedValueOnce("testbot" as never)
|
|
207
|
-
.mockResolvedValueOnce("test-client-id" as never);
|
|
208
|
-
|
|
209
|
-
const result = await configureWithEnvToken(
|
|
210
|
-
{} as Parameters<typeof configureWithEnvToken>[0],
|
|
211
|
-
mockPrompter,
|
|
212
|
-
null,
|
|
213
|
-
"oauth:fromenv",
|
|
214
|
-
false,
|
|
215
|
-
{} as Parameters<typeof configureWithEnvToken>[5],
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Should return config with username and clientId
|
|
219
|
-
if (!result) {
|
|
220
|
-
throw new Error("expected Twitch env-token setup result");
|
|
221
|
-
}
|
|
222
|
-
const defaultAccount = result.cfg.channels?.twitch?.accounts?.default as
|
|
223
|
-
| { username?: string; clientId?: string }
|
|
224
|
-
| undefined;
|
|
225
|
-
expect(defaultAccount?.username).toBe("testbot");
|
|
226
|
-
expect(defaultAccount?.clientId).toBe("test-client-id");
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("skips env-token shortcut for non-default accounts", async () => {
|
|
230
|
-
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
|
|
231
|
-
mockPromptText
|
|
232
|
-
.mockReset()
|
|
233
|
-
.mockResolvedValueOnce("secondary-bot" as never)
|
|
234
|
-
.mockResolvedValueOnce("secondary-client" as never);
|
|
235
|
-
|
|
236
|
-
const result = await configureWithEnvToken(
|
|
237
|
-
{
|
|
238
|
-
channels: {
|
|
239
|
-
twitch: {
|
|
240
|
-
defaultAccount: "secondary",
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
} as Parameters<typeof configureWithEnvToken>[0],
|
|
244
|
-
mockPrompter,
|
|
245
|
-
null,
|
|
246
|
-
"oauth:fromenv",
|
|
247
|
-
false,
|
|
248
|
-
{} as Parameters<typeof configureWithEnvToken>[5],
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
expect(result).toBeNull();
|
|
252
|
-
expect(mockPromptConfirm).not.toHaveBeenCalled();
|
|
253
|
-
expect(mockPromptText).not.toHaveBeenCalled();
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe("defaultAccount setup resolution", () => {
|
|
258
|
-
it("reports status for the configured default account", () => {
|
|
259
|
-
const lines = twitchSetupWizard.status?.resolveStatusLines?.({
|
|
260
|
-
cfg: {
|
|
261
|
-
channels: {
|
|
262
|
-
twitch: {
|
|
263
|
-
defaultAccount: "secondary",
|
|
264
|
-
accounts: {
|
|
265
|
-
secondary: {
|
|
266
|
-
username: "secondary-bot",
|
|
267
|
-
accessToken: "oauth:secondary",
|
|
268
|
-
clientId: "secondary-client",
|
|
269
|
-
channel: "#secondary",
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
} as never);
|
|
276
|
-
|
|
277
|
-
expect(lines).toEqual(["Twitch (secondary): configured"]);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("reports status for the requested account override", () => {
|
|
281
|
-
const lines = twitchSetupWizard.status?.resolveStatusLines?.({
|
|
282
|
-
cfg: {
|
|
283
|
-
channels: {
|
|
284
|
-
twitch: {
|
|
285
|
-
accounts: {
|
|
286
|
-
default: {
|
|
287
|
-
username: "default-bot",
|
|
288
|
-
accessToken: "oauth:default",
|
|
289
|
-
clientId: "default-client",
|
|
290
|
-
channel: "#default",
|
|
291
|
-
},
|
|
292
|
-
secondary: {
|
|
293
|
-
username: "secondary-bot",
|
|
294
|
-
accessToken: "oauth:secondary",
|
|
295
|
-
clientId: "secondary-client",
|
|
296
|
-
channel: "#secondary",
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
accountId: "secondary",
|
|
303
|
-
configured: true,
|
|
304
|
-
} as never);
|
|
305
|
-
|
|
306
|
-
expect(lines).toEqual(["Twitch (secondary): configured"]);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("reports env-token default account setup as configured", async () => {
|
|
310
|
-
process.env.KLAW_TWITCH_ACCESS_TOKEN = "oauth:fromenv";
|
|
311
|
-
|
|
312
|
-
const cfg = {
|
|
313
|
-
channels: {
|
|
314
|
-
twitch: {
|
|
315
|
-
accounts: {
|
|
316
|
-
default: {
|
|
317
|
-
username: "env-bot",
|
|
318
|
-
accessToken: "",
|
|
319
|
-
clientId: "env-client",
|
|
320
|
-
channel: "#env",
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
} as Parameters<NonNullable<typeof twitchSetupWizard.status>["resolveConfigured"]>[0]["cfg"];
|
|
326
|
-
|
|
327
|
-
expect(twitchSetupWizard.status?.resolveConfigured({ cfg })).toBe(true);
|
|
328
|
-
const account = twitchSetupPlugin.config.resolveAccount(cfg, "default");
|
|
329
|
-
expect(await twitchSetupPlugin.config.isConfigured?.(account, cfg)).toBe(true);
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
describe("setup wizard account routing", () => {
|
|
334
|
-
type FinalizeArgs = Parameters<NonNullable<typeof twitchSetupWizard.finalize>>[0];
|
|
335
|
-
|
|
336
|
-
async function finalizeTwitchSetupForAccount(cfg: FinalizeArgs["cfg"]) {
|
|
337
|
-
return await twitchSetupWizard.finalize?.({
|
|
338
|
-
cfg,
|
|
339
|
-
accountId: "secondary",
|
|
340
|
-
credentialValues: {},
|
|
341
|
-
runtime: {} as FinalizeArgs["runtime"],
|
|
342
|
-
prompter: mockPrompter,
|
|
343
|
-
options: {},
|
|
344
|
-
forceAllowFrom: false,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
it("rejects reserved account ids before using them as config keys", () => {
|
|
349
|
-
expect(() =>
|
|
350
|
-
setTwitchAccount(
|
|
351
|
-
{} as Parameters<typeof setTwitchAccount>[0],
|
|
352
|
-
{
|
|
353
|
-
username: "reserved-bot",
|
|
354
|
-
accessToken: "oauth:reserved",
|
|
355
|
-
clientId: "reserved-client",
|
|
356
|
-
channel: "#reserved",
|
|
357
|
-
},
|
|
358
|
-
"__proto__",
|
|
359
|
-
),
|
|
360
|
-
).toThrow("Invalid Twitch account id");
|
|
361
|
-
|
|
362
|
-
expect(Object.prototype).not.toHaveProperty("username");
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("rejects reserved account ids before env-token writes", async () => {
|
|
366
|
-
await expect(
|
|
367
|
-
configureWithEnvToken(
|
|
368
|
-
{} as Parameters<typeof configureWithEnvToken>[0],
|
|
369
|
-
mockPrompter,
|
|
370
|
-
null,
|
|
371
|
-
"oauth:fromenv",
|
|
372
|
-
false,
|
|
373
|
-
{} as Parameters<typeof configureWithEnvToken>[5],
|
|
374
|
-
"__proto__",
|
|
375
|
-
),
|
|
376
|
-
).rejects.toThrow("Invalid Twitch account id");
|
|
377
|
-
|
|
378
|
-
expect(mockPromptConfirm).not.toHaveBeenCalled();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("normalizes account ids before rendering status lines", () => {
|
|
382
|
-
expect(
|
|
383
|
-
twitchSetupWizard.status?.resolveStatusLines?.({
|
|
384
|
-
cfg: {},
|
|
385
|
-
accountId: "Alerts\r\n\u001b[31m",
|
|
386
|
-
configured: false,
|
|
387
|
-
} as never),
|
|
388
|
-
).toEqual(["Twitch (alerts-31m): needs username, token, and clientId"]);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("reports account-scoped DM policy config keys", () => {
|
|
392
|
-
expect(
|
|
393
|
-
twitchSetupWizard.dmPolicy?.resolveConfigKeys?.(
|
|
394
|
-
{
|
|
395
|
-
channels: {
|
|
396
|
-
twitch: {
|
|
397
|
-
defaultAccount: "secondary",
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
} as Parameters<
|
|
401
|
-
NonNullable<NonNullable<typeof twitchSetupWizard.dmPolicy>["resolveConfigKeys"]>
|
|
402
|
-
>[0],
|
|
403
|
-
undefined,
|
|
404
|
-
),
|
|
405
|
-
).toEqual({
|
|
406
|
-
policyKey: "channels.twitch.accounts.secondary.allowedRoles",
|
|
407
|
-
allowFromKey: "channels.twitch.accounts.secondary.allowFrom",
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
expect(twitchSetupWizard.dmPolicy?.resolveConfigKeys?.({} as never, "alerts")).toEqual({
|
|
411
|
-
policyKey: "channels.twitch.accounts.alerts.allowedRoles",
|
|
412
|
-
allowFromKey: "channels.twitch.accounts.alerts.allowFrom",
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("writes to the requested account when defaultAccount is not created yet", async () => {
|
|
417
|
-
mockPromptText
|
|
418
|
-
.mockReset()
|
|
419
|
-
.mockResolvedValueOnce("secondary-bot" as never)
|
|
420
|
-
.mockResolvedValueOnce("oauth:secondary" as never)
|
|
421
|
-
.mockResolvedValueOnce("secondary-client" as never)
|
|
422
|
-
.mockResolvedValueOnce("#secondary" as never);
|
|
423
|
-
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
|
|
424
|
-
|
|
425
|
-
const result = await finalizeTwitchSetupForAccount({
|
|
426
|
-
channels: {
|
|
427
|
-
twitch: {
|
|
428
|
-
defaultAccount: "secondary",
|
|
429
|
-
accounts: {
|
|
430
|
-
default: {
|
|
431
|
-
username: "default-bot",
|
|
432
|
-
accessToken: "oauth:default",
|
|
433
|
-
clientId: "default-client",
|
|
434
|
-
channel: "#default",
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
} as FinalizeArgs["cfg"]);
|
|
440
|
-
|
|
441
|
-
const twitch = result?.cfg?.channels?.twitch;
|
|
442
|
-
expect(twitch?.accounts?.secondary?.username).toBe("secondary-bot");
|
|
443
|
-
expect(twitch?.accounts?.secondary?.accessToken).toBe("oauth:secondary");
|
|
444
|
-
expect(twitch?.accounts?.default?.username).toBe("default-bot");
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it("persists a token instead of using env-token shortcut for non-default finalize", async () => {
|
|
448
|
-
process.env.KLAW_TWITCH_ACCESS_TOKEN = "oauth:fromenv";
|
|
449
|
-
mockPromptText
|
|
450
|
-
.mockReset()
|
|
451
|
-
.mockResolvedValueOnce("secondary-bot" as never)
|
|
452
|
-
.mockResolvedValueOnce("oauth:persisted" as never)
|
|
453
|
-
.mockResolvedValueOnce("secondary-client" as never)
|
|
454
|
-
.mockResolvedValueOnce("#secondary" as never);
|
|
455
|
-
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
|
|
456
|
-
|
|
457
|
-
const result = await finalizeTwitchSetupForAccount({
|
|
458
|
-
channels: {
|
|
459
|
-
twitch: {
|
|
460
|
-
accounts: {},
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
} as FinalizeArgs["cfg"]);
|
|
464
|
-
|
|
465
|
-
const twitch = result?.cfg?.channels?.twitch;
|
|
466
|
-
expect(twitch?.accounts?.secondary?.accessToken).toBe("oauth:persisted");
|
|
467
|
-
expect(mockPromptConfirm).toHaveBeenCalledTimes(1);
|
|
468
|
-
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
|
469
|
-
message: "Enable automatic token refresh (requires client secret and refresh token)?",
|
|
470
|
-
initialValue: false,
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
describe("setup-only plugin config", () => {
|
|
476
|
-
it("lists all configured Twitch accounts", () => {
|
|
477
|
-
const cfg = {
|
|
478
|
-
channels: {
|
|
479
|
-
twitch: {
|
|
480
|
-
defaultAccount: "secondary",
|
|
481
|
-
accounts: {
|
|
482
|
-
default: {
|
|
483
|
-
username: "default-bot",
|
|
484
|
-
accessToken: "oauth:default",
|
|
485
|
-
clientId: "default-client",
|
|
486
|
-
channel: "#default",
|
|
487
|
-
},
|
|
488
|
-
secondary: {
|
|
489
|
-
username: "secondary-bot",
|
|
490
|
-
accessToken: "oauth:secondary",
|
|
491
|
-
clientId: "secondary-client",
|
|
492
|
-
channel: "#secondary",
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
},
|
|
497
|
-
} as Parameters<typeof twitchSetupPlugin.config.listAccountIds>[0];
|
|
498
|
-
|
|
499
|
-
expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["default", "secondary"]);
|
|
500
|
-
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("normalizes exposed account ids", () => {
|
|
504
|
-
const cfg = {
|
|
505
|
-
channels: {
|
|
506
|
-
twitch: {
|
|
507
|
-
accounts: {
|
|
508
|
-
Secondary: {
|
|
509
|
-
username: "secondary-bot",
|
|
510
|
-
accessToken: "oauth:secondary",
|
|
511
|
-
clientId: "secondary-client",
|
|
512
|
-
channel: "#secondary",
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
} as Parameters<typeof twitchSetupPlugin.config.listAccountIds>[0];
|
|
518
|
-
|
|
519
|
-
expect(twitchSetupPlugin.config.listAccountIds(cfg)).toEqual(["secondary"]);
|
|
520
|
-
expect(twitchSetupPlugin.config.defaultAccountId?.(cfg)).toBe("secondary");
|
|
521
|
-
expect(twitchSetupPlugin.config.resolveAccount(cfg, "SECONDARY\r\n").accountId).toBe(
|
|
522
|
-
"secondary",
|
|
523
|
-
);
|
|
524
|
-
expect(twitchSetupPlugin.config.resolveAccount(cfg, "SECONDARY\r\n").username).toBe(
|
|
525
|
-
"secondary-bot",
|
|
526
|
-
);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
});
|