pi-harness-runtime 1.1.13 → 1.1.15

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +19 -26
  2. package/footer-status.ts +107 -7
  3. package/harness/e2e/glm-quota-scraper.ts +44 -0
  4. package/harness/glm-quota-countdown.test.ts +323 -0
  5. package/harness/glm-quota-countdown.ts +615 -0
  6. package/harness/index.ts +12 -0
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/architecture-generator/package.json +1 -1
  10. package/packages/auth/package.json +1 -1
  11. package/packages/autonomous-refactor/package.json +1 -1
  12. package/packages/autonomous-runtime/package.json +1 -1
  13. package/packages/cache-strategy/package.json +1 -1
  14. package/packages/capability-registry/package.json +1 -1
  15. package/packages/checkpoint/package.json +1 -1
  16. package/packages/cli-plugin-sdk/package.json +1 -1
  17. package/packages/clipboard/package.json +1 -1
  18. package/packages/clipboard-plugin/package.json +1 -1
  19. package/packages/code-generation/package.json +1 -1
  20. package/packages/code-review/package.json +1 -1
  21. package/packages/codex-adapter/package.json +1 -1
  22. package/packages/config-capture/package.json +1 -1
  23. package/packages/context-compiler/package.json +1 -1
  24. package/packages/context-discovery/package.json +1 -1
  25. package/packages/context-manager/package.json +1 -1
  26. package/packages/cookie-sanitizer/package.json +1 -1
  27. package/packages/cost-optimizer/package.json +1 -1
  28. package/packages/dependency-analyzer/package.json +1 -1
  29. package/packages/django-plugin/package.json +1 -1
  30. package/packages/doc-generator/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-store/package.json +1 -1
  35. package/packages/experience-replay/package.json +1 -1
  36. package/packages/feedback-collector/package.json +1 -1
  37. package/packages/file-copy-helper/package.json +1 -1
  38. package/packages/framework-detector/package.json +1 -1
  39. package/packages/framework-plugin-sdk/package.json +1 -1
  40. package/packages/frappe-plugin/package.json +1 -1
  41. package/packages/generic-web-plugin/package.json +1 -1
  42. package/packages/health-monitor/package.json +1 -1
  43. package/packages/intent-analyzer/package.json +1 -1
  44. package/packages/knowledge-graph/package.json +1 -1
  45. package/packages/knowledge-retrieval/package.json +1 -1
  46. package/packages/laravel-plugin/package.json +1 -1
  47. package/packages/learning-engine/package.json +1 -1
  48. package/packages/mcp-adapter/package.json +1 -1
  49. package/packages/memory-engine/package.json +1 -1
  50. package/packages/milestone-manager/package.json +1 -1
  51. package/packages/model-registry/package.json +1 -1
  52. package/packages/nextjs-plugin/package.json +1 -1
  53. package/packages/notification/package.json +1 -1
  54. package/packages/observability/package.json +1 -1
  55. package/packages/okf-indexer/package.json +1 -1
  56. package/packages/performance-optimizer/package.json +1 -1
  57. package/packages/privilege-broker/package.json +1 -1
  58. package/packages/project-analyzer/package.json +1 -1
  59. package/packages/project-bootstrap/package.json +1 -1
  60. package/packages/projection-engine/package.json +1 -1
  61. package/packages/prompt-compiler/package.json +1 -1
  62. package/packages/prompt-versioning/package.json +1 -1
  63. package/packages/provider-adapter-sdk/package.json +1 -1
  64. package/packages/provider-router/package.json +1 -1
  65. package/packages/provider-selector/package.json +1 -1
  66. package/packages/providers/package.json +1 -1
  67. package/packages/quota-manager/dist/tui-usage-monitor.d.ts.map +1 -1
  68. package/packages/quota-manager/dist/tui-usage-monitor.js +20 -4
  69. package/packages/quota-manager/dist/tui-usage-monitor.js.map +1 -1
  70. package/packages/quota-manager/package.json +1 -1
  71. package/packages/quota-manager/src/tui-usage-monitor.ts +21 -4
  72. package/packages/rate-limiter/package.json +1 -1
  73. package/packages/react-vite-plugin/package.json +1 -1
  74. package/packages/release-manager/package.json +1 -1
  75. package/packages/requirement-compiler/package.json +1 -1
  76. package/packages/runtime/package.json +1 -1
  77. package/packages/scheduler/package.json +1 -1
  78. package/packages/scheduler-adapter/package.json +1 -1
  79. package/packages/session/package.json +1 -1
  80. package/packages/session-api/package.json +1 -1
  81. package/packages/session-export/package.json +1 -1
  82. package/packages/shared-context/package.json +1 -1
  83. package/packages/skill-registry/package.json +1 -1
  84. package/packages/sprint-planner/package.json +1 -1
  85. package/packages/subscription-engine/package.json +1 -1
  86. package/packages/task-compiler/package.json +1 -1
  87. package/packages/tencentdb-memory/package.json +1 -1
  88. package/packages/tencentdb-sync/package.json +1 -1
  89. package/packages/test-data-generator/package.json +1 -1
  90. package/packages/test-generator/package.json +1 -1
  91. package/packages/todo-bd-sync/package.json +1 -1
  92. package/packages/token-estimation/package.json +1 -1
  93. package/packages/token-optimizer/package.json +1 -1
  94. package/packages/tui/package.json +1 -1
  95. package/packages/types/package.json +1 -1
  96. package/packages/workflow-events/package.json +1 -1
  97. package/packages/workspace-scanner/package.json +1 -1
  98. package/packages/worktree/package.json +1 -1
  99. package/packages/write-review/package.json +1 -1
@@ -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
+ }