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.
- package/CHANGELOG.md +19 -26
- package/footer-status.ts +107 -7
- package/harness/e2e/glm-quota-scraper.ts +44 -0
- 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/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/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/package.json +1 -1
- package/packages/quota-manager/src/tui-usage-monitor.ts +21 -4
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- 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
|
+
}
|