@sns-myagent/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +7 -8
  3. package/bin/.gitkeep +0 -0
  4. package/bin/snscoder.js +7 -96
  5. package/dist/cli.js +22026 -0
  6. package/package.json +4 -3
  7. package/scripts/apply-pi-natives-patch.js +82 -56
  8. package/scripts/fetch-binary.mjs +106 -217
  9. package/scripts/smoke-test.sh +92 -0
  10. package/src/adapters/telegram/bot.ts +41 -1
  11. package/src/adapters/telegram/bridge.ts +186 -0
  12. package/src/adapters/telegram/handler.ts +138 -13
  13. package/src/adapters/telegram/index.ts +12 -2
  14. package/src/agents/__tests__/config.test.ts +79 -0
  15. package/src/agents/__tests__/resilience.test.ts +119 -0
  16. package/src/agents/__tests__/strategies.test.ts +83 -0
  17. package/src/agents/config.ts +367 -0
  18. package/src/agents/ensemble.ts +230 -0
  19. package/src/agents/resilience.ts +332 -0
  20. package/src/agents/strategies/best-of-n.ts +108 -0
  21. package/src/agents/strategies/consensus.ts +104 -0
  22. package/src/agents/strategies/critic.ts +131 -0
  23. package/src/agents/strategies/index.ts +12 -0
  24. package/src/agents/strategies/types.ts +68 -0
  25. package/src/async/__tests__/task-runner.test.ts +162 -0
  26. package/src/async/__tests__/task-store.test.ts +146 -0
  27. package/src/async/index.ts +28 -1
  28. package/src/async/notifier.ts +133 -0
  29. package/src/async/task-runner.ts +175 -0
  30. package/src/async/task-store.ts +170 -0
  31. package/src/async/types.ts +70 -0
  32. package/src/cli/entry.ts +3 -1
  33. package/src/cli/index.ts +74 -55
  34. package/src/config/index.ts +1 -1
  35. package/src/debug/index.ts +1 -1
  36. package/src/internal-urls/docs-index.generated.txt +0 -2
  37. package/src/modes/components/welcome.ts +13 -6
  38. package/src/modes/controllers/event-controller.ts +1 -1
  39. package/src/modes/setup-wizard/scenes/splash.ts +1 -1
  40. package/src/session/agent-session.ts +1 -1
  41. package/src/slash-commands/builtin-registry.ts +63 -0
  42. package/src/slash-commands/helpers/task.ts +181 -0
  43. package/src/tbm/__tests__/tbm.test.ts +660 -0
  44. package/src/tbm/comm-modes.ts +165 -0
  45. package/src/tbm/config.ts +136 -0
  46. package/src/tbm/context-delta.ts +146 -0
  47. package/src/tbm/context-pyramid.ts +202 -0
  48. package/src/tbm/dashboard.ts +131 -0
  49. package/src/tbm/index.ts +247 -0
  50. package/src/tbm/lazy-skills.ts +182 -0
  51. package/src/tbm/response-cache.ts +220 -0
  52. package/src/tbm/tombstone.ts +189 -0
  53. package/src/tbm/tool-compress.ts +230 -0
  54. package/src/tools/ask.ts +1 -1
  55. package/src/tui/chat-blocks.ts +205 -0
  56. package/src/tui/chat-ui.ts +270 -0
  57. package/src/tui/code-cell.ts +90 -1
  58. package/src/tui/command-palette.ts +189 -0
  59. package/src/tui/index.ts +4 -0
  60. package/src/tui/splash.ts +130 -0
  61. package/src/ui/banner.ts +69 -29
  62. package/src/ui/colors.ts +24 -0
  63. package/src/ui/error-display.ts +130 -0
  64. package/src/ui/gradient.ts +104 -0
  65. package/src/ui/index.ts +15 -0
  66. package/src/ui/memory-toast.ts +102 -0
  67. package/src/ui/status-bar.ts +36 -30
