pi-mega-compact 0.8.7 → 0.8.10

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 (110) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
  4. package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
  5. package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
  6. package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
  7. package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
  8. package/dist/extensions/dashboard-server/api-contracts.js +8 -0
  9. package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
  10. package/dist/extensions/dashboard-server/auth.js +18 -0
  11. package/dist/extensions/dashboard-server/helpers.js +37 -0
  12. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  13. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  14. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  15. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  16. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  17. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  18. package/dist/extensions/dashboard-server/html/script.js +259 -0
  19. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  20. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  21. package/dist/extensions/dashboard-server/html-template.js +41 -0
  22. package/dist/extensions/dashboard-server/html.js +46 -1
  23. package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
  24. package/dist/extensions/dashboard-server/server.js +269 -30
  25. package/dist/extensions/dashboard-server/tailscale.js +8 -0
  26. package/dist/extensions/dashboard-server/types.js +4 -0
  27. package/dist/extensions/mega-dashboard.js +9 -0
  28. package/dist/extensions/mega-events/perf-handler.js +71 -0
  29. package/dist/extensions/mega-events/register.js +2 -0
  30. package/dist/extensions/mega-events.js +1 -0
  31. package/dist/extensions/mega-runtime/state.js +59 -1
  32. package/dist/src/store/sqlite/connection.js +35 -0
  33. package/dist/src/store/sqlite/index-store.js +167 -0
  34. package/dist/src/store/sqlite/memory.js +54 -0
  35. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  36. package/dist/src/store/sqlite/perf-samples.js +81 -0
  37. package/dist/src/store/sqlite/perf-samples.test.js +54 -0
  38. package/dist/src/store/sqlite/schema.js +14 -0
  39. package/dist/src/store/sqlite/sessions.js +39 -0
  40. package/dist/src/store/sqlite/transaction.js +19 -0
  41. package/dist/src/store/sqlite.js +1 -0
  42. package/dist/src/vectorStore/add.js +260 -0
  43. package/dist/src/vectorStore/dedup.js +52 -0
  44. package/dist/src/vectorStore/index.js +10 -0
  45. package/dist/src/vectorStore/queries.js +83 -0
  46. package/dist/src/vectorStore/search.js +95 -0
  47. package/dist/src/vectorStore/session.js +19 -0
  48. package/dist/src/vectorStore/store.js +105 -0
  49. package/dist/src/vectorStore/types.js +6 -0
  50. package/dist/src/vectorStore/utils.js +23 -0
  51. package/extensions/dashboard-client/package-lock.json +1771 -0
  52. package/extensions/dashboard-client/package.json +27 -0
  53. package/extensions/dashboard-client/src/App.tsx +82 -0
  54. package/extensions/dashboard-client/src/api/client.ts +145 -0
  55. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
  56. package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
  57. package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
  58. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
  59. package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
  60. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
  61. package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
  62. package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
  63. package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
  64. package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
  65. package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
  66. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
  67. package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
  68. package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
  69. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
  70. package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
  71. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
  72. package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
  73. package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
  74. package/extensions/dashboard-client/src/index.html +12 -0
  75. package/extensions/dashboard-client/src/main.tsx +23 -0
  76. package/extensions/dashboard-client/src/styles/base.css +121 -0
  77. package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
  78. package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
  79. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
  80. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
  81. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
  82. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
  83. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
  84. package/extensions/dashboard-client/tsconfig.json +28 -0
  85. package/extensions/dashboard-client/vite.config.ts +38 -0
  86. package/extensions/dashboard-server/api-contracts/core.ts +302 -0
  87. package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
  88. package/extensions/dashboard-server/api-contracts/game.ts +145 -0
  89. package/extensions/dashboard-server/api-contracts/index.ts +159 -0
  90. package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
  91. package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
  92. package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
  94. package/extensions/dashboard-server/api-contracts.ts +96 -0
  95. package/extensions/dashboard-server/auth.ts +20 -0
  96. package/extensions/dashboard-server/html.ts +46 -1
  97. package/extensions/dashboard-server/perf-server.test.ts +101 -0
  98. package/extensions/dashboard-server/server.ts +862 -503
  99. package/extensions/dashboard-server/tailscale.ts +7 -0
  100. package/extensions/dashboard-server/types.ts +23 -114
  101. package/extensions/mega-dashboard.ts +17 -0
  102. package/extensions/mega-events/perf-handler.ts +113 -0
  103. package/extensions/mega-events/register.ts +2 -0
  104. package/extensions/mega-events.ts +1 -0
  105. package/extensions/mega-runtime/state.ts +57 -0
  106. package/package.json +2 -1
  107. package/src/store/sqlite/perf-samples.test.ts +65 -0
  108. package/src/store/sqlite/perf-samples.ts +125 -0
  109. package/src/store/sqlite/schema.ts +14 -0
  110. package/src/store/sqlite.ts +1 -0
@@ -0,0 +1,869 @@
1
+ /**
2
+ * api-contracts.test.ts — Validates the dashboard API contracts.
3
+ *
4
+ * Two validation layers:
5
+ * 1. COMPILE-TIME: `satisfies` checks for each endpoint in the ENDPOINTS registry,
6
+ * ensuring response types are structurally compatible with their interfaces.
7
+ * Also verifies that every `/api/*` endpoint in `server.ts` has a corresponding
8
+ * entry in ENDPOINTS.
9
+ * 2. RUNTIME: For each endpoint, a hand-crafted minimal JSON payload is parsed
10
+ * and validated for field presence and correct primitive types.
11
+ *
12
+ * SSE: The `SseEvent` union is validated by checking each variant has a `type`
13
+ * field matching its discriminator.
14
+ *
15
+ * PREVENT-PI-004: zero network calls — all payloads are inline JSON strings.
16
+ * PREVENT-011: no `any` type — `unknown` + runtime guards used throughout.
17
+ */
18
+ import { test, describe } from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import { ENDPOINTS } from "./api-contracts/index.js";
21
+ // ─── Helpers ────────────────────────────────────────────────────────────────
22
+ /** Primitive type name from a JSON-parsed value (matches typeof for JSON types). */
23
+ function primitiveType(v) {
24
+ if (v === null)
25
+ return "null";
26
+ if (Array.isArray(v))
27
+ return "array";
28
+ return typeof v;
29
+ }
30
+ /** Assert that a field exists on an object and has one of the expected primitive types. */
31
+ function assertField(obj, field, expected) {
32
+ assert.ok(field in obj, `field "${field}" must exist`);
33
+ const actual = primitiveType(obj[field]);
34
+ assert.ok(expected.includes(actual), `field "${field}" expected type ${expected.join("|")}, got ${actual}`);
35
+ }
36
+ /** Assert that a nested object field exists and is an object (or null if allowed). */
37
+ function assertObject(obj, field, allowNull = false) {
38
+ assert.ok(field in obj, `field "${field}" must exist`);
39
+ const val = obj[field];
40
+ if (allowNull && val === null)
41
+ return null;
42
+ assert.equal(primitiveType(val), "object", `field "${field}" must be an object${allowNull ? " or null" : ""}`);
43
+ return val;
44
+ }
45
+ // ─── Compile-Time: ENDPOINTS registry satisfies checks ──────────────────────
46
+ // These are type-level checks — if the response type of an endpoint doesn't
47
+ // match the expected interface, tsc will fail at compile time.
48
+ const _c_snapshot = ENDPOINTS.snapshot;
49
+ const _c_version = ENDPOINTS.version;
50
+ const _c_index = ENDPOINTS.index;
51
+ const _c_repos = ENDPOINTS.repos;
52
+ const _c_summary = ENDPOINTS.summary;
53
+ const _c_drift = ENDPOINTS.drift;
54
+ const _c_servers = ENDPOINTS.servers;
55
+ const _c_events = ENDPOINTS.events;
56
+ const _c_getGameState = ENDPOINTS.getGameState;
57
+ const _c_putGameState = ENDPOINTS.putGameState;
58
+ const _c_gameScores = ENDPOINTS.gameScores;
59
+ const _c_perf = ENDPOINTS.perf;
60
+ const _c_achievements = ENDPOINTS.achievements;
61
+ // Silence unused-variable warnings — these are compile-time-only checks.
62
+ void _c_snapshot;
63
+ void _c_version;
64
+ void _c_index;
65
+ void _c_repos;
66
+ void _c_summary;
67
+ void _c_drift;
68
+ void _c_servers;
69
+ void _c_events;
70
+ void _c_getGameState;
71
+ void _c_putGameState;
72
+ void _c_gameScores;
73
+ void _c_perf;
74
+ void _c_achievements;
75
+ // ─── Compile-Time: ENDPOINTS path/method consistency ─────────────────────────
76
+ // Verify the ENDPOINTS registry has exactly 13 entries with correct paths.
77
+ const ENDPOINT_KEYS = Object.keys(ENDPOINTS);
78
+ const EXPECTED_ENDPOINT_COUNT = 13;
79
+ /** All `/api/*` paths served by server.ts (extracted from the route handlers). */
80
+ const SERVER_TS_PATHS = [
81
+ "/api/snapshot",
82
+ "/api/version",
83
+ "/api/index",
84
+ "/api/repos",
85
+ "/api/summary",
86
+ "/api/drift",
87
+ "/api/servers",
88
+ "/api/events",
89
+ "/api/game-state",
90
+ "/api/game-scores",
91
+ "/api/perf",
92
+ "/api/achievements",
93
+ ];
94
+ // ─── Tests ───────────────────────────────────────────────────────────────────
95
+ describe("ENDPOINTS registry", () => {
96
+ test("has exactly 13 endpoint entries", () => {
97
+ assert.equal(ENDPOINT_KEYS.length, EXPECTED_ENDPOINT_COUNT, `ENDPOINTS must have ${EXPECTED_ENDPOINT_COUNT} entries, got ${ENDPOINT_KEYS.length}`);
98
+ });
99
+ test("every server.ts /api/* path has a corresponding ENDPOINTS entry", () => {
100
+ const registryPaths = new Set(ENDPOINT_KEYS.map((k) => ENDPOINTS[k].path));
101
+ for (const path of SERVER_TS_PATHS) {
102
+ assert.ok(registryPaths.has(path), `server.ts path "${path}" has no ENDPOINTS entry`);
103
+ }
104
+ });
105
+ test("every ENDPOINTS path starts with /api/", () => {
106
+ for (const key of ENDPOINT_KEYS) {
107
+ assert.ok(ENDPOINTS[key].path.startsWith("/api/"), `ENDPOINTS.${key}.path must start with /api/ — got "${ENDPOINTS[key].path}"`);
108
+ }
109
+ });
110
+ test("every ENDPOINTS entry has method and description", () => {
111
+ for (const key of ENDPOINT_KEYS) {
112
+ const ep = ENDPOINTS[key];
113
+ assert.ok(ep.method, `ENDPOINTS.${key} must have method`);
114
+ assert.ok(ep.description, `ENDPOINTS.${key} must have description`);
115
+ }
116
+ });
117
+ });
118
+ // ─── Per-endpoint runtime validation ─────────────────────────────────────────
119
+ describe("GET /api/snapshot", () => {
120
+ test("sample payload validates field presence and types", () => {
121
+ const raw = JSON.stringify({
122
+ updatedAt: "2025-01-01T00:00:00Z",
123
+ tier: "ultra-compact",
124
+ presetTier: "super-compact",
125
+ pressure: 45.2,
126
+ config: {
127
+ fastGatePct: 60,
128
+ thresholdTokens: 100000,
129
+ anchorUserMessages: 3,
130
+ preserveRecent: 10,
131
+ auto: true,
132
+ autoInlineK: 5,
133
+ },
134
+ session: {
135
+ id: "sess-123",
136
+ state: "idle",
137
+ persistedThisSession: true,
138
+ lastCheckpointId: "ckpt-456",
139
+ lastCompactedFrom: 50000,
140
+ },
141
+ context: {
142
+ tokens: 42000,
143
+ percent: 42,
144
+ contextWindow: 200000,
145
+ },
146
+ trigger: {
147
+ armed: true,
148
+ ready: false,
149
+ currentTokens: 42000,
150
+ thresholdTokens: 100000,
151
+ fastGatePct: 60,
152
+ },
153
+ store: {
154
+ checkpointCount: 15,
155
+ totalTokenEstimate: 120000,
156
+ originalTokens: 200000,
157
+ tokensSaved: 80000,
158
+ injectedCount: 30,
159
+ dedupHitRate: 12.5,
160
+ storageDedupRate: 8.3,
161
+ dedupCollapsed: 5,
162
+ },
163
+ crew: { activeAgents: 2, currentTurn: 1 },
164
+ repo: {
165
+ checkpointCount: 15,
166
+ totalTokenEstimate: 120000,
167
+ originalTokens: 200000,
168
+ tokensSaved: 80000,
169
+ sessionCount: 5,
170
+ dedupAttempts: 10,
171
+ dedupCollapsed: 5,
172
+ storageDedupRate: 8.3,
173
+ },
174
+ integrity: {
175
+ regionsRetained: 8,
176
+ compressedOriginalBytes: 4096,
177
+ duplicatesCollapsed: 5,
178
+ bytesPermanentlyDeleted: 1024,
179
+ },
180
+ cacheHits: {
181
+ session: 3,
182
+ total: 20,
183
+ sessionTokensSaved: 1500,
184
+ totalTokensSaved: 10000,
185
+ },
186
+ compacts: { session: 3, total: 20 },
187
+ timeSaved: {
188
+ compact: { sessionSec: 120, totalSec: 600 },
189
+ cacheHit: { sessionSec: 30, totalSec: 200 },
190
+ },
191
+ compression: {
192
+ session: {
193
+ tokensIn: 50000,
194
+ tokensOut: 20000,
195
+ tokensFreed: 30000,
196
+ compressionPct: 60,
197
+ dedupPct: 5,
198
+ },
199
+ repo: {
200
+ tokensIn: 200000,
201
+ tokensOut: 80000,
202
+ tokensFreed: 120000,
203
+ compressionPct: 60,
204
+ dedupPct: 8,
205
+ },
206
+ },
207
+ });
208
+ const obj = JSON.parse(raw);
209
+ assertField(obj, "updatedAt", ["string", "null"]);
210
+ assertField(obj, "tier", ["string"]);
211
+ assertField(obj, "presetTier", ["string"]);
212
+ assertField(obj, "pressure", ["number"]);
213
+ assertField(obj, "config", ["object"]);
214
+ assertField(obj, "session", ["object"]);
215
+ assertField(obj, "context", ["object"]);
216
+ assertField(obj, "trigger", ["object"]);
217
+ assertField(obj, "store", ["object"]);
218
+ assertField(obj, "crew", ["object"]);
219
+ assertField(obj, "repo", ["object"]);
220
+ assertField(obj, "integrity", ["object"]);
221
+ assertField(obj, "cacheHits", ["object"]);
222
+ assertField(obj, "compacts", ["object"]);
223
+ assertField(obj, "timeSaved", ["object"]);
224
+ assertField(obj, "compression", ["object"]);
225
+ // Spot-check nested fields
226
+ const config = assertObject(obj, "config");
227
+ assertField(config, "fastGatePct", ["number"]);
228
+ assertField(config, "auto", ["boolean"]);
229
+ const session = assertObject(obj, "session");
230
+ assertField(session, "id", ["string", "null"]);
231
+ assertField(session, "persistedThisSession", ["boolean"]);
232
+ const context = assertObject(obj, "context");
233
+ assertField(context, "tokens", ["number", "null"]);
234
+ assertField(context, "contextWindow", ["number"]);
235
+ });
236
+ });
237
+ describe("GET /api/version", () => {
238
+ test("sample payload validates field presence and types", () => {
239
+ const raw = JSON.stringify({ version: "1.2.3" });
240
+ const obj = JSON.parse(raw);
241
+ assertField(obj, "version", ["string"]);
242
+ });
243
+ });
244
+ describe("GET /api/index", () => {
245
+ test("summary payload validates field presence and types", () => {
246
+ const raw = JSON.stringify({
247
+ updatedAt: "2025-01-01T00:00:00Z",
248
+ totalRepos: 3,
249
+ totalCheckpoints: 45,
250
+ totalTokensSaved: 150000,
251
+ totalCompressedOriginalBytes: 40960,
252
+ repos: [
253
+ {
254
+ repoRoot: "/home/user/repo1",
255
+ displayName: "repo1",
256
+ stateDir: "/home/user/.pi/mega-compact/repo1",
257
+ checkpointCount: 15,
258
+ tokensSaved: 50000,
259
+ compressedOriginalBytes: 16384,
260
+ lastCompactedAt: 1700000000000,
261
+ provider: "anthropic",
262
+ providerName: "Anthropic",
263
+ modelName: "claude-3",
264
+ inputRate: 100,
265
+ outputRate: 80,
266
+ lastSeen: 1700000000000,
267
+ tokensKept: 20000,
268
+ tokensDropped: 30000,
269
+ sessions: 5,
270
+ contextWindow: 200000,
271
+ maxTokens: 180000,
272
+ reasoning: false,
273
+ },
274
+ ],
275
+ });
276
+ const obj = JSON.parse(raw);
277
+ assertField(obj, "updatedAt", ["string"]);
278
+ assertField(obj, "totalRepos", ["number"]);
279
+ assertField(obj, "totalCheckpoints", ["number"]);
280
+ assertField(obj, "totalTokensSaved", ["number"]);
281
+ assertField(obj, "totalCompressedOriginalBytes", ["number"]);
282
+ assertField(obj, "repos", ["array"]);
283
+ const repos = obj["repos"];
284
+ assert.ok(repos.length > 0, "repos array should have at least one entry");
285
+ const first = repos[0];
286
+ assertField(first, "repoRoot", ["string"]);
287
+ assertField(first, "displayName", ["string"]);
288
+ assertField(first, "checkpointCount", ["number"]);
289
+ assertField(first, "lastCompactedAt", ["number", "null"]);
290
+ assertField(first, "provider", ["string", "null"]);
291
+ assertField(first, "lastSeen", ["number"]);
292
+ });
293
+ test("fallback payload validates field presence and types", () => {
294
+ const raw = JSON.stringify({
295
+ updatedAt: null,
296
+ summary: null,
297
+ repos: [],
298
+ });
299
+ const obj = JSON.parse(raw);
300
+ assertField(obj, "updatedAt", ["null"]);
301
+ assertField(obj, "summary", ["null"]);
302
+ assertField(obj, "repos", ["array"]);
303
+ assert.equal(obj["repos"].length, 0);
304
+ });
305
+ });
306
+ describe("GET /api/repos", () => {
307
+ test("sample payload validates field presence and types", () => {
308
+ const raw = JSON.stringify({
309
+ updatedAt: "2025-01-01T00:00:00Z",
310
+ repos: [
311
+ {
312
+ repoRoot: "/home/user/repo1",
313
+ displayName: "repo1",
314
+ stateDir: "/home/user/.pi/repo1",
315
+ checkpointCount: 15,
316
+ tokensSaved: 50000,
317
+ compressedOriginalBytes: 16384,
318
+ lastCompactedAt: null,
319
+ provider: null,
320
+ providerName: null,
321
+ modelName: null,
322
+ inputRate: null,
323
+ outputRate: null,
324
+ lastSeen: 1700000000000,
325
+ tokensKept: 20000,
326
+ tokensDropped: 30000,
327
+ sessions: 5,
328
+ contextWindow: null,
329
+ maxTokens: null,
330
+ reasoning: null,
331
+ },
332
+ ],
333
+ count: 1,
334
+ });
335
+ const obj = JSON.parse(raw);
336
+ assertField(obj, "updatedAt", ["string", "null"]);
337
+ assertField(obj, "repos", ["array"]);
338
+ assertField(obj, "count", ["number"]);
339
+ });
340
+ });
341
+ describe("GET /api/summary", () => {
342
+ test("sample payload validates field presence and types", () => {
343
+ const raw = JSON.stringify({
344
+ updatedAt: "2025-01-01T00:00:00Z",
345
+ summary: {
346
+ totalRepos: 3,
347
+ totalCheckpoints: 45,
348
+ totalTokensSaved: 150000,
349
+ totalCompressedOriginalBytes: 40960,
350
+ },
351
+ activeRepos: 2,
352
+ totalRepos: 3,
353
+ });
354
+ const obj = JSON.parse(raw);
355
+ assertField(obj, "updatedAt", ["string", "null"]);
356
+ assertField(obj, "summary", ["object", "null"]);
357
+ assertField(obj, "activeRepos", ["number"]);
358
+ assertField(obj, "totalRepos", ["number"]);
359
+ const summary = assertObject(obj, "summary");
360
+ assertField(summary, "totalRepos", ["number"]);
361
+ assertField(summary, "totalCheckpoints", ["number"]);
362
+ assertField(summary, "totalTokensSaved", ["number"]);
363
+ assertField(summary, "totalCompressedOriginalBytes", ["number"]);
364
+ });
365
+ test("null summary validates", () => {
366
+ const raw = JSON.stringify({
367
+ updatedAt: null,
368
+ summary: null,
369
+ activeRepos: 0,
370
+ totalRepos: 0,
371
+ });
372
+ const obj = JSON.parse(raw);
373
+ assertField(obj, "updatedAt", ["null"]);
374
+ assertField(obj, "summary", ["null"]);
375
+ assertField(obj, "activeRepos", ["number"]);
376
+ assertField(obj, "totalRepos", ["number"]);
377
+ });
378
+ });
379
+ describe("GET /api/drift", () => {
380
+ test("sample payload validates field presence and types", () => {
381
+ const raw = JSON.stringify({
382
+ generatedAt: 1700000000,
383
+ totals: {
384
+ ok: 2,
385
+ warn: 1,
386
+ stale: 1,
387
+ compactionLag: 0,
388
+ modelChurn: 0,
389
+ },
390
+ repos: [
391
+ {
392
+ repoRoot: "/home/user/repo1",
393
+ displayName: "repo1",
394
+ lastSeen: 1700000000,
395
+ lastCompactedAt: 1699990000,
396
+ modelCapturedAt: null,
397
+ signals: [
398
+ {
399
+ kind: "stale",
400
+ severity: "warn",
401
+ detail: "No activity for 48h",
402
+ },
403
+ ],
404
+ status: "warn",
405
+ },
406
+ ],
407
+ });
408
+ const obj = JSON.parse(raw);
409
+ assertField(obj, "generatedAt", ["number"]);
410
+ assertField(obj, "totals", ["object"]);
411
+ assertField(obj, "repos", ["array"]);
412
+ const totals = assertObject(obj, "totals");
413
+ assertField(totals, "ok", ["number"]);
414
+ assertField(totals, "warn", ["number"]);
415
+ assertField(totals, "stale", ["number"]);
416
+ assertField(totals, "compactionLag", ["number"]);
417
+ assertField(totals, "modelChurn", ["number"]);
418
+ const repos = obj["repos"];
419
+ const first = repos[0];
420
+ assertField(first, "repoRoot", ["string"]);
421
+ assertField(first, "displayName", ["string"]);
422
+ assertField(first, "lastSeen", ["number"]);
423
+ assertField(first, "lastCompactedAt", ["number", "null"]);
424
+ assertField(first, "modelCapturedAt", ["number", "null"]);
425
+ assertField(first, "signals", ["array"]);
426
+ assertField(first, "status", ["string"]);
427
+ const signals = first["signals"];
428
+ const sig = signals[0];
429
+ assertField(sig, "kind", ["string"]);
430
+ assertField(sig, "severity", ["string"]);
431
+ assertField(sig, "detail", ["string"]);
432
+ });
433
+ });
434
+ describe("GET /api/servers", () => {
435
+ test("sample payload validates field presence and types", () => {
436
+ const raw = JSON.stringify({
437
+ updatedAt: "2025-01-01T00:00:00Z",
438
+ servers: [
439
+ {
440
+ repoRoot: "/home/user/repo1",
441
+ displayName: "repo1",
442
+ model: "claude-3",
443
+ provider: "anthropic",
444
+ lastSeen: 1700000000,
445
+ lastCompactedAt: 1699990000,
446
+ tier: "ultra-compact",
447
+ contextPct: 42,
448
+ state: "idle",
449
+ },
450
+ ],
451
+ });
452
+ const obj = JSON.parse(raw);
453
+ assertField(obj, "updatedAt", ["string"]);
454
+ assertField(obj, "servers", ["array"]);
455
+ const servers = obj["servers"];
456
+ const first = servers[0];
457
+ assertField(first, "repoRoot", ["string"]);
458
+ assertField(first, "displayName", ["string"]);
459
+ assertField(first, "model", ["string", "null"]);
460
+ assertField(first, "provider", ["string", "null"]);
461
+ assertField(first, "lastSeen", ["number"]);
462
+ assertField(first, "lastCompactedAt", ["number", "null"]);
463
+ });
464
+ });
465
+ describe("GET /api/game-state", () => {
466
+ test("sample payload with active ritual validates", () => {
467
+ const raw = JSON.stringify({
468
+ config: {
469
+ enabled: true,
470
+ theme: "ocean",
471
+ displayMode: "full",
472
+ },
473
+ activeRitual: {
474
+ sessionId: "ritual-1",
475
+ stageIndex: 2,
476
+ startedAt: "2025-01-01T00:00:00Z",
477
+ elapsed: 300000,
478
+ },
479
+ });
480
+ const obj = JSON.parse(raw);
481
+ assertField(obj, "config", ["object"]);
482
+ assertField(obj, "activeRitual", ["object", "null"]);
483
+ const config = assertObject(obj, "config");
484
+ assertField(config, "enabled", ["boolean"]);
485
+ assertField(config, "theme", ["string"]);
486
+ assertField(config, "displayMode", ["string"]);
487
+ const ritual = assertObject(obj, "activeRitual");
488
+ assertField(ritual, "sessionId", ["string"]);
489
+ assertField(ritual, "stageIndex", ["number"]);
490
+ assertField(ritual, "startedAt", ["string"]);
491
+ assertField(ritual, "elapsed", ["number"]);
492
+ });
493
+ test("sample payload with null ritual validates", () => {
494
+ const raw = JSON.stringify({
495
+ config: {
496
+ enabled: false,
497
+ theme: "mono",
498
+ displayMode: "minimal",
499
+ },
500
+ activeRitual: null,
501
+ });
502
+ const obj = JSON.parse(raw);
503
+ assertField(obj, "config", ["object"]);
504
+ assertField(obj, "activeRitual", ["null"]);
505
+ });
506
+ });
507
+ describe("PUT /api/game-state", () => {
508
+ test("response payload validates field presence and types", () => {
509
+ // PUT /api/game-state returns GameStateResponse (same as GET)
510
+ const raw = JSON.stringify({
511
+ config: {
512
+ enabled: true,
513
+ theme: "ember",
514
+ displayMode: "full",
515
+ },
516
+ activeRitual: null,
517
+ });
518
+ const obj = JSON.parse(raw);
519
+ assertField(obj, "config", ["object"]);
520
+ assertField(obj, "activeRitual", ["object", "null"]);
521
+ const config = assertObject(obj, "config");
522
+ assertField(config, "enabled", ["boolean"]);
523
+ assertField(config, "theme", ["string"]);
524
+ assertField(config, "displayMode", ["string"]);
525
+ });
526
+ });
527
+ describe("GET /api/game-scores", () => {
528
+ test("sample payload (array) validates element field presence and types", () => {
529
+ const raw = JSON.stringify([
530
+ {
531
+ repo_root: "/home/user/repo1",
532
+ value: 1500,
533
+ ts: 1700000000000,
534
+ meta: { metric: "cache" },
535
+ },
536
+ ]);
537
+ const arr = JSON.parse(raw);
538
+ assert.ok(Array.isArray(arr), "response must be an array");
539
+ const first = arr[0];
540
+ assertField(first, "repo_root", ["string"]);
541
+ assertField(first, "value", ["number"]);
542
+ assertField(first, "ts", ["number"]);
543
+ assert.ok("meta" in first, 'field "meta" must exist');
544
+ });
545
+ });
546
+ describe("GET /api/perf", () => {
547
+ test("sample payload validates field presence and types", () => {
548
+ const raw = JSON.stringify({
549
+ updatedAt: "2025-01-01T00:00:00Z",
550
+ windowMinutes: 30,
551
+ sampleCount: 100,
552
+ turn_latency_ms: { p50: 500, p95: 2000, n: 100 },
553
+ provider_latency_ms: { p50: 300, p95: 1500, n: 100 },
554
+ tps: { avg: 45.5, n: 100 },
555
+ cache_hit_pct: { avg: 30, latest: 25, n: 100 },
556
+ db_recompute_ms: { p50: 50, p95: 200, n: 50 },
557
+ disk_write_ms: { p50: 10, p95: 50, n: 50 },
558
+ rss_mb: { latest: 256, n: 10 },
559
+ heap_mb: { latest: 128, n: 10 },
560
+ cpu_user_ms: { latest: 5000, n: 10 },
561
+ cpu_sys_ms: { latest: 1000, n: 10 },
562
+ diag: {
563
+ ctxFastGate: 2,
564
+ liveTrimFires: 5,
565
+ liveTrimReplays: 3,
566
+ },
567
+ });
568
+ const obj = JSON.parse(raw);
569
+ assertField(obj, "updatedAt", ["string"]);
570
+ assertField(obj, "windowMinutes", ["number"]);
571
+ assertField(obj, "sampleCount", ["number"]);
572
+ assertField(obj, "turn_latency_ms", ["object"]);
573
+ assertField(obj, "provider_latency_ms", ["object"]);
574
+ assertField(obj, "tps", ["object"]);
575
+ assertField(obj, "cache_hit_pct", ["object"]);
576
+ assertField(obj, "db_recompute_ms", ["object"]);
577
+ assertField(obj, "disk_write_ms", ["object"]);
578
+ assertField(obj, "rss_mb", ["object"]);
579
+ assertField(obj, "heap_mb", ["object"]);
580
+ assertField(obj, "cpu_user_ms", ["object"]);
581
+ assertField(obj, "cpu_sys_ms", ["object"]);
582
+ assertField(obj, "diag", ["object", "null"]);
583
+ // Spot-check percentile sub-object
584
+ const tl = assertObject(obj, "turn_latency_ms");
585
+ assertField(tl, "p50", ["number"]);
586
+ assertField(tl, "p95", ["number"]);
587
+ assertField(tl, "n", ["number"]);
588
+ const diag = assertObject(obj, "diag");
589
+ assertField(diag, "ctxFastGate", ["number"]);
590
+ assertField(diag, "liveTrimFires", ["number"]);
591
+ assertField(diag, "liveTrimReplays", ["number"]);
592
+ });
593
+ test("null diag validates", () => {
594
+ const raw = JSON.stringify({
595
+ updatedAt: "2025-01-01T00:00:00Z",
596
+ windowMinutes: 30,
597
+ sampleCount: 0,
598
+ turn_latency_ms: { p50: 0, p95: 0, n: 0 },
599
+ provider_latency_ms: { p50: 0, p95: 0, n: 0 },
600
+ tps: { avg: 0, n: 0 },
601
+ cache_hit_pct: { avg: 0, latest: 0, n: 0 },
602
+ db_recompute_ms: { p50: 0, p95: 0, n: 0 },
603
+ disk_write_ms: { p50: 0, p95: 0, n: 0 },
604
+ rss_mb: { latest: 0, n: 0 },
605
+ heap_mb: { latest: 0, n: 0 },
606
+ cpu_user_ms: { latest: 0, n: 0 },
607
+ cpu_sys_ms: { latest: 0, n: 0 },
608
+ diag: null,
609
+ });
610
+ const obj = JSON.parse(raw);
611
+ assertField(obj, "diag", ["null"]);
612
+ });
613
+ });
614
+ describe("GET /api/achievements", () => {
615
+ test("sample payload (array) validates element field presence and types", () => {
616
+ const raw = JSON.stringify([
617
+ {
618
+ id: "first-compact",
619
+ title: "First Compaction",
620
+ description: "Complete your first compaction",
621
+ hidden: 0,
622
+ icon: "trophy",
623
+ unlocked_at: 1700000000,
624
+ },
625
+ {
626
+ id: "secret-ach",
627
+ title: "Secret Achievement",
628
+ description: "???",
629
+ hidden: 1,
630
+ icon: null,
631
+ unlocked_at: null,
632
+ },
633
+ ]);
634
+ const arr = JSON.parse(raw);
635
+ assert.ok(Array.isArray(arr), "response must be an array");
636
+ const first = arr[0];
637
+ assertField(first, "id", ["string"]);
638
+ assertField(first, "title", ["string"]);
639
+ assertField(first, "description", ["string"]);
640
+ assertField(first, "hidden", ["number"]);
641
+ assertField(first, "icon", ["string", "null"]);
642
+ assertField(first, "unlocked_at", ["number", "null"]);
643
+ const second = arr[1];
644
+ assertField(second, "icon", ["null"]);
645
+ assertField(second, "unlocked_at", ["null"]);
646
+ });
647
+ });
648
+ // ─── SSE Event Union Validation ──────────────────────────────────────────────
649
+ // Validate the SseEvent union by checking each variant has a `type` field
650
+ // matching its discriminator.
651
+ describe("SSE /api/events — SseEvent union discriminator validation", () => {
652
+ /** All 20 SseEvent variants with their discriminator `type` value and a minimal payload. */
653
+ const SSE_VARIANTS = [
654
+ {
655
+ discriminator: "compact_start",
656
+ payload: JSON.stringify({
657
+ type: "compact_start",
658
+ ts: "2025-01-01T00:00:00Z",
659
+ trigger: "auto",
660
+ sessionId: "s1",
661
+ }),
662
+ },
663
+ {
664
+ discriminator: "compact_end",
665
+ payload: JSON.stringify({
666
+ type: "compact_end",
667
+ ts: "2025-01-01T00:00:00Z",
668
+ sessionId: "s1",
669
+ checkpointId: "c1",
670
+ tokensIn: 10000,
671
+ tokensOut: 4000,
672
+ tokensFreed: 6000,
673
+ success: true,
674
+ }),
675
+ },
676
+ {
677
+ discriminator: "compact_trigger",
678
+ payload: JSON.stringify({
679
+ type: "compact_trigger",
680
+ ts: "2025-01-01T00:00:00Z",
681
+ pressure: 75,
682
+ threshold: 80,
683
+ armed: true,
684
+ }),
685
+ },
686
+ {
687
+ discriminator: "compact_skip",
688
+ payload: JSON.stringify({
689
+ type: "compact_skip",
690
+ ts: "2025-01-01T00:00:00Z",
691
+ reason: "pressure insufficient",
692
+ }),
693
+ },
694
+ {
695
+ discriminator: "tier_changed",
696
+ payload: JSON.stringify({
697
+ type: "tier_changed",
698
+ ts: "2025-01-01T00:00:00Z",
699
+ from: "super-compact",
700
+ to: "ultra-compact",
701
+ contextPct: 82,
702
+ }),
703
+ },
704
+ {
705
+ discriminator: "model_changed",
706
+ payload: JSON.stringify({
707
+ type: "model_changed",
708
+ ts: "2025-01-01T00:00:00Z",
709
+ provider: "anthropic",
710
+ providerName: "Anthropic",
711
+ model: "claude-3.5-sonnet",
712
+ }),
713
+ },
714
+ {
715
+ discriminator: "pressure_lifted",
716
+ payload: JSON.stringify({
717
+ type: "pressure_lifted",
718
+ ts: "2025-01-01T00:00:00Z",
719
+ beforePct: 85,
720
+ afterPct: 40,
721
+ }),
722
+ },
723
+ {
724
+ discriminator: "checkpoint_persisted",
725
+ payload: JSON.stringify({
726
+ type: "checkpoint_persisted",
727
+ ts: "2025-01-01T00:00:00Z",
728
+ checkpointId: "ckpt-1",
729
+ sessionTokens: 8000,
730
+ }),
731
+ },
732
+ {
733
+ discriminator: "recall_inject",
734
+ payload: JSON.stringify({
735
+ type: "recall_inject",
736
+ ts: "2025-01-01T00:00:00Z",
737
+ query: "compaction history",
738
+ chunks: 5,
739
+ tokens: 2000,
740
+ }),
741
+ },
742
+ {
743
+ discriminator: "anchors_updated",
744
+ payload: JSON.stringify({
745
+ type: "anchors_updated",
746
+ ts: "2025-01-01T00:00:00Z",
747
+ count: 10,
748
+ pinned: 3,
749
+ }),
750
+ },
751
+ {
752
+ discriminator: "config_updated",
753
+ payload: JSON.stringify({
754
+ type: "config_updated",
755
+ ts: "2025-01-01T00:00:00Z",
756
+ key: "thresholdTokens",
757
+ value: 120000,
758
+ }),
759
+ },
760
+ {
761
+ discriminator: "config_preset",
762
+ payload: JSON.stringify({
763
+ type: "config_preset",
764
+ ts: "2025-01-01T00:00:00Z",
765
+ preset: "aggressive",
766
+ }),
767
+ },
768
+ {
769
+ discriminator: "crew_presence_changed",
770
+ payload: JSON.stringify({
771
+ type: "crew_presence_changed",
772
+ ts: "2025-01-01T00:00:00Z",
773
+ activeAgents: 3,
774
+ currentTurn: 1,
775
+ }),
776
+ },
777
+ {
778
+ discriminator: "crew_turn_changed",
779
+ payload: JSON.stringify({
780
+ type: "crew_turn_changed",
781
+ ts: "2025-01-01T00:00:00Z",
782
+ turnIndex: 2,
783
+ agentName: "worker-2",
784
+ }),
785
+ },
786
+ {
787
+ discriminator: "crew_bandit_chosen",
788
+ payload: JSON.stringify({
789
+ type: "crew_bandit_chosen",
790
+ ts: "2025-01-01T00:00:00Z",
791
+ chosenAgent: "worker-1",
792
+ score: 0.85,
793
+ regret: 0.12,
794
+ }),
795
+ },
796
+ {
797
+ discriminator: "game_ritual_start",
798
+ payload: JSON.stringify({
799
+ type: "game_ritual_start",
800
+ ts: "2025-01-01T00:00:00Z",
801
+ sessionId: "ritual-1",
802
+ stages: [
803
+ { index: 0, name: "warmup", durationMs: 60000, status: "pending" },
804
+ ],
805
+ }),
806
+ },
807
+ {
808
+ discriminator: "game_ritual_stage",
809
+ payload: JSON.stringify({
810
+ type: "game_ritual_stage",
811
+ ts: "2025-01-01T00:00:00Z",
812
+ sessionId: "ritual-1",
813
+ stageIndex: 1,
814
+ stageName: "deep-work",
815
+ }),
816
+ },
817
+ {
818
+ discriminator: "game_ritual_end",
819
+ payload: JSON.stringify({
820
+ type: "game_ritual_end",
821
+ ts: "2025-01-01T00:00:00Z",
822
+ sessionId: "ritual-1",
823
+ success: true,
824
+ }),
825
+ },
826
+ {
827
+ discriminator: "game_mode_changed",
828
+ payload: JSON.stringify({
829
+ type: "game_mode_changed",
830
+ ts: "2025-01-01T00:00:00Z",
831
+ config: { enabled: true, theme: "neon", displayMode: "full" },
832
+ }),
833
+ },
834
+ {
835
+ discriminator: "game_render",
836
+ payload: JSON.stringify({
837
+ type: "game_render",
838
+ ts: "2025-01-01T00:00:00Z",
839
+ frame: "\x1b[2J\x1b[H",
840
+ }),
841
+ },
842
+ ];
843
+ test("all 20 SseEvent variants have a type field matching their discriminator", () => {
844
+ assert.equal(SSE_VARIANTS.length, 20, "SseEvent union should have 20 variants");
845
+ for (const variant of SSE_VARIANTS) {
846
+ const obj = JSON.parse(variant.payload);
847
+ assert.ok("type" in obj, `SSE variant "${variant.discriminator}" must have a "type" field`);
848
+ assert.equal(typeof obj["type"], "string", `SSE variant "${variant.discriminator}" type field must be a string`);
849
+ assert.equal(obj["type"], variant.discriminator, `SSE variant type field must match discriminator "${variant.discriminator}"`);
850
+ }
851
+ });
852
+ test("every SSE variant has a ts field of type string", () => {
853
+ for (const variant of SSE_VARIANTS) {
854
+ const obj = JSON.parse(variant.payload);
855
+ assertField(obj, "ts", ["string"]);
856
+ }
857
+ });
858
+ test("SSE ENDPOINTS entry is correctly configured as SSE type", () => {
859
+ assert.equal(ENDPOINTS.events.type, "sse");
860
+ assert.equal(ENDPOINTS.events.method, "GET");
861
+ assert.equal(ENDPOINTS.events.path, "/api/events");
862
+ assert.ok(ENDPOINTS.events.event, "SSE endpoint must have an event name");
863
+ });
864
+ test("SSE discriminators are unique across all variants", () => {
865
+ const discriminators = SSE_VARIANTS.map((v) => v.discriminator);
866
+ const unique = new Set(discriminators);
867
+ assert.equal(unique.size, discriminators.length, "SSE discriminators must be unique");
868
+ });
869
+ });