@openclaw/twitch 2026.2.21 → 2026.5.1-beta.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/README.md +2 -2
- package/api.ts +21 -0
- package/channel-plugin-api.ts +1 -0
- package/index.test.ts +13 -0
- package/index.ts +12 -16
- package/openclaw.plugin.json +6 -0
- package/package.json +29 -7
- package/runtime-api.ts +22 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/access-control.test.ts +136 -239
- package/src/access-control.ts +11 -4
- package/src/actions.test.ts +74 -0
- package/src/actions.ts +7 -6
- package/src/client-manager-registry.ts +0 -28
- package/src/config-schema.ts +2 -2
- package/src/config.test.ts +147 -1
- package/src/config.ts +76 -15
- package/src/monitor.ts +126 -85
- package/src/outbound.test.ts +140 -75
- package/src/outbound.ts +4 -5
- package/src/plugin.test.ts +39 -1
- package/src/plugin.ts +176 -242
- package/src/probe.test.ts +1 -1
- package/src/probe.ts +16 -5
- package/src/resolver.ts +5 -3
- package/src/runtime.ts +8 -13
- package/src/send.test.ts +92 -59
- package/src/send.ts +18 -15
- package/src/setup-surface.test.ts +511 -0
- package/src/setup-surface.ts +520 -0
- package/src/status.test.ts +120 -153
- package/src/status.ts +1 -1
- package/src/test-fixtures.ts +1 -1
- package/src/token.test.ts +22 -1
- package/src/token.ts +8 -6
- package/src/twitch-client.test.ts +18 -25
- package/src/twitch-client.ts +5 -6
- package/src/types.ts +7 -46
- package/src/utils/twitch.ts +8 -2
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -21
- package/src/onboarding.test.ts +0 -316
- package/src/onboarding.ts +0 -417
package/src/status.test.ts
CHANGED
|
@@ -14,17 +14,28 @@ import { describe, expect, it } from "vitest";
|
|
|
14
14
|
import { collectTwitchStatusIssues } from "./status.js";
|
|
15
15
|
import type { ChannelAccountSnapshot } from "./types.js";
|
|
16
16
|
|
|
17
|
+
function createSnapshot(overrides: Partial<ChannelAccountSnapshot> = {}): ChannelAccountSnapshot {
|
|
18
|
+
return {
|
|
19
|
+
accountId: "default",
|
|
20
|
+
configured: true,
|
|
21
|
+
enabled: true,
|
|
22
|
+
running: false,
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createSimpleTwitchConfig(overrides: Record<string, unknown>) {
|
|
28
|
+
return {
|
|
29
|
+
channels: {
|
|
30
|
+
twitch: overrides,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
describe("status", () => {
|
|
18
36
|
describe("collectTwitchStatusIssues", () => {
|
|
19
37
|
it("should detect unconfigured accounts", () => {
|
|
20
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
21
|
-
{
|
|
22
|
-
accountId: "default",
|
|
23
|
-
configured: false,
|
|
24
|
-
enabled: true,
|
|
25
|
-
running: false,
|
|
26
|
-
},
|
|
27
|
-
];
|
|
38
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot({ configured: false })];
|
|
28
39
|
|
|
29
40
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
30
41
|
|
|
@@ -34,215 +45,166 @@ describe("status", () => {
|
|
|
34
45
|
});
|
|
35
46
|
|
|
36
47
|
it("should detect disabled accounts", () => {
|
|
37
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
38
|
-
{
|
|
39
|
-
accountId: "default",
|
|
40
|
-
configured: true,
|
|
41
|
-
enabled: false,
|
|
42
|
-
running: false,
|
|
43
|
-
},
|
|
44
|
-
];
|
|
48
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot({ enabled: false })];
|
|
45
49
|
|
|
46
50
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
47
51
|
|
|
48
52
|
expect(issues.length).toBeGreaterThan(0);
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
expect(issues).toContainEqual(
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
kind: "config",
|
|
56
|
+
message: "Twitch account is disabled",
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
51
59
|
});
|
|
52
60
|
|
|
53
61
|
it("should detect missing clientId when account configured (simplified config)", () => {
|
|
54
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const mockCfg = {
|
|
64
|
-
channels: {
|
|
65
|
-
twitch: {
|
|
66
|
-
username: "testbot",
|
|
67
|
-
accessToken: "oauth:test123",
|
|
68
|
-
// clientId missing
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
};
|
|
62
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
|
|
63
|
+
const mockCfg = createSimpleTwitchConfig({
|
|
64
|
+
username: "testbot",
|
|
65
|
+
accessToken: "oauth:test123",
|
|
66
|
+
// clientId missing
|
|
67
|
+
});
|
|
72
68
|
|
|
73
69
|
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
expect(issues).toContainEqual(
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
kind: "config",
|
|
74
|
+
message: "Twitch client ID is required",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it("should warn about oauth: prefix in token (simplified config)", () => {
|
|
80
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
const mockCfg = {
|
|
90
|
-
channels: {
|
|
91
|
-
twitch: {
|
|
92
|
-
username: "testbot",
|
|
93
|
-
accessToken: "oauth:test123", // has prefix
|
|
94
|
-
clientId: "test-id",
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
};
|
|
80
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
|
|
81
|
+
const mockCfg = createSimpleTwitchConfig({
|
|
82
|
+
username: "testbot",
|
|
83
|
+
accessToken: "oauth:test123", // has prefix
|
|
84
|
+
clientId: "test-id",
|
|
85
|
+
});
|
|
98
86
|
|
|
99
87
|
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
expect(issues).toContainEqual(
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
kind: "config",
|
|
92
|
+
message: "Token contains 'oauth:' prefix (will be stripped)",
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
104
95
|
});
|
|
105
96
|
|
|
106
97
|
it("should detect clientSecret without refreshToken (simplified config)", () => {
|
|
107
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const mockCfg = {
|
|
117
|
-
channels: {
|
|
118
|
-
twitch: {
|
|
119
|
-
username: "testbot",
|
|
120
|
-
accessToken: "oauth:test123",
|
|
121
|
-
clientId: "test-id",
|
|
122
|
-
clientSecret: "secret123",
|
|
123
|
-
// refreshToken missing
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
};
|
|
98
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
|
|
99
|
+
const mockCfg = createSimpleTwitchConfig({
|
|
100
|
+
username: "testbot",
|
|
101
|
+
accessToken: "oauth:test123",
|
|
102
|
+
clientId: "test-id",
|
|
103
|
+
clientSecret: "secret123",
|
|
104
|
+
// refreshToken missing
|
|
105
|
+
});
|
|
127
106
|
|
|
128
107
|
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
|
129
108
|
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
expect(issues).toContainEqual(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
kind: "config",
|
|
112
|
+
message: "clientSecret provided without refreshToken",
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
132
115
|
});
|
|
133
116
|
|
|
134
117
|
it("should detect empty allowFrom array (simplified config)", () => {
|
|
135
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
const mockCfg = {
|
|
145
|
-
channels: {
|
|
146
|
-
twitch: {
|
|
147
|
-
username: "testbot",
|
|
148
|
-
accessToken: "test123",
|
|
149
|
-
clientId: "test-id",
|
|
150
|
-
allowFrom: [], // empty array
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
};
|
|
118
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
|
|
119
|
+
const mockCfg = createSimpleTwitchConfig({
|
|
120
|
+
username: "testbot",
|
|
121
|
+
accessToken: "test123",
|
|
122
|
+
clientId: "test-id",
|
|
123
|
+
allowFrom: [], // empty array
|
|
124
|
+
});
|
|
154
125
|
|
|
155
126
|
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
|
156
127
|
|
|
157
|
-
|
|
158
|
-
|
|
128
|
+
expect(issues).toContainEqual(
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
kind: "config",
|
|
131
|
+
message: "allowFrom is configured but empty",
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
159
134
|
});
|
|
160
135
|
|
|
161
136
|
it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => {
|
|
162
|
-
const snapshots: ChannelAccountSnapshot[] = [
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const mockCfg = {
|
|
172
|
-
channels: {
|
|
173
|
-
twitch: {
|
|
174
|
-
username: "testbot",
|
|
175
|
-
accessToken: "test123",
|
|
176
|
-
clientId: "test-id",
|
|
177
|
-
allowedRoles: ["all"],
|
|
178
|
-
allowFrom: ["123456"], // conflict!
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
137
|
+
const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
|
|
138
|
+
const mockCfg = createSimpleTwitchConfig({
|
|
139
|
+
username: "testbot",
|
|
140
|
+
accessToken: "test123",
|
|
141
|
+
clientId: "test-id",
|
|
142
|
+
allowedRoles: ["all"],
|
|
143
|
+
allowFrom: ["123456"], // conflict!
|
|
144
|
+
});
|
|
182
145
|
|
|
183
146
|
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
|
184
147
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
148
|
+
expect(issues).toContainEqual(
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
kind: "intent",
|
|
151
|
+
message: "allowedRoles is set to 'all' but allowFrom is also configured",
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
188
154
|
});
|
|
189
155
|
|
|
190
156
|
it("should detect runtime errors", () => {
|
|
191
157
|
const snapshots: ChannelAccountSnapshot[] = [
|
|
192
|
-
{
|
|
193
|
-
accountId: "default",
|
|
194
|
-
configured: true,
|
|
195
|
-
enabled: true,
|
|
196
|
-
running: false,
|
|
197
|
-
lastError: "Connection timeout",
|
|
198
|
-
},
|
|
158
|
+
createSnapshot({ lastError: "Connection timeout" }),
|
|
199
159
|
];
|
|
200
160
|
|
|
201
161
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
202
162
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
163
|
+
expect(issues).toContainEqual(
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
kind: "runtime",
|
|
166
|
+
message: "Last error: Connection timeout",
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
206
169
|
});
|
|
207
170
|
|
|
208
171
|
it("should detect accounts that never connected", () => {
|
|
209
172
|
const snapshots: ChannelAccountSnapshot[] = [
|
|
210
|
-
{
|
|
211
|
-
accountId: "default",
|
|
212
|
-
configured: true,
|
|
213
|
-
enabled: true,
|
|
214
|
-
running: false,
|
|
173
|
+
createSnapshot({
|
|
215
174
|
lastStartAt: undefined,
|
|
216
175
|
lastInboundAt: undefined,
|
|
217
176
|
lastOutboundAt: undefined,
|
|
218
|
-
},
|
|
177
|
+
}),
|
|
219
178
|
];
|
|
220
179
|
|
|
221
180
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
222
181
|
|
|
223
|
-
|
|
224
|
-
|
|
182
|
+
expect(issues).toContainEqual(
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
kind: "runtime",
|
|
185
|
+
message: "Account has never connected successfully",
|
|
186
|
+
}),
|
|
225
187
|
);
|
|
226
|
-
expect(neverConnectedIssue).toBeDefined();
|
|
227
188
|
});
|
|
228
189
|
|
|
229
190
|
it("should detect long-running connections", () => {
|
|
230
191
|
const oldDate = Date.now() - 8 * 24 * 60 * 60 * 1000; // 8 days ago
|
|
231
192
|
|
|
232
193
|
const snapshots: ChannelAccountSnapshot[] = [
|
|
233
|
-
{
|
|
234
|
-
accountId: "default",
|
|
235
|
-
configured: true,
|
|
236
|
-
enabled: true,
|
|
194
|
+
createSnapshot({
|
|
237
195
|
running: true,
|
|
238
196
|
lastStartAt: oldDate,
|
|
239
|
-
},
|
|
197
|
+
}),
|
|
240
198
|
];
|
|
241
199
|
|
|
242
200
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
243
201
|
|
|
244
|
-
|
|
245
|
-
|
|
202
|
+
expect(issues).toContainEqual(
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
kind: "runtime",
|
|
205
|
+
message: "Connection has been running for 8 days",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
246
208
|
});
|
|
247
209
|
|
|
248
210
|
it("should handle empty snapshots array", () => {
|
|
@@ -263,8 +225,13 @@ describe("status", () => {
|
|
|
263
225
|
|
|
264
226
|
const issues = collectTwitchStatusIssues(snapshots);
|
|
265
227
|
|
|
266
|
-
|
|
267
|
-
|
|
228
|
+
expect(issues).toEqual([
|
|
229
|
+
expect.objectContaining({
|
|
230
|
+
accountId: "unknown",
|
|
231
|
+
kind: "config",
|
|
232
|
+
message: "Twitch account is not properly configured",
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
268
235
|
});
|
|
269
236
|
});
|
|
270
237
|
});
|
package/src/status.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Detects and reports configuration issues for Twitch accounts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ChannelStatusIssue } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { ChannelStatusIssue } from "openclaw/plugin-sdk/channel-contract";
|
|
8
8
|
import { getAccountConfig } from "./config.js";
|
|
9
9
|
import { resolveTwitchToken } from "./token.js";
|
|
10
10
|
import type { ChannelAccountSnapshot } from "./types.js";
|
package/src/test-fixtures.ts
CHANGED
package/src/token.test.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - Account ID normalization
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
12
11
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { OpenClawConfig } from "../api.js";
|
|
13
13
|
import { resolveTwitchToken, type TwitchTokenSource } from "./token.js";
|
|
14
14
|
|
|
15
15
|
describe("token", () => {
|
|
@@ -65,6 +65,27 @@ describe("token", () => {
|
|
|
65
65
|
expect(result.source).toBe("config");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("should resolve token from normalized account id", () => {
|
|
69
|
+
const result = resolveTwitchToken(
|
|
70
|
+
{
|
|
71
|
+
channels: {
|
|
72
|
+
twitch: {
|
|
73
|
+
accounts: {
|
|
74
|
+
Secondary: {
|
|
75
|
+
username: "secondary",
|
|
76
|
+
accessToken: "oauth:secondary-token",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
} as unknown as OpenClawConfig,
|
|
82
|
+
{ accountId: "secondary" },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(result.token).toBe("oauth:secondary-token");
|
|
86
|
+
expect(result.source).toBe("config");
|
|
87
|
+
});
|
|
88
|
+
|
|
68
89
|
it("should prioritize config token over env var (simplified config)", () => {
|
|
69
90
|
process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:env-token";
|
|
70
91
|
|
package/src/token.ts
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_ACCOUNT_ID,
|
|
14
|
+
normalizeAccountId,
|
|
15
|
+
resolveNormalizedAccountEntry,
|
|
16
|
+
} from "openclaw/plugin-sdk/account-resolution";
|
|
17
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
14
18
|
|
|
15
19
|
export type TwitchTokenSource = "env" | "config" | "none";
|
|
16
20
|
|
|
@@ -56,10 +60,8 @@ export function resolveTwitchToken(
|
|
|
56
60
|
|
|
57
61
|
// Get merged account config (handles both simplified and multi-account patterns)
|
|
58
62
|
const twitchCfg = cfg?.channels?.twitch;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
? (twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID] as Record<string, unknown> | undefined)
|
|
62
|
-
: (twitchCfg?.accounts?.[accountId] as Record<string, unknown> | undefined);
|
|
63
|
+
const accounts = twitchCfg?.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
64
|
+
const accountCfg = resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
|
|
63
65
|
|
|
64
66
|
// For default account, also check base-level config
|
|
65
67
|
let token: string | undefined;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - Error handling and edge cases
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { resolveTwitchToken } from "./token.js";
|
|
13
14
|
import { TwitchClientManager } from "./twitch-client.js";
|
|
14
15
|
import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
|
15
16
|
|
|
@@ -21,12 +22,10 @@ const mockQuit = vi.fn();
|
|
|
21
22
|
const mockUnbind = vi.fn();
|
|
22
23
|
|
|
23
24
|
// Event handler storage for testing
|
|
24
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
25
25
|
const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> =
|
|
26
26
|
[];
|
|
27
27
|
|
|
28
28
|
// Mock functions that track handlers and return unbind objects
|
|
29
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
30
29
|
const mockOnMessage = vi.fn((handler: any) => {
|
|
31
30
|
messageHandlers.push(handler);
|
|
32
31
|
return { unbind: mockUnbind };
|
|
@@ -59,10 +58,8 @@ const mockAuthProvider = {
|
|
|
59
58
|
};
|
|
60
59
|
|
|
61
60
|
vi.mock("@twurple/auth", () => ({
|
|
62
|
-
StaticAuthProvider:
|
|
63
|
-
constructor(...args
|
|
64
|
-
mockAuthProvider.constructor(...args);
|
|
65
|
-
}
|
|
61
|
+
StaticAuthProvider: function StaticAuthProvider(...args: unknown[]) {
|
|
62
|
+
mockAuthProvider.constructor(...args);
|
|
66
63
|
},
|
|
67
64
|
RefreshingAuthProvider: class {
|
|
68
65
|
addUserForToken = mockAddUserForToken;
|
|
@@ -83,6 +80,7 @@ vi.mock("./token.js", () => ({
|
|
|
83
80
|
describe("TwitchClientManager", () => {
|
|
84
81
|
let manager: TwitchClientManager;
|
|
85
82
|
let mockLogger: ChannelLogSink;
|
|
83
|
+
let resolveTwitchTokenMock: ReturnType<typeof vi.mocked<typeof resolveTwitchToken>>;
|
|
86
84
|
|
|
87
85
|
const testAccount: TwitchAccountConfig = {
|
|
88
86
|
username: "testbot",
|
|
@@ -100,7 +98,11 @@ describe("TwitchClientManager", () => {
|
|
|
100
98
|
enabled: true,
|
|
101
99
|
};
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
beforeAll(() => {
|
|
102
|
+
resolveTwitchTokenMock = vi.mocked(resolveTwitchToken);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
104
106
|
// Clear all mocks first
|
|
105
107
|
vi.clearAllMocks();
|
|
106
108
|
|
|
@@ -108,8 +110,7 @@ describe("TwitchClientManager", () => {
|
|
|
108
110
|
messageHandlers.length = 0;
|
|
109
111
|
|
|
110
112
|
// Re-set up the default token mock implementation after clearing
|
|
111
|
-
|
|
112
|
-
vi.mocked(resolveTwitchToken).mockReturnValue({
|
|
113
|
+
resolveTwitchTokenMock.mockReturnValue({
|
|
113
114
|
token: "oauth:mock-token-from-tests",
|
|
114
115
|
source: "config" as const,
|
|
115
116
|
});
|
|
@@ -176,8 +177,7 @@ describe("TwitchClientManager", () => {
|
|
|
176
177
|
};
|
|
177
178
|
|
|
178
179
|
// Override the mock to return a specific token for this test
|
|
179
|
-
|
|
180
|
-
vi.mocked(resolveTwitchToken).mockReturnValue({
|
|
180
|
+
resolveTwitchTokenMock.mockReturnValue({
|
|
181
181
|
token: "oauth:actualtoken123",
|
|
182
182
|
source: "config" as const,
|
|
183
183
|
});
|
|
@@ -189,8 +189,7 @@ describe("TwitchClientManager", () => {
|
|
|
189
189
|
|
|
190
190
|
it("should use token directly when no oauth: prefix", async () => {
|
|
191
191
|
// Override the mock to return a token without oauth: prefix
|
|
192
|
-
|
|
193
|
-
vi.mocked(resolveTwitchToken).mockReturnValue({
|
|
192
|
+
resolveTwitchTokenMock.mockReturnValue({
|
|
194
193
|
token: "oauth:mock-token-from-tests",
|
|
195
194
|
source: "config" as const,
|
|
196
195
|
});
|
|
@@ -221,8 +220,7 @@ describe("TwitchClientManager", () => {
|
|
|
221
220
|
|
|
222
221
|
it("should throw error when token is missing", async () => {
|
|
223
222
|
// Override the mock to return empty token
|
|
224
|
-
|
|
225
|
-
vi.mocked(resolveTwitchToken).mockReturnValue({
|
|
223
|
+
resolveTwitchTokenMock.mockReturnValue({
|
|
226
224
|
token: "",
|
|
227
225
|
source: "none" as const,
|
|
228
226
|
});
|
|
@@ -271,7 +269,6 @@ describe("TwitchClientManager", () => {
|
|
|
271
269
|
|
|
272
270
|
// Check the stored handler is handler2
|
|
273
271
|
const key = manager.getAccountKey(testAccount);
|
|
274
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
275
272
|
expect((manager as any).messageHandlers.get(key)).toBe(handler2);
|
|
276
273
|
});
|
|
277
274
|
});
|
|
@@ -293,9 +290,7 @@ describe("TwitchClientManager", () => {
|
|
|
293
290
|
await manager.disconnect(testAccount);
|
|
294
291
|
|
|
295
292
|
const key = manager.getAccountKey(testAccount);
|
|
296
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
297
293
|
expect((manager as any).clients.has(key)).toBe(false);
|
|
298
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
299
294
|
expect((manager as any).messageHandlers.has(key)).toBe(false);
|
|
300
295
|
});
|
|
301
296
|
|
|
@@ -314,7 +309,6 @@ describe("TwitchClientManager", () => {
|
|
|
314
309
|
expect(mockQuit).toHaveBeenCalledTimes(1);
|
|
315
310
|
|
|
316
311
|
const key2 = manager.getAccountKey(testAccount2);
|
|
317
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
318
312
|
expect((manager as any).clients.has(key2)).toBe(true);
|
|
319
313
|
});
|
|
320
314
|
});
|
|
@@ -327,9 +321,7 @@ describe("TwitchClientManager", () => {
|
|
|
327
321
|
await manager.disconnectAll();
|
|
328
322
|
|
|
329
323
|
expect(mockQuit).toHaveBeenCalledTimes(2);
|
|
330
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
331
324
|
expect((manager as any).clients.size).toBe(0);
|
|
332
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
333
325
|
expect((manager as any).messageHandlers.size).toBe(0);
|
|
334
326
|
});
|
|
335
327
|
|
|
@@ -348,8 +340,10 @@ describe("TwitchClientManager", () => {
|
|
|
348
340
|
it("should send message successfully", async () => {
|
|
349
341
|
const result = await manager.sendMessage(testAccount, "testchannel", "Hello, world!");
|
|
350
342
|
|
|
351
|
-
expect(result
|
|
352
|
-
expect(result.messageId).
|
|
343
|
+
expect(result).toMatchObject({ ok: true });
|
|
344
|
+
expect(result.messageId).toMatch(
|
|
345
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
346
|
+
);
|
|
353
347
|
expect(mockSay).toHaveBeenCalledWith("testchannel", "Hello, world!");
|
|
354
348
|
});
|
|
355
349
|
|
|
@@ -395,7 +389,6 @@ describe("TwitchClientManager", () => {
|
|
|
395
389
|
|
|
396
390
|
it("should create client if not already connected", async () => {
|
|
397
391
|
// Clear the existing client
|
|
398
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
399
392
|
(manager as any).clients.clear();
|
|
400
393
|
|
|
401
394
|
// Reset connect call count for this specific test
|
package/src/twitch-client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
|
2
2
|
import { ChatClient, LogLevel } from "@twurple/chat";
|
|
3
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
4
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
5
|
import { resolveTwitchToken } from "./token.js";
|
|
5
6
|
import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
|
6
7
|
import { normalizeToken } from "./utils/twitch.js";
|
|
@@ -45,7 +46,7 @@ export class TwitchClientManager {
|
|
|
45
46
|
})
|
|
46
47
|
.catch((err) => {
|
|
47
48
|
this.logger.error(
|
|
48
|
-
`Failed to add user to RefreshingAuthProvider: ${
|
|
49
|
+
`Failed to add user to RefreshingAuthProvider: ${formatErrorMessage(err)}`,
|
|
49
50
|
);
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -250,12 +251,10 @@ export class TwitchClientManager {
|
|
|
250
251
|
|
|
251
252
|
return { ok: true, messageId };
|
|
252
253
|
} catch (error) {
|
|
253
|
-
this.logger.error(
|
|
254
|
-
`Failed to send message: ${error instanceof Error ? error.message : String(error)}`,
|
|
255
|
-
);
|
|
254
|
+
this.logger.error(`Failed to send message: ${formatErrorMessage(error)}`);
|
|
256
255
|
return {
|
|
257
256
|
ok: false,
|
|
258
|
-
error:
|
|
257
|
+
error: formatErrorMessage(error),
|
|
259
258
|
};
|
|
260
259
|
}
|
|
261
260
|
}
|