pi-harness-runtime 1.1.13 → 1.1.14
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/CHANGELOG.md +7 -26
- package/footer-status.ts +49 -4
- package/harness/glm-quota-countdown.test.ts +323 -0
- package/harness/glm-quota-countdown.ts +615 -0
- package/harness/index.ts +12 -0
- package/package.json +1 -1
- package/packages/quota-manager/dist/tui-usage-monitor.d.ts.map +1 -1
- package/packages/quota-manager/dist/tui-usage-monitor.js +20 -4
- package/packages/quota-manager/dist/tui-usage-monitor.js.map +1 -1
- package/packages/quota-manager/src/tui-usage-monitor.ts +21 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
### [1.1.
|
|
5
|
+
### [1.1.14](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.13...v1.1.14) (2026-08-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
6
9
|
|
|
10
|
+
* **glm-quota:** add robust GLM quota countdown with auto-resume ([cb78a2b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/cb78a2bdf5ecf79fc028030d43e3ce40767253cb))
|
|
11
|
+
|
|
12
|
+
### [1.1.13](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.12...v1.1.13) (2026-08-21)
|
|
7
13
|
|
|
8
14
|
### Bug Fixes
|
|
9
15
|
|
|
@@ -11,14 +17,12 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
11
17
|
|
|
12
18
|
### [1.1.12](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.11...v1.1.12) (2026-08-20)
|
|
13
19
|
|
|
14
|
-
|
|
15
20
|
### Bug Fixes
|
|
16
21
|
|
|
17
22
|
* **runtime:** refresh minimax quota from provider mirror ([a8976b4](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a8976b4ff61e28b964bf76405142672798714647))
|
|
18
23
|
|
|
19
24
|
### [1.1.11](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.42...v1.1.11) (2026-08-20)
|
|
20
25
|
|
|
21
|
-
|
|
22
26
|
### Features
|
|
23
27
|
|
|
24
28
|
* add quota scrapers for ChatGPT and GLM providers ([17f9d0e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/17f9d0e7d6f486210a30244f2fecc0131e8b7d57))
|
|
@@ -26,7 +30,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
26
30
|
* index.ts ([d0fc002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0fc002d6d16e59318155677cb6128846bc993a4))
|
|
27
31
|
* update workflow run name to show version number ([ee8c59f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ee8c59fec4c0116a5948b2ae04ad4609edaed3ac))
|
|
28
32
|
|
|
29
|
-
|
|
30
33
|
### Bug Fixes
|
|
31
34
|
|
|
32
35
|
* add typescript as devDependency, build notification dist ([c76b522](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c76b5221c33262c38615c6117ecf9db13ab7b5d2))
|
|
@@ -49,7 +52,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
49
52
|
|
|
50
53
|
### [1.1.10](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.42...v1.1.10) (2026-08-20)
|
|
51
54
|
|
|
52
|
-
|
|
53
55
|
### Features
|
|
54
56
|
|
|
55
57
|
* add quota scrapers for ChatGPT and GLM providers ([17f9d0e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/17f9d0e7d6f486210a30244f2fecc0131e8b7d57))
|
|
@@ -57,7 +59,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
57
59
|
* index.ts ([d0fc002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0fc002d6d16e59318155677cb6128846bc993a4))
|
|
58
60
|
* update workflow run name to show version number ([ee8c59f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ee8c59fec4c0116a5948b2ae04ad4609edaed3ac))
|
|
59
61
|
|
|
60
|
-
|
|
61
62
|
### Bug Fixes
|
|
62
63
|
|
|
63
64
|
* add typescript as devDependency, build notification dist ([c76b522](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c76b5221c33262c38615c6117ecf9db13ab7b5d2))
|
|
@@ -79,7 +80,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
79
80
|
|
|
80
81
|
### [0.10.30](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.29...v0.10.30) (2026-08-11)
|
|
81
82
|
|
|
82
|
-
|
|
83
83
|
### Bug Fixes
|
|
84
84
|
|
|
85
85
|
* **file-copy-helper:** require explicit intent for copy rule injection ([ddfece0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ddfece0bcbf440dfd4edc0d442b078e84e906c68))
|
|
@@ -98,13 +98,11 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
98
98
|
|
|
99
99
|
### [0.10.27](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.27) (2026-08-10)
|
|
100
100
|
|
|
101
|
-
|
|
102
101
|
### Features
|
|
103
102
|
|
|
104
103
|
* create packages for missing RFC implementations ([e6ce2ff](https://github.com/ManotLuijiu/pi-harness-runtime/commit/e6ce2ffdd455b33c55212ec2b61eedcdcfb43b5e))
|
|
105
104
|
* **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
|
|
106
105
|
|
|
107
|
-
|
|
108
106
|
### Bug Fixes
|
|
109
107
|
|
|
110
108
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -120,12 +118,10 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
120
118
|
|
|
121
119
|
### [0.10.26](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.26) (2026-08-10)
|
|
122
120
|
|
|
123
|
-
|
|
124
121
|
### Features
|
|
125
122
|
|
|
126
123
|
* **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
|
|
127
124
|
|
|
128
|
-
|
|
129
125
|
### Bug Fixes
|
|
130
126
|
|
|
131
127
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -141,12 +137,10 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
141
137
|
|
|
142
138
|
### [0.10.25](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.25) (2026-08-10)
|
|
143
139
|
|
|
144
|
-
|
|
145
140
|
### Features
|
|
146
141
|
|
|
147
142
|
* **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
|
|
148
143
|
|
|
149
|
-
|
|
150
144
|
### Bug Fixes
|
|
151
145
|
|
|
152
146
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -161,12 +155,10 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
161
155
|
|
|
162
156
|
### [0.10.24](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.24) (2026-08-09)
|
|
163
157
|
|
|
164
|
-
|
|
165
158
|
### Features
|
|
166
159
|
|
|
167
160
|
* **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
|
|
168
161
|
|
|
169
|
-
|
|
170
162
|
### Bug Fixes
|
|
171
163
|
|
|
172
164
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -180,12 +172,10 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
180
172
|
|
|
181
173
|
### [0.10.23](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.23) (2026-08-09)
|
|
182
174
|
|
|
183
|
-
|
|
184
175
|
### Features
|
|
185
176
|
|
|
186
177
|
* **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
|
|
187
178
|
|
|
188
|
-
|
|
189
179
|
### Bug Fixes
|
|
190
180
|
|
|
191
181
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -198,7 +188,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
198
188
|
|
|
199
189
|
### [0.10.22](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.22) (2026-08-09)
|
|
200
190
|
|
|
201
|
-
|
|
202
191
|
### Bug Fixes
|
|
203
192
|
|
|
204
193
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -211,7 +200,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
211
200
|
|
|
212
201
|
### [0.10.21](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.21) (2026-08-08)
|
|
213
202
|
|
|
214
|
-
|
|
215
203
|
### Bug Fixes
|
|
216
204
|
|
|
217
205
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -223,7 +211,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
223
211
|
|
|
224
212
|
### [0.10.20](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.20) (2026-08-08)
|
|
225
213
|
|
|
226
|
-
|
|
227
214
|
### Bug Fixes
|
|
228
215
|
|
|
229
216
|
* align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
|
|
@@ -234,14 +221,12 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
234
221
|
|
|
235
222
|
### [0.10.11](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.9...v0.10.11) (2026-08-03)
|
|
236
223
|
|
|
237
|
-
|
|
238
224
|
### Features
|
|
239
225
|
|
|
240
226
|
* add knowledge-retrieval package for TencentDB-Agent-Memory MCP (RFC-0105) ([a4558d0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a4558d098c7b662b01d9502c8560219eafbf1941))
|
|
241
227
|
* add okf-indexer package for SKILL.md indexing (RFC-0106) ([a3cc7da](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a3cc7da0a390d5c8176d0d871418780facabcb5c))
|
|
242
228
|
* extend a2a-adapter with discovery server (RFC-0104) ([0c4fc18](https://github.com/ManotLuijiu/pi-harness-runtime/commit/0c4fc1826fd55f10305bfbd894b32b5b55f1ab86))
|
|
243
229
|
|
|
244
|
-
|
|
245
230
|
### Bug Fixes
|
|
246
231
|
|
|
247
232
|
* act on CONFLICTING immediately even when mergeStateStatus=DIRTY ([b7f290e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/b7f290ece8e0e989635aacc456fffd4ee19183ec))
|
|
@@ -263,14 +248,12 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
263
248
|
|
|
264
249
|
## [0.10.0](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.24...v0.10.0) (2026-07-30)
|
|
265
250
|
|
|
266
|
-
|
|
267
251
|
### Features
|
|
268
252
|
|
|
269
253
|
* blackboard-coordinated loop controller with early exit ([f3cf3ea](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f3cf3ea479170dd75b7e9a7c3515dde230bb0da7))
|
|
270
254
|
* herdr inter-agent event bus for autonomous code review ([1490cdd](https://github.com/ManotLuijiu/pi-harness-runtime/commit/1490cddec80dac812065c3c681984524ad3194e1))
|
|
271
255
|
* loop controller for write-review cycles with early exit ([95066ce](https://github.com/ManotLuijiu/pi-harness-runtime/commit/95066ce8cb58c55eaa14d69fd826b5aaff680c16))
|
|
272
256
|
|
|
273
|
-
|
|
274
257
|
### Bug Fixes
|
|
275
258
|
|
|
276
259
|
* **clipboard:** pass UTF-8 bytes directly to xclip via stdin, avoid pipeline corruption ([051ca18](https://github.com/ManotLuijiu/pi-harness-runtime/commit/051ca18fb1deb1169a8a4da16aa9314d2e6ec66d))
|
|
@@ -283,7 +266,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
283
266
|
|
|
284
267
|
### [0.9.25](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.21...v0.9.25) (2026-07-27)
|
|
285
268
|
|
|
286
|
-
|
|
287
269
|
### Bug Fixes
|
|
288
270
|
|
|
289
271
|
* **ci:** explicitly list test files to exclude cookie auth tests in release workflow ([1c96002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/1c9600232282a1ef9323300fbd763c962aff49f6))
|
|
@@ -294,7 +276,6 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
294
276
|
|
|
295
277
|
### [0.9.24](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.21...v0.9.24) (2026-07-27)
|
|
296
278
|
|
|
297
|
-
|
|
298
279
|
### Bug Fixes
|
|
299
280
|
|
|
300
281
|
* **ci:** skip browser auth tests in release workflow ([6117ed5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/6117ed5af11357c6f12d11f4cd8d3441de519043))
|
package/footer-status.ts
CHANGED
|
@@ -6,6 +6,51 @@ import {
|
|
|
6
6
|
type ParsedContextWindowUsage,
|
|
7
7
|
type ParsedQuotaUsageData,
|
|
8
8
|
} from "./status-parsers.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format countdown seconds into a human-readable string for TUI display.
|
|
12
|
+
* Examples: "4h 32m", "45m 12s", "12s"
|
|
13
|
+
*/
|
|
14
|
+
export function formatCountdownForDisplay(seconds: number): string {
|
|
15
|
+
if (seconds <= 0) return "RESET";
|
|
16
|
+
|
|
17
|
+
const days = Math.floor(seconds / 86400);
|
|
18
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
19
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
20
|
+
const secs = seconds % 60;
|
|
21
|
+
|
|
22
|
+
if (days > 0) {
|
|
23
|
+
return `${days}d ${hours}h`;
|
|
24
|
+
}
|
|
25
|
+
if (hours > 0) {
|
|
26
|
+
return `${hours}h ${minutes}m`;
|
|
27
|
+
}
|
|
28
|
+
if (minutes > 0) {
|
|
29
|
+
return `${minutes}m ${secs}s`;
|
|
30
|
+
}
|
|
31
|
+
return `${secs}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a TUI-signal exhaustion status line with optional countdown.
|
|
36
|
+
* This is used when a provider (like GLM) hits its limit.
|
|
37
|
+
*/
|
|
38
|
+
export function buildExhaustionStatusLine(
|
|
39
|
+
provider: string,
|
|
40
|
+
view: ProviderMirrorRecord,
|
|
41
|
+
countdownSeconds?: number,
|
|
42
|
+
): string {
|
|
43
|
+
const label = providerDisplayName(provider);
|
|
44
|
+
const limitType = view.limitType ?? "tokens";
|
|
45
|
+
const reset = view.resets_at ?? view.h5_resets_at ?? "soon";
|
|
46
|
+
|
|
47
|
+
if (countdownSeconds !== undefined && countdownSeconds > 0) {
|
|
48
|
+
const countdown = formatCountdownForDisplay(countdownSeconds);
|
|
49
|
+
return `${label}: ⏳ ${countdown} (${limitType} hit)`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${label}: limit hit (${limitType}), reset ${reset}`;
|
|
53
|
+
}
|
|
9
54
|
import { getProviderLabel } from "./packages/types/src/ai-providers.js";
|
|
10
55
|
import {
|
|
11
56
|
providerHasContinuousScrape,
|
|
@@ -153,14 +198,14 @@ export function buildFooterStatusValue(
|
|
|
153
198
|
const weeklyResets = view.weekly_resets_at ?? "soon";
|
|
154
199
|
|
|
155
200
|
let statusLine: string;
|
|
156
|
-
if (view.h5_used_pct
|
|
201
|
+
if (view.h5_used_pct === undefined) {
|
|
202
|
+
// OpenAI: weekly-only (no 5h window)
|
|
203
|
+
statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
|
|
204
|
+
} else {
|
|
157
205
|
// MiniMax: has both 5h and weekly windows
|
|
158
206
|
const h5Pct = view.h5_used_pct;
|
|
159
207
|
const h5Left = Math.max(0, 100 - h5Pct);
|
|
160
208
|
statusLine = `5h: ${h5Left.toFixed(0)}% left · week: ${weeklyLeft.toFixed(0)}% left`;
|
|
161
|
-
} else {
|
|
162
|
-
// OpenAI: weekly-only (no 5h window)
|
|
163
|
-
statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
const freshnessSuffix =
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Quota Countdown Tests
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test harness/glm-quota-countdown.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
parseGLMResetTime,
|
|
10
|
+
formatCountdown,
|
|
11
|
+
GLMQuotaCountdown,
|
|
12
|
+
} from "./glm-quota-countdown.js";
|
|
13
|
+
|
|
14
|
+
describe("parseGLMResetTime", () => {
|
|
15
|
+
it("parses full datetime format 'reset at 2026-08-25 01:47:16'", () => {
|
|
16
|
+
const result = parseGLMResetTime(
|
|
17
|
+
'Error: 429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-08-25 01:47:16"}',
|
|
18
|
+
);
|
|
19
|
+
expect(result).not.toBeNull();
|
|
20
|
+
const date = new Date(result!);
|
|
21
|
+
expect(date.getUTCFullYear()).toBe(2026);
|
|
22
|
+
expect(date.getUTCMonth()).toBe(7); // August is 7 (0-indexed)
|
|
23
|
+
expect(date.getUTCDate()).toBe(25);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("parses full datetime format with T separator", () => {
|
|
27
|
+
const result = parseGLMResetTime(
|
|
28
|
+
"Your limit will reset at 2026-08-25T01:47:16",
|
|
29
|
+
);
|
|
30
|
+
expect(result).not.toBeNull();
|
|
31
|
+
const date = new Date(result!);
|
|
32
|
+
expect(date.getUTCFullYear()).toBe(2026);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("parses time-only format when date is implied", () => {
|
|
36
|
+
const result = parseGLMResetTime("will reset at 14:30:00");
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
const date = new Date(result!);
|
|
39
|
+
expect(date.getUTCHours()).toBe(14);
|
|
40
|
+
expect(date.getUTCMinutes()).toBe(30);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns null for non-GLM messages", () => {
|
|
44
|
+
expect(parseGLMResetTime("Hello world")).toBeNull();
|
|
45
|
+
expect(parseGLMResetTime("OpenAI rate limit exceeded")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles GLM error with code 1308", () => {
|
|
49
|
+
const result = parseGLMResetTime(
|
|
50
|
+
'{"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-08-25 01:47:16"}',
|
|
51
|
+
);
|
|
52
|
+
expect(result).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("parses alternative format 'reset at 2026-08-25 01:47:16'", () => {
|
|
56
|
+
const result = parseGLMResetTime(
|
|
57
|
+
"reset at 2026-08-25 01:47:16 - please retry later",
|
|
58
|
+
);
|
|
59
|
+
expect(result).not.toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("formatCountdown", () => {
|
|
64
|
+
it("formats days, hours, minutes, seconds", () => {
|
|
65
|
+
expect(formatCountdown(90061)).toBe("1d 1h 1m 1s"); // 1 day, 1 hour, 1 min, 1 sec
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("formats hours and minutes when no days", () => {
|
|
69
|
+
expect(formatCountdown(3661)).toBe("1h 1m 1s");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("formats minutes and seconds when no hours", () => {
|
|
73
|
+
expect(formatCountdown(125)).toBe("2m 5s");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("formats seconds only when less than a minute", () => {
|
|
77
|
+
expect(formatCountdown(45)).toBe("45s");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns 'RESET NOW' for zero or negative", () => {
|
|
81
|
+
expect(formatCountdown(0)).toBe("RESET NOW");
|
|
82
|
+
expect(formatCountdown(-100)).toBe("RESET NOW");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles exactly 24 hours", () => {
|
|
86
|
+
expect(formatCountdown(86400)).toBe("1d 0h 0m 0s");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("handles exactly 1 hour", () => {
|
|
90
|
+
expect(formatCountdown(3600)).toBe("1h 0m 0s");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("handles exactly 1 minute", () => {
|
|
94
|
+
expect(formatCountdown(60)).toBe("1m 0s");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("GLMQuotaCountdown", () => {
|
|
99
|
+
let countdown: GLMQuotaCountdown;
|
|
100
|
+
let mockCheckpoint: any;
|
|
101
|
+
let mockTransition: any;
|
|
102
|
+
let mockReadProvider: any;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
countdown = new GLMQuotaCountdown();
|
|
106
|
+
|
|
107
|
+
// Track calls for verification
|
|
108
|
+
mockCheckpoint = { jobId: "test-job", status: "paused_quota" };
|
|
109
|
+
mockTransition = { success: true };
|
|
110
|
+
mockReadProvider = null;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
countdown.dispose();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("hasCountdown / getCountdown", () => {
|
|
118
|
+
it("returns false initially for unknown job", () => {
|
|
119
|
+
expect(countdown.hasCountdown("unknown")).toBe(false);
|
|
120
|
+
expect(countdown.getCountdown("unknown")).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("startCountdown", () => {
|
|
125
|
+
it("starts countdown and stores state", async () => {
|
|
126
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
|
|
127
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
128
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
129
|
+
|
|
130
|
+
const state = await countdown.startCountdown(
|
|
131
|
+
"job-1",
|
|
132
|
+
futureDate,
|
|
133
|
+
mockMirror,
|
|
134
|
+
mockMachine,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(state.jobId).toBe("job-1");
|
|
138
|
+
expect(state.resetAt).toBe(futureDate);
|
|
139
|
+
expect(state.totalSeconds).toBeGreaterThan(3500); // ~1 hour
|
|
140
|
+
expect(countdown.hasCountdown("job-1")).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("cancels existing countdown before starting new one", async () => {
|
|
144
|
+
const futureDate1 = new Date(Date.now() + 3600000).toISOString();
|
|
145
|
+
const futureDate2 = new Date(Date.now() + 7200000).toISOString();
|
|
146
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
147
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
148
|
+
|
|
149
|
+
await countdown.startCountdown(
|
|
150
|
+
"job-1",
|
|
151
|
+
futureDate1,
|
|
152
|
+
mockMirror,
|
|
153
|
+
mockMachine,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const state = await countdown.startCountdown(
|
|
157
|
+
"job-1",
|
|
158
|
+
futureDate2,
|
|
159
|
+
mockMirror,
|
|
160
|
+
mockMachine,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Should have only one countdown
|
|
164
|
+
expect(countdown.getAllCountdowns().length).toBe(1);
|
|
165
|
+
expect(state.resetAt).toBe(futureDate2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("updates mirror store with reset time", async () => {
|
|
169
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString();
|
|
170
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
171
|
+
let capturedRecord: any = null;
|
|
172
|
+
const mockMirror = createMockMirror(
|
|
173
|
+
() => mockReadProvider,
|
|
174
|
+
(_p, r) => {
|
|
175
|
+
capturedRecord = r;
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
|
|
180
|
+
|
|
181
|
+
expect(capturedRecord).not.toBeNull();
|
|
182
|
+
expect(capturedRecord.provider).toBe("glm");
|
|
183
|
+
expect(capturedRecord.exhausted).toBe(true);
|
|
184
|
+
expect(capturedRecord.h5_resets_at).toBe(futureDate);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("startFromError", () => {
|
|
189
|
+
it("parses reset time from error message", async () => {
|
|
190
|
+
const errorMsg =
|
|
191
|
+
'Error: 429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2099-12-31 23:59:59"}';
|
|
192
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
193
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
194
|
+
|
|
195
|
+
const state = await countdown.startFromError(
|
|
196
|
+
"job-1",
|
|
197
|
+
errorMsg,
|
|
198
|
+
mockMirror,
|
|
199
|
+
mockMachine,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(state).not.toBeNull();
|
|
203
|
+
expect(state!.jobId).toBe("job-1");
|
|
204
|
+
expect(countdown.hasCountdown("job-1")).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns null when reset time cannot be parsed", async () => {
|
|
208
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
209
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
210
|
+
|
|
211
|
+
const state = await countdown.startFromError(
|
|
212
|
+
"job-1",
|
|
213
|
+
"Some unrelated error message",
|
|
214
|
+
mockMirror,
|
|
215
|
+
mockMachine,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(state).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("cancelCountdown", () => {
|
|
223
|
+
it("cancels active countdown", async () => {
|
|
224
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString();
|
|
225
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
226
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
227
|
+
|
|
228
|
+
await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
|
|
229
|
+
|
|
230
|
+
expect(countdown.hasCountdown("job-1")).toBe(true);
|
|
231
|
+
|
|
232
|
+
countdown.cancelCountdown("job-1");
|
|
233
|
+
|
|
234
|
+
expect(countdown.hasCountdown("job-1")).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("handles cancelling non-existent countdown", () => {
|
|
238
|
+
// Should not throw
|
|
239
|
+
expect(() => countdown.cancelCountdown("non-existent")).not.toThrow();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("onTick callback", () => {
|
|
244
|
+
it("receives tick events", async () => {
|
|
245
|
+
const ticks: any[] = [];
|
|
246
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString();
|
|
247
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
248
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
249
|
+
|
|
250
|
+
const unsubscribe = countdown.onTick((event) => {
|
|
251
|
+
ticks.push(event);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
|
|
255
|
+
|
|
256
|
+
// Wait a bit for initial tick
|
|
257
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
258
|
+
|
|
259
|
+
expect(ticks.length).toBeGreaterThan(0);
|
|
260
|
+
expect(ticks[0].jobId).toBe("job-1");
|
|
261
|
+
expect(ticks[0].remainingFormatted).toBeDefined();
|
|
262
|
+
|
|
263
|
+
unsubscribe();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("getAllCountdowns", () => {
|
|
268
|
+
it("returns all active countdowns", async () => {
|
|
269
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString();
|
|
270
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
271
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
272
|
+
|
|
273
|
+
await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
|
|
274
|
+
|
|
275
|
+
await countdown.startCountdown("job-2", futureDate, mockMirror, mockMachine);
|
|
276
|
+
|
|
277
|
+
const all = countdown.getAllCountdowns();
|
|
278
|
+
expect(all.length).toBe(2);
|
|
279
|
+
expect(all.map((c) => c.jobId)).toContain("job-1");
|
|
280
|
+
expect(all.map((c) => c.jobId)).toContain("job-2");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("dispose", () => {
|
|
285
|
+
it("clears all timers and countdowns", async () => {
|
|
286
|
+
const futureDate = new Date(Date.now() + 3600000).toISOString();
|
|
287
|
+
const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
|
|
288
|
+
const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
|
|
289
|
+
|
|
290
|
+
await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
|
|
291
|
+
|
|
292
|
+
await countdown.startCountdown("job-2", futureDate, mockMirror, mockMachine);
|
|
293
|
+
|
|
294
|
+
expect(countdown.getAllCountdowns().length).toBe(2);
|
|
295
|
+
|
|
296
|
+
countdown.dispose();
|
|
297
|
+
|
|
298
|
+
expect(countdown.getAllCountdowns().length).toBe(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// No-op write function for tests that don't need to verify write calls
|
|
304
|
+
function noopWrite(_provider: string, _record: any): void {}
|
|
305
|
+
|
|
306
|
+
// Helper: Create mock JobStateMachine
|
|
307
|
+
function createMockMachine(getCheckpointResult: any, transitionResult: any) {
|
|
308
|
+
return {
|
|
309
|
+
getCheckpoint: () => getCheckpointResult,
|
|
310
|
+
transition: async () => transitionResult,
|
|
311
|
+
} as any;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Helper: Create mock MirrorStore
|
|
315
|
+
function createMockMirror(
|
|
316
|
+
readProviderFn: () => any,
|
|
317
|
+
writeProviderFn: (provider: string, record: any) => void,
|
|
318
|
+
) {
|
|
319
|
+
return {
|
|
320
|
+
readProvider: readProviderFn,
|
|
321
|
+
writeProvider: writeProviderFn,
|
|
322
|
+
} as any;
|
|
323
|
+
}
|