@@ -0,0 +1,660 @@
1
+ /**
2
+ * TBM Unit Tests
3
+ *
4
+ * Tests all 7 TBM subsystems + dashboard.
5
+ * Run: npx vitest src/tbm/__tests__/tbm.test.ts
6
+ * (or: bun test src/tbm/__tests__/tbm.test.ts)
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach } from "vitest";
10
+
11
+ // We import from compiled output or use relative paths
12
+ // For test discovery, these test the pure logic functions
13
+
14
+ import { ContextDeltaCache } from "../context-delta";
15
+ import { ContextPyramid, PYRAMID_LEVELS } from "../context-pyramid";
16
+ import { LazySkillManager } from "../lazy-skills";
17
+ import { ToolCompressor, DEFAULT_TOOL_BUDGETS } from "../tool-compress";
18
+ import { CommModeManager, COMM_MODES } from "../comm-modes";
19
+ import { TombstoneManager } from "../tombstone";
20
+ import { ResponseCache } from "../response-cache";
21
+ import { renderTokenDashboard, formatCompactDashboard } from "../dashboard";
22
+ import { TbmManager } from "../index";
23
+ import { resolveTbmConfig, DEFAULT_TBM_CONFIG } from "../config";
24
+
25
+ // ═══════════════════════════════════════════════════════════════════════════
26
+ // Context Delta Cache
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+
29
+ describe("ContextDeltaCache", () => {
30
+ it("should miss on first call", () => {
31
+ const cdc = new ContextDeltaCache();
32
+ const result = cdc.computeDelta("system prompt", "user message");
33
+ expect(result.isCacheHit).toBe(false);
34
+ expect(result.sendCachePrefix).toBe(false);
35
+ expect(cdc.stats.cacheMisses).toBe(1);
36
+ expect(cdc.stats.cacheHits).toBe(0);
37
+ });
38
+
39
+ it("should hit on second call with same prefix", () => {
40
+ const cdc = new ContextDeltaCache();
41
+ cdc.computeDelta("system prompt", "msg1");
42
+ const result = cdc.computeDelta("system prompt", "msg2");
43
+ expect(result.isCacheHit).toBe(true);
44
+ expect(result.sendCachePrefix).toBe(true);
45
+ expect(cdc.stats.cacheHits).toBe(1);
46
+ expect(cdc.stats.tokensSaved).toBeGreaterThan(0);
47
+ });
48
+
49
+ it("should miss when prefix changes", () => {
50
+ const cdc = new ContextDeltaCache();
51
+ cdc.computeDelta("system prompt v1", "msg1");
52
+ const result = cdc.computeDelta("system prompt v2", "msg2");
53
+ expect(result.isCacheHit).toBe(false);
54
+ expect(cdc.stats.cacheMisses).toBe(2);
55
+ });
56
+
57
+ it("should track hit rate", () => {
58
+ const cdc = new ContextDeltaCache();
59
+ cdc.computeDelta("prefix", "msg1");
60
+ cdc.computeDelta("prefix", "msg2");
61
+ cdc.computeDelta("prefix", "msg3");
62
+ expect(cdc.hitRate).toBeCloseTo(2 / 3);
63
+ expect(cdc.savingsPercent).toBeGreaterThan(0);
64
+ });
65
+
66
+ it("should reset properly", () => {
67
+ const cdc = new ContextDeltaCache();
68
+ cdc.computeDelta("prefix", "msg1");
69
+ cdc.reset();
70
+ expect(cdc.stats.totalCalls).toBe(0);
71
+ expect(cdc.hasCachedPrefix).toBe(false);
72
+ });
73
+
74
+ it("should invalidate cache", () => {
75
+ const cdc = new ContextDeltaCache();
76
+ cdc.computeDelta("prefix", "msg1");
77
+ cdc.invalidate();
78
+ const result = cdc.computeDelta("prefix", "msg2");
79
+ expect(result.isCacheHit).toBe(false);
80
+ });
81
+ });
82
+
83
+ // ═══════════════════════════════════════════════════════════════════════════
84
+ // Context Pyramid
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+
87
+ describe("ContextPyramid", () => {
88
+ const testCtx = {
89
+ identity: "You are a helpful assistant.",
90
+ recentMessages: ["msg1", "msg2", "msg3", "msg4"],
91
+ fullHistory: ["h1", "h2", "h3", "h4", "h5"],
92
+ memories: ["memory1", "memory2"],
93
+ skills: ["skill1: desc", "skill2: desc"],
94
+ };
95
+
96
+ it("should start at configured level", () => {
97
+ const pyramid = new ContextPyramid(0);
98
+ expect(pyramid.currentLevel).toBe(0);
99
+ });
100
+
101
+ it("should include only identity at level 0", () => {
102
+ const pyramid = new ContextPyramid(0);
103
+ const result = pyramid.assembleContext(testCtx);
104
+ expect(result.level).toBe(0);
105
+ expect(result.includedComponents).toContain("identity");
106
+ expect(result.includedComponents).not.toContain("recent_messages");
107
+ });
108
+
109
+ it("should include recent messages at level 1", () => {
110
+ const pyramid = new ContextPyramid(1);
111
+ const result = pyramid.assembleContext(testCtx);
112
+ expect(result.includedComponents).toContain("recent_messages");
113
+ expect(result.context).toContain("msg2"); // last 3
114
+ });
115
+
116
+ it("should include memories at level 2", () => {
117
+ const pyramid = new ContextPyramid(2);
118
+ const result = pyramid.assembleContext(testCtx);
119
+ expect(result.includedComponents).toContain("memories");
120
+ expect(result.context).toContain("memory1");
121
+ });
122
+
123
+ it("should include skills at level 3", () => {
124
+ const pyramid = new ContextPyramid(3);
125
+ const result = pyramid.assembleContext(testCtx);
126
+ expect(result.includedComponents).toContain("skills");
127
+ });
128
+
129
+ it("should include full history at level 4", () => {
130
+ const pyramid = new ContextPyramid(4);
131
+ const result = pyramid.assembleContext(testCtx);
132
+ expect(result.includedComponents).toContain("full_history");
133
+ expect(result.context).toContain("h1");
134
+ });
135
+
136
+ it("should escalate on repeated low quality", () => {
137
+ const pyramid = new ContextPyramid(0);
138
+ pyramid.evaluateQuality({ refusal: true, requestedContext: false, tooShort: false, forceEscalate: false });
139
+ expect(pyramid.currentLevel).toBe(0); // needs 2 consecutive
140
+ pyramid.evaluateQuality({ refusal: true, requestedContext: false, tooShort: false, forceEscalate: false });
141
+ expect(pyramid.currentLevel).toBe(1);
142
+ });
143
+
144
+ it("should force escalate immediately", () => {
145
+ const pyramid = new ContextPyramid(0);
146
+ pyramid.evaluateQuality({ refusal: false, requestedContext: false, tooShort: false, forceEscalate: true });
147
+ expect(pyramid.currentLevel).toBe(1);
148
+ });
149
+
150
+ it("should de-escalate after sustained high quality", () => {
151
+ const pyramid = new ContextPyramid(2);
152
+ for (let i = 0; i < 5; i++) {
153
+ pyramid.evaluateQuality({ refusal: false, requestedContext: false, tooShort: false, forceEscalate: false });
154
+ }
155
+ expect(pyramid.currentLevel).toBe(1);
156
+ });
157
+
158
+ it("should have 5 pyramid levels defined", () => {
159
+ expect(PYRAMID_LEVELS).toHaveLength(5);
160
+ expect(PYRAMID_LEVELS[0]!.level).toBe(0);
161
+ expect(PYRAMID_LEVELS[4]!.level).toBe(4);
162
+ });
163
+ });
164
+
165
+ // ═══════════════════════════════════════════════════════════════════════════
166
+ // Lazy Skill Manager
167
+ // ═══════════════════════════════════════════════════════════════════════════
168
+
169
+ describe("LazySkillManager", () => {
170
+ const skills = [
171
+ { name: "code-review", description: "Review code for bugs" },
172
+ { name: "deploy", description: "Deploy to production" },
173
+ ];
174
+
175
+ it("should return skill index with names only", () => {
176
+ const lsm = new LazySkillManager(skills);
177
+ const index = lsm.getSkillIndex();
178
+ expect(index).toContain("code-review");
179
+ expect(index).toContain("deploy");
180
+ expect(lsm.loadedCount).toBe(0);
181
+ });
182
+
183
+ it("should load skill on demand", async () => {
184
+ const lsm = new LazySkillManager(skills);
185
+ const info = await lsm.loadSkill("code-review", async () => "Full code review guide...");
186
+ expect(info).not.toBeNull();
187
+ expect(info!.isLoaded).toBe(true);
188
+ expect(lsm.loadedCount).toBe(1);
189
+ });
190
+
191
+ it("should return null for unknown skills", async () => {
192
+ const lsm = new LazySkillManager(skills);
193
+ const info = await lsm.loadSkill("nonexistent");
194
+ expect(info).toBeNull();
195
+ });
196
+
197
+ it("should track stats correctly", async () => {
198
+ const lsm = new LazySkillManager(skills);
199
+ await lsm.loadSkill("deploy", async () => "Deploy instructions...");
200
+ const stats = lsm.stats;
201
+ expect(stats.totalSkills).toBe(2);
202
+ expect(stats.loadedSkills).toBe(1);
203
+ expect(stats.demandLoads).toBe(1);
204
+ });
205
+
206
+ it("should unload skills", async () => {
207
+ const lsm = new LazySkillManager(skills);
208
+ await lsm.loadSkill("code-review", async () => "content");
209
+ lsm.unloadSkill("code-review");
210
+ expect(lsm.loadedCount).toBe(0);
211
+ expect(lsm.getSkillContent("code-review")).toBeNull();
212
+ });
213
+ });
214
+
215
+ // ═══════════════════════════════════════════════════════════════════════════
216
+ // Tool Compressor
217
+ // ═══════════════════════════════════════════════════════════════════════════
218
+
219
+ describe("ToolCompressor", () => {
220
+ it("should not compress output within budget", () => {
221
+ const tc = new ToolCompressor();
222
+ const result = tc.compress("terminal", "short output");
223
+ expect(result.wasCompressed).toBe(false);
224
+ expect(result.output).toBe("short output");
225
+ });
226
+
227
+ it("should compress terminal output exceeding budget", () => {
228
+ const tc = new ToolCompressor();
229
+ const longOutput = "x".repeat(3000); // ~750 tokens > 500 budget
230
+ const result = tc.compress("terminal", longOutput);
231
+ expect(result.wasCompressed).toBe(true);
232
+ expect(result.outputTokens).toBeLessThan(result.inputTokens);
233
+ });
234
+
235
+ it("should strip ANSI from terminal output", () => {
236
+ const tc = new ToolCompressor();
237
+ const ansiOutput = "\x1b[31mError\x1b[0m: " + "x".repeat(3000);
238
+ const result = tc.compress("terminal", ansiOutput);
239
+ expect(result.output).not.toContain("\x1b[31m");
240
+ });
241
+
242
+ it("should compress read_file with line truncation", () => {
243
+ const tc = new ToolCompressor();
244
+ const lines = Array.from({ length: 200 }, (_, i) => `line ${i}: ${"x".repeat(40)}`).join("\n");
245
+ const result = tc.compress("read_file", lines);
246
+ expect(result.wasCompressed).toBe(true);
247
+ expect(result.output).toContain("lines omitted");
248
+ });
249
+
250
+ it("should compress search_files keeping top results", () => {
251
+ const tc = new ToolCompressor();
252
+ const results = Array.from({ length: 100 }, (_, i) => `result ${i}: ${"match".repeat(20)}`).join("\n");
253
+ const result = tc.compress("search_files", results);
254
+ expect(result.wasCompressed).toBe(true);
255
+ expect(result.output).toContain("truncated");
256
+ });
257
+
258
+ it("should track compression stats", () => {
259
+ const tc = new ToolCompressor();
260
+ tc.compress("terminal", "short");
261
+ tc.compress("terminal", "x".repeat(3000));
262
+ const stats = tc.stats;
263
+ expect(stats.totalCalls).toBe(2);
264
+ expect(stats.compressedCalls).toBe(1);
265
+ });
266
+
267
+ it("should handle unknown tools with default budget", () => {
268
+ const tc = new ToolCompressor();
269
+ const result = tc.compress("unknown_tool", "x".repeat(4000));
270
+ expect(result.wasCompressed).toBe(true);
271
+ });
272
+
273
+ it("should have all defined tool budgets", () => {
274
+ expect(DEFAULT_TOOL_BUDGETS.length).toBeGreaterThanOrEqual(4);
275
+ const terminal = DEFAULT_TOOL_BUDGETS.find(b => b.tool === "terminal");
276
+ expect(terminal?.maxTokens).toBe(500);
277
+ });
278
+ });
279
+
280
+ // ═══════════════════════════════════════════════════════════════════════════
281
+ // Communication Modes
282
+ // ═══════════════════════════════════════════════════════════════════════════
283
+
284
+ describe("CommModeManager", () => {
285
+ it("should default to auto mode", () => {
286
+ const cmm = new CommModeManager();
287
+ expect(cmm.currentMode).toBe("auto");
288
+ expect(cmm.effectiveMode).toBe("normal"); // default auto → normal
289
+ });
290
+
291
+ it("should switch modes explicitly", () => {
292
+ const cmm = new CommModeManager();
293
+ cmm.setMode("caveman");
294
+ expect(cmm.effectiveMode).toBe("caveman");
295
+ expect(cmm.targetTokens).toBe(20);
296
+ });
297
+
298
+ it("should provide directives", () => {
299
+ const cmm = new CommModeManager("verbose");
300
+ expect(cmm.directive).toContain("full explanation");
301
+ });
302
+
303
+ it("should auto-detect caveman for simple ops", () => {
304
+ const cmm = new CommModeManager("auto");
305
+ const mode = cmm.detectMode({
306
+ queryTokens: 5,
307
+ hasCode: true,
308
+ isQuestion: false,
309
+ hasReferences: false,
310
+ recentToolCalls: 10,
311
+ requestsExplanation: false,
312
+ });
313
+ expect(mode).toBe("caveman");
314
+ });
315
+
316
+ it("should auto-detect verbose for explanation requests", () => {
317
+ const cmm = new CommModeManager("auto");
318
+ const mode = cmm.detectMode({
319
+ queryTokens: 150,
320
+ hasCode: false,
321
+ isQuestion: true,
322
+ hasReferences: true,
323
+ recentToolCalls: 0,
324
+ requestsExplanation: true,
325
+ });
326
+ expect(mode).toBe("verbose");
327
+ });
328
+
329
+ it("should have all 3 mode definitions", () => {
330
+ expect(COMM_MODES.caveman).toBeDefined();
331
+ expect(COMM_MODES.normal).toBeDefined();
332
+ expect(COMM_MODES.verbose).toBeDefined();
333
+ });
334
+
335
+ it("should describe current mode", () => {
336
+ const cmm = new CommModeManager("caveman");
337
+ expect(cmm.describe()).toContain("caveman");
338
+ expect(cmm.describe()).toContain("20");
339
+ });
340
+ });
341
+
342
+ // ═══════════════════════════════════════════════════════════════════════════
343
+ // Tombstone Manager
344
+ // ═══════════════════════════════════════════════════════════════════════════
345
+
346
+ describe("TombstoneManager", () => {
347
+ it("should tombstone old messages", () => {
348
+ const tm = new TombstoneManager({ afterTurns: 3 });
349
+ const messages = [
350
+ { turnNumber: 1, role: "user", content: "First message with lots of content here" },
351
+ { turnNumber: 2, role: "assistant", content: "Response to first message with details" },
352
+ { turnNumber: 3, role: "user", content: "Second question about something" },
353
+ { turnNumber: 4, role: "assistant", content: "Answer to second question" },
354
+ { turnNumber: 5, role: "user", content: "Third question" },
355
+ { turnNumber: 6, role: "assistant", content: "Answer to third" },
356
+ ];
357
+ const result = tm.processHistory(messages, 6);
358
+ // Turns 1-3 should be tombstoned (6 - 3 = 3 cutoff)
359
+ expect(result.tombstonedCount).toBe(3);
360
+ expect(result.reductionPercent).toBeGreaterThan(0);
361
+ });
362
+
363
+ it("should keep recent messages intact", () => {
364
+ const tm = new TombstoneManager({ afterTurns: 5 });
365
+ const messages = [
366
+ { turnNumber: 1, role: "user", content: "old message" },
367
+ { turnNumber: 8, role: "user", content: "recent message" },
368
+ ];
369
+ const result = tm.processHistory(messages, 10);
370
+ expect(result.activeMessages).toHaveLength(2);
371
+ // Turn 8 should not be tombstoned
372
+ const recent = result.activeMessages.find(m => m.turnNumber === 8);
373
+ expect(recent?.content).toBe("recent message");
374
+ });
375
+
376
+ it("should retrieve original content", () => {
377
+ const tm = new TombstoneManager({ afterTurns: 2 });
378
+ const messages = [
379
+ { turnNumber: 1, role: "user", content: "original full content" },
380
+ { turnNumber: 3, role: "user", content: "recent" },
381
+ ];
382
+ tm.processHistory(messages, 3);
383
+ const original = tm.getOriginal(1);
384
+ expect(original).toBe("original full content");
385
+ });
386
+
387
+ it("should track stats", () => {
388
+ const tm = new TombstoneManager({ afterTurns: 2 });
389
+ const messages = Array.from({ length: 10 }, (_, i) => ({
390
+ turnNumber: i + 1,
391
+ role: i % 2 === 0 ? "user" : "assistant",
392
+ content: `Message ${i + 1} with some content here`,
393
+ }));
394
+ tm.processHistory(messages, 10);
395
+ expect(tm.stats.totalTombstoned).toBe(8); // turns 1-8 (cutoff = 10-2 = 8)
396
+ expect(tm.stats.archiveSize).toBe(8);
397
+ });
398
+
399
+ it("should handle afterTurns change", () => {
400
+ const tm = new TombstoneManager({ afterTurns: 10 });
401
+ tm.afterTurns = 5;
402
+ expect(tm.afterTurns).toBe(5);
403
+ });
404
+ });
405
+
406
+ // ═══════════════════════════════════════════════════════════════════════════
407
+ // Response Cache
408
+ // ═══════════════════════════════════════════════════════════════════════════
409
+
410
+ describe("ResponseCache", () => {
411
+ it("should miss on empty cache", () => {
412
+ const cache = new ResponseCache();
413
+ expect(cache.lookup("test query")).toBeNull();
414
+ expect(cache.stats.misses).toBe(1);
415
+ });
416
+
417
+ it("should hit on exact match", () => {
418
+ const cache = new ResponseCache();
419
+ cache.store("what is 2+2?", "4", 1);
420
+ expect(cache.lookup("what is 2+2?")).toBe("4");
421
+ expect(cache.stats.exactHits).toBe(1);
422
+ });
423
+
424
+ it("should hit on case-insensitive match", () => {
425
+ const cache = new ResponseCache();
426
+ cache.store("Hello World", "hi", 1);
427
+ expect(cache.lookup("hello world")).toBe("hi");
428
+ });
429
+
430
+ it("should track hit rate", () => {
431
+ const cache = new ResponseCache();
432
+ cache.store("q1", "a1", 1);
433
+ cache.lookup("q1"); // hit
434
+ cache.lookup("q2"); // miss
435
+ expect(cache.stats.hitRate).toBeCloseTo(0.5);
436
+ });
437
+
438
+ it("should expire entries based on TTL", async () => {
439
+ const cache = new ResponseCache({ ttlSeconds: 0 });
440
+ cache.store("q", "a", 1);
441
+ // TTL=0 means already expired
442
+ const result = cache.lookup("q");
443
+ expect(result).toBeNull();
444
+ });
445
+
446
+ it("should reset properly", () => {
447
+ const cache = new ResponseCache();
448
+ cache.store("q", "a", 1);
449
+ cache.reset();
450
+ expect(cache.stats.entryCount).toBe(0);
451
+ expect(cache.lookup("q")).toBeNull();
452
+ });
453
+
454
+ it("should count tokens saved", () => {
455
+ const cache = new ResponseCache();
456
+ cache.store("q", "answer", 50);
457
+ cache.lookup("q");
458
+ expect(cache.stats.tokensSaved).toBe(50);
459
+ });
460
+ });
461
+
462
+ // ═══════════════════════════════════════════════════════════════════════════
463
+ // Token Dashboard
464
+ // ═══════════════════════════════════════════════════════════════════════════
465
+
466
+ describe("Token Dashboard", () => {
467
+ const mockData: Parameters<typeof renderTokenDashboard>[0] = {
468
+ sessionStart: Date.now() - 60000,
469
+ now: Date.now(),
470
+ contextDelta: {
471
+ totalCalls: 10,
472
+ cacheHits: 8,
473
+ cacheMisses: 2,
474
+ totalInputTokens: 50000,
475
+ billedInputTokens: 20000,
476
+ tokensSaved: 30000,
477
+ lastAccessAt: Date.now(),
478
+ },
479
+ pyramidLevel: 2,
480
+ pyramidEscalations: 1,
481
+ commMode: "auto",
482
+ effectiveCommMode: "normal",
483
+ compression: {
484
+ totalCalls: 5,
485
+ compressedCalls: 2,
486
+ tokensBeforeCompression: 10000,
487
+ tokensAfterCompression: 7000,
488
+ byTool: {},
489
+ },
490
+ responseCache: {
491
+ totalLookups: 20,
492
+ exactHits: 15,
493
+ semanticHits: 2,
494
+ misses: 3,
495
+ tokensSaved: 5000,
496
+ entryCount: 15,
497
+ hitRate: 0.85,
498
+ },
499
+ tombstone: {
500
+ totalTombstoned: 5,
501
+ tokensBefore: 2000,
502
+ tokensAfter: 300,
503
+ reductionPercent: 85,
504
+ archiveSize: 5,
505
+ },
506
+ skills: {
507
+ totalSkills: 10,
508
+ loadedSkills: 3,
509
+ demandLoads: 3,
510
+ totalLoadedTokens: 1500,
511
+ tokensSaved: 30000,
512
+ },
513
+ totalInputTokens: 50000,
514
+ totalOutputTokens: 10000,
515
+ totalCachedTokens: 30000,
516
+ inputCostPer1k: 0.003,
517
+ outputCostPer1k: 0.015,
518
+ };
519
+
520
+ it("should render full dashboard", () => {
521
+ const report = renderTokenDashboard(mockData);
522
+ expect(report).toContain("Token Budget Manager Dashboard");
523
+ expect(report).toContain("Pyramid level:");
524
+ expect(report).toContain("Comm mode:");
525
+ expect(report).toContain("Response Cache:");
526
+ expect(report).toContain("Compression:");
527
+ expect(report).toContain("Tombstoning:");
528
+ expect(report).toContain("Skills:");
529
+ });
530
+
531
+ it("should render compact dashboard", () => {
532
+ const compact = formatCompactDashboard(mockData);
533
+ expect(compact).toContain("TBM:");
534
+ expect(compact).toContain("L2");
535
+ expect(compact).toContain("normal");
536
+ });
537
+ });
538
+
539
+ // ═══════════════════════════════════════════════════════════════════════════
540
+ // TBM Config
541
+ // ═══════════════════════════════════════════════════════════════════════════
542
+
543
+ describe("TBM Config", () => {
544
+ it("should use defaults with no overrides", () => {
545
+ const config = resolveTbmConfig();
546
+ expect(config.enabled).toBe(true);
547
+ expect(config.context_delta.enabled).toBe(true);
548
+ expect(config.pyramid.enabled).toBe(true);
549
+ expect(config.tombstone.after_turns).toBe(10);
550
+ });
551
+
552
+ it("should merge overrides", () => {
553
+ const config = resolveTbmConfig({
554
+ enabled: false,
555
+ comm_mode: "caveman",
556
+ tombstone: { after_turns: 5 },
557
+ });
558
+ expect(config.enabled).toBe(false);
559
+ expect(config.comm_mode).toBe("caveman");
560
+ expect(config.tombstone.after_turns).toBe(5);
561
+ // Other defaults preserved
562
+ expect(config.pyramid.enabled).toBe(true);
563
+ });
564
+
565
+ it("should have sensible defaults", () => {
566
+ expect(DEFAULT_TBM_CONFIG.tombstone.after_turns).toBe(10);
567
+ expect(DEFAULT_TBM_CONFIG.response_cache.ttl_seconds).toBe(3600);
568
+ expect(DEFAULT_TBM_CONFIG.response_cache.semantic_threshold).toBe(0.95);
569
+ });
570
+ });
571
+
572
+ // ═══════════════════════════════════════════════════════════════════════════
573
+ // TBM Manager (Integration)
574
+ // ═══════════════════════════════════════════════════════════════════════════
575
+
576
+ describe("TbmManager", () => {
577
+ it("should initialize with all subsystems", () => {
578
+ const tbm = new TbmManager();
579
+ expect(tbm.enabled).toBe(true);
580
+ expect(tbm.contextDelta).toBeDefined();
581
+ expect(tbm.pyramid).toBeDefined();
582
+ expect(tbm.compressor).toBeDefined();
583
+ expect(tbm.commMode).toBeDefined();
584
+ expect(tbm.tombstone).toBeDefined();
585
+ expect(tbm.responseCache).toBeDefined();
586
+ });
587
+
588
+ it("should record token usage", () => {
589
+ const tbm = new TbmManager();
590
+ tbm.recordUsage(1000, 500, 200);
591
+ const data = tbm.dashboardData();
592
+ expect(data.totalInputTokens).toBe(1000);
593
+ expect(data.totalOutputTokens).toBe(500);
594
+ expect(data.totalCachedTokens).toBe(200);
595
+ });
596
+
597
+ it("should process tool output through compressor", () => {
598
+ const tbm = new TbmManager();
599
+ const result = tbm.processToolOutput("terminal", "short output");
600
+ expect(result).toBe("short output");
601
+ });
602
+
603
+ it("should check response cache", () => {
604
+ const tbm = new TbmManager();
605
+ expect(tbm.checkCache("test")).toBeNull();
606
+ tbm.storeInCache("test", "answer", 10);
607
+ expect(tbm.checkCache("test")).toBe("answer");
608
+ });
609
+
610
+ it("should provide comm directive", () => {
611
+ const tbm = new TbmManager({ comm_mode: "caveman" });
612
+ expect(tbm.getCommDirective()).toContain("caveman");
613
+ });
614
+
615
+ it("should provide skill index", () => {
616
+ const tbm = new TbmManager(undefined, [
617
+ { name: "test-skill", description: "A test skill" },
618
+ ]);
619
+ expect(tbm.getSkillIndex()).toContain("test-skill");
620
+ });
621
+
622
+ it("should tombstone old messages", () => {
623
+ const tbm = new TbmManager({ tombstone: { after_turns: 3 } });
624
+ const messages = Array.from({ length: 10 }, (_, i) => ({
625
+ turnNumber: i + 1,
626
+ role: "user",
627
+ content: `Message ${i + 1}`,
628
+ }));
629
+ const result = tbm.tombstoneHistory(messages, 10);
630
+ expect(result.tombstonedCount).toBeGreaterThan(0);
631
+ });
632
+
633
+ it("should render full dashboard", () => {
634
+ const tbm = new TbmManager();
635
+ tbm.recordUsage(1000, 500);
636
+ const report = tbm.dashboard();
637
+ expect(report).toContain("Token Budget Manager Dashboard");
638
+ });
639
+
640
+ it("should render compact dashboard", () => {
641
+ const tbm = new TbmManager();
642
+ const compact = tbm.compactDashboard();
643
+ expect(compact).toContain("TBM:");
644
+ });
645
+
646
+ it("should reset all subsystems", () => {
647
+ const tbm = new TbmManager();
648
+ tbm.recordUsage(1000, 500);
649
+ tbm.responseCache.store("q", "a");
650
+ tbm.reset();
651
+ const data = tbm.dashboardData();
652
+ expect(data.totalInputTokens).toBe(0);
653
+ });
654
+
655
+ it("should respect disabled config", () => {
656
+ const tbm = new TbmManager({ enabled: false, response_cache: { enabled: false } });
657
+ expect(tbm.checkCache("test")).toBeNull();
658
+ expect(tbm.getCommDirective()).toBe("");
659
+ });
660
+ });