@animalabs/connectome-host 0.3.1 → 0.3.7

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 (41) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/publish.yml +9 -4
  3. package/README.md +5 -0
  4. package/bun.lock +8 -4
  5. package/docs/AGENT-ONBOARDING.md +6 -1
  6. package/package.json +5 -5
  7. package/scripts/import-codex-rollout.ts +288 -0
  8. package/src/call-ledger.ts +371 -0
  9. package/src/call-pricing.ts +119 -0
  10. package/src/commands.ts +57 -3
  11. package/src/index.ts +87 -27
  12. package/src/logging-adapter.ts +72 -9
  13. package/src/modules/fleet-module.ts +21 -9
  14. package/src/modules/mcpl-admin-module.ts +6 -0
  15. package/src/modules/observers-module.ts +180 -0
  16. package/src/modules/time-module.ts +15 -9
  17. package/src/modules/web-ui-module.ts +415 -16
  18. package/src/modules/web-ui-observers.ts +322 -0
  19. package/src/recipe.ts +48 -3
  20. package/src/strategies/frontdesk-strategy.ts +10 -4
  21. package/src/web/protocol.ts +141 -0
  22. package/test/call-ledger.test.ts +91 -0
  23. package/test/call-pricing.test.ts +69 -0
  24. package/test/fleet-subscribe-union-e2e.test.ts +5 -0
  25. package/test/fleet-tree-aggregator-e2e.test.ts +2 -0
  26. package/test/frontdesk-strategy.test.ts +10 -0
  27. package/test/import-codex-rollout.test.ts +36 -0
  28. package/test/logging-adapter.test.ts +58 -1
  29. package/test/recipe-cache-ttl.test.ts +7 -3
  30. package/test/recipe-provider.test.ts +36 -0
  31. package/test/recipe-timezone.test.ts +20 -0
  32. package/test/time-module.test.ts +13 -0
  33. package/test/web-ui-context-coverage.test.ts +61 -0
  34. package/test/web-ui-observers.test.ts +344 -0
  35. package/web/package-lock.json +2446 -0
  36. package/web/src/App.tsx +24 -0
  37. package/web/src/Context.tsx +207 -7
  38. package/web/src/ObserverGate.tsx +78 -0
  39. package/web/src/Usage.tsx +116 -2
  40. package/web/src/observer-identity.ts +110 -0
  41. package/web/src/wire.ts +60 -1
@@ -0,0 +1,91 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { CallLedger, summarizeCacheControls, type ProviderCallRecord } from '../src/call-ledger.js';
3
+
4
+ const at = (seconds: number): string => new Date(Date.UTC(2026, 0, 1, 0, 0, seconds)).toISOString();
5
+
6
+ function call(overrides: Partial<ProviderCallRecord> = {}): ProviderCallRecord {
7
+ return {
8
+ timestamp: at(0),
9
+ kind: 'stream',
10
+ durationMs: 1000,
11
+ model: 'claude-fable-5',
12
+ messages: 10,
13
+ inputTokens: 2,
14
+ outputTokens: 100,
15
+ cacheReadTokens: 0,
16
+ cacheWriteTokens: 0,
17
+ cacheBreakpoints: 4,
18
+ cacheTtls: ['1h', '1h', '1h', '1h'],
19
+ stopReason: 'end_turn',
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe('CallLedger cache verdicts', () => {
25
+ test('distinguishes first write, hit, and expired rewrite using the effective TTL', () => {
26
+ const ledger = new CallLedger({ hydrate: false, defaultTtl: '1h' });
27
+ ledger.record(call({ timestamp: at(0), cacheWriteTokens: 100_000 }));
28
+ ledger.record(call({ timestamp: at(1800), cacheReadTokens: 100_000 }));
29
+ ledger.record(call({ timestamp: at(5500), cacheWriteTokens: 101_000 }));
30
+
31
+ const rows = ledger.snapshot().rows;
32
+ expect(rows.map((r) => r.verdict)).toEqual(['first-write', 'HIT', 'rewrite:expired']);
33
+ expect(rows[2]!.cause).toContain('gap 3700s > ttl 3600s');
34
+ });
35
+
36
+ test('calls without cache flags are visibly uncached', () => {
37
+ const ledger = new CallLedger({ hydrate: false });
38
+ ledger.record(call({ kind: 'complete', cacheBreakpoints: 0, cacheTtls: [], inputTokens: 48_000 }));
39
+ expect(ledger.snapshot().rows[0]).toMatchObject({
40
+ verdict: 'uncached',
41
+ cause: 'no cache_control flags in request',
42
+ });
43
+ });
44
+
45
+ test('reports hit+extend and an aggregate cache ratio', () => {
46
+ const ledger = new CallLedger({ hydrate: false });
47
+ ledger.record(call({ inputTokens: 100, cacheReadTokens: 900, cacheWriteTokens: 100 }));
48
+ const snap = ledger.snapshot();
49
+ expect(snap.rows[0]!.verdict).toBe('hit+extend');
50
+ expect(snap.summary.cacheHitRatio).toBeCloseTo(900 / 1100);
51
+ });
52
+
53
+ test('allocates an auditable per-call cost and reconciles the retained total', () => {
54
+ const ledger = new CallLedger({ hydrate: false });
55
+ ledger.record(call({
56
+ cacheWriteTokens: 336_010,
57
+ cacheWrite5mTokens: 336_010,
58
+ cacheWrite1hTokens: 0,
59
+ cacheWriteBucketsAuthoritative: true,
60
+ cacheTtls: ['5m'],
61
+ outputTokens: 639,
62
+ serviceTier: 'standard',
63
+ inferenceGeo: 'global',
64
+ }));
65
+ ledger.record(call({ model: 'unknown-model' }));
66
+
67
+ const snap = ledger.snapshot();
68
+ expect(snap.rows[0]!.cost?.total).toBeCloseTo(4.232095, 9);
69
+ expect(snap.rows[1]!.cost).toBeUndefined();
70
+ expect(snap.summary.cost).toMatchObject({
71
+ total: 4.232095,
72
+ pricedCalls: 1,
73
+ unpricedCalls: 1,
74
+ currency: 'USD',
75
+ });
76
+ });
77
+
78
+ test('does not guess a cache-write TTL bucket from request flags', () => {
79
+ const ledger = new CallLedger({ hydrate: false });
80
+ ledger.record(call({ cacheWriteTokens: 100_000, cacheTtls: ['1h'] }));
81
+ expect(ledger.snapshot().rows[0]!.cost).toBeUndefined();
82
+ });
83
+ });
84
+
85
+ test('summarizeCacheControls inventories markers without retaining content', () => {
86
+ const raw = {
87
+ system: [{ type: 'text', text: 'secret', cache_control: { type: 'ephemeral', ttl: '1h' } }],
88
+ messages: [{ role: 'user', content: [{ type: 'text', text: 'private', cache_control: { type: 'ephemeral' } }] }],
89
+ };
90
+ expect(summarizeCacheControls(raw)).toEqual({ count: 2, ttls: ['1h', 'default'] });
91
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { ANTHROPIC_PRICING_VERSION, priceAnthropicCall } from '../src/call-pricing.js';
3
+
4
+ const timestamp = '2026-07-13T12:00:00.000Z';
5
+
6
+ function usage(overrides: Partial<Parameters<typeof priceAnthropicCall>[2]> = {}) {
7
+ return {
8
+ inputTokens: 0,
9
+ outputTokens: 0,
10
+ cacheReadTokens: 0,
11
+ cacheWrite5mTokens: 0,
12
+ cacheWrite1hTokens: 0,
13
+ unclassifiedCacheWriteTokens: 0,
14
+ serviceTier: 'standard',
15
+ inferenceGeo: 'global',
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ describe('Anthropic per-call pricing', () => {
21
+ test('reproduces the ledger-dashboard Fable/Mythos 5m sample exactly', () => {
22
+ const cost = priceAnthropicCall('claude-fable-5', timestamp, usage({
23
+ inputTokens: 2,
24
+ cacheWrite5mTokens: 336_010,
25
+ outputTokens: 639,
26
+ }));
27
+
28
+ expect(cost?.total).toBeCloseTo(4.232095, 9);
29
+ expect(cost).toMatchObject({
30
+ cacheWrite5m: 4.200125,
31
+ cacheWrite1h: 0,
32
+ currency: 'USD',
33
+ grade: 'billing',
34
+ pricingVersion: ANTHROPIC_PRICING_VERSION,
35
+ });
36
+ });
37
+
38
+ test('prices 1h writes at 2x base input and cache reads at 0.1x', () => {
39
+ const write = priceAnthropicCall('claude-mythos-5', timestamp, usage({ cacheWrite1hTokens: 336_010 }));
40
+ const read = priceAnthropicCall('claude-mythos-5', timestamp, usage({ cacheReadTokens: 336_010 }));
41
+ expect(write?.cacheWrite1h).toBeCloseTo(6.7202, 9);
42
+ expect(read?.cacheRead).toBeCloseTo(0.33601, 9);
43
+ });
44
+
45
+ test('applies the US-only inference multiplier to every token category', () => {
46
+ const global = priceAnthropicCall('claude-fable-5', timestamp, usage({ inputTokens: 1_000_000 }));
47
+ const us = priceAnthropicCall('claude-fable-5', timestamp, usage({
48
+ inputTokens: 1_000_000,
49
+ inferenceGeo: 'us',
50
+ }));
51
+ expect(global?.total).toBe(10);
52
+ expect(us?.total).toBeCloseTo(11, 9);
53
+ });
54
+
55
+ test('leaves unknown rates, custom tiers, and unclassified writes unpriced', () => {
56
+ expect(priceAnthropicCall('unknown-model', timestamp, usage())).toBeUndefined();
57
+ expect(priceAnthropicCall('claude-fable-5', timestamp, usage({ serviceTier: 'priority' }))).toBeUndefined();
58
+ expect(priceAnthropicCall('claude-fable-5', timestamp, usage({
59
+ unclassifiedCacheWriteTokens: 1,
60
+ }))).toBeUndefined();
61
+ });
62
+
63
+ test('honors the published Sonnet 5 promotional cutoff', () => {
64
+ const promo = priceAnthropicCall('claude-sonnet-5', '2026-08-31T23:59:59Z', usage({ inputTokens: 1_000_000 }));
65
+ const standard = priceAnthropicCall('claude-sonnet-5', '2026-09-01T00:00:00Z', usage({ inputTokens: 1_000_000 }));
66
+ expect(promo?.total).toBe(2);
67
+ expect(standard?.total).toBe(3);
68
+ });
69
+ });
@@ -45,8 +45,11 @@ describe('FleetModule subscribe union e2e', () => {
45
45
  let tmpDir: string;
46
46
  let recipePath: string;
47
47
  let fleet: FleetModule;
48
+ let previousApiKey: string | undefined;
48
49
 
49
50
  beforeAll(async () => {
51
+ previousApiKey = process.env.ANTHROPIC_API_KEY;
52
+ process.env.ANTHROPIC_API_KEY = previousApiKey || 'sk-test-fleet-subscribe-union';
50
53
  tmpDir = mkdtempSync(join(tmpdir(), 'fkm-sub-union-'));
51
54
  recipePath = join(tmpDir, 'recipe.json');
52
55
  writeFileSync(recipePath, JSON.stringify(NARROW_RECIPE), 'utf-8');
@@ -68,6 +71,8 @@ describe('FleetModule subscribe union e2e', () => {
68
71
  } catch { /* noop */ }
69
72
  await new Promise((r) => setTimeout(r, 500));
70
73
  try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* noop */ }
74
+ if (previousApiKey === undefined) delete process.env.ANTHROPIC_API_KEY;
75
+ else process.env.ANTHROPIC_API_KEY = previousApiKey;
71
76
  });
72
77
 
73
78
  test('narrow recipe subscription gets reducer-required events forced in by FleetModule', async () => {
@@ -51,6 +51,8 @@ describe('FleetTreeAggregator — e2e against headless child', () => {
51
51
  let aggregator: FleetTreeAggregator;
52
52
 
53
53
  beforeAll(async () => {
54
+ // Children validate credentials during startup but never infer in this test.
55
+ process.env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || 'sk-test-fleet-aggregator';
54
56
  tmpDir = mkdtempSync(join(tmpdir(), 'fkm-agg-e2e-'));
55
57
  recipePath = join(tmpDir, 'recipe.json');
56
58
  writeFileSync(recipePath, JSON.stringify(CHILD_RECIPE), 'utf-8');
@@ -81,6 +81,7 @@ function makeStrategy(): TestFrontdesk {
81
81
  targetChunkTokens: 50,
82
82
  autoTickOnNewMessage: false,
83
83
  maxMessageTokens: 0,
84
+ timeZone: 'UTC',
84
85
  });
85
86
  }
86
87
 
@@ -110,6 +111,15 @@ describe('provenance wrapping', () => {
110
111
  expect(header!.endsWith('\n')).toBe(true);
111
112
  });
112
113
 
114
+ test('renders provenance time in the configured zone', () => {
115
+ const s = new TestFrontdesk({ timeZone: 'America/Los_Angeles' });
116
+ const m = msg('User', 'hello', {
117
+ serverId: 'zulip',
118
+ timestamp: '2026-07-17T14:32:00Z',
119
+ });
120
+ expect(s.pub_buildHeader(m)).toContain('07:32');
121
+ });
122
+
113
123
  test('wrapProvenance prepends header into the first text block', () => {
114
124
  const s = makeStrategy();
115
125
  const m = msg('User', 'hello', {
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ projectItem,
4
+ reconstructEffectiveHistory,
5
+ } from '../scripts/import-codex-rollout.js';
6
+
7
+ describe('Codex rollout import', () => {
8
+ test('uses replacement_history as the authoritative compaction boundary', () => {
9
+ const history = reconstructEffectiveHistory([
10
+ { type: 'response_item', payload: { type: 'message', id: 'discarded' } },
11
+ {
12
+ type: 'compacted', timestamp: '2026-07-13T00:00:00Z',
13
+ payload: { replacement_history: [
14
+ { type: 'message', role: 'user', content: 'kept' },
15
+ { type: 'compaction', id: 'cmp_1', encrypted_content: 'opaque' },
16
+ ] },
17
+ },
18
+ { type: 'response_item', payload: { type: 'reasoning', id: 'rs_2', encrypted_content: 'tail' } },
19
+ ]);
20
+
21
+ expect(history.map(entry => entry.item.id ?? entry.item.type)).toEqual([
22
+ 'message', 'cmp_1', 'rs_2',
23
+ ]);
24
+ expect(history[0]!.restoredByCompaction).toBe(true);
25
+ expect(history[2]!.restoredByCompaction).toBe(false);
26
+ });
27
+
28
+ test('keeps the exact native item on the Chronicle projection block', () => {
29
+ const item = {
30
+ type: 'message', id: 'msg_1', role: 'assistant', phase: 'commentary',
31
+ content: [{ type: 'output_text', text: 'hello' }],
32
+ };
33
+ const [block] = projectItem(item);
34
+ expect(block).toMatchObject({ type: 'text', text: 'hello', rawItem: item });
35
+ });
36
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
2
  import { LoggingAnthropicAdapter } from '../src/logging-adapter.js';
3
+ import type { ProviderCallRecord } from '../src/call-ledger.js';
3
4
  import type { ProviderRequest, ProviderResponse } from '@animalabs/membrane';
4
5
 
5
6
  // Regression guard for the reasoning passthrough. `withReasoning` injects
@@ -57,7 +58,7 @@ describe('LoggingAnthropicAdapter.withReasoning', () => {
57
58
  describe('LoggingAnthropicAdapter request logging', () => {
58
59
  const adapter = new LoggingAnthropicAdapter({ apiKey: 'test' }, '/dev/null');
59
60
  const internals = adapter as unknown as {
60
- requestSummary(r: ProviderRequest): Record<string, unknown>;
61
+ requestSummary(r: ProviderRequest, raw?: unknown): Record<string, unknown>;
61
62
  refusalRawRequest(r: ProviderResponse, raw: unknown): unknown;
62
63
  };
63
64
 
@@ -76,6 +77,20 @@ describe('LoggingAnthropicAdapter request logging', () => {
76
77
  });
77
78
  });
78
79
 
80
+ test('summarizes provider cache markers without retaining prompt content', () => {
81
+ const raw = {
82
+ messages: [{
83
+ role: 'user',
84
+ content: [{ type: 'text', text: 'do not log me', cache_control: { type: 'ephemeral', ttl: '1h' } }],
85
+ }],
86
+ };
87
+ expect(internals.requestSummary(baseRequest, raw)).toMatchObject({
88
+ cacheBreakpoints: 1,
89
+ cacheTtls: ['1h'],
90
+ });
91
+ expect(JSON.stringify(internals.requestSummary(baseRequest, raw))).not.toContain('do not log me');
92
+ });
93
+
79
94
  test('retains the raw request only for refusals', () => {
80
95
  const rawRequest = { messages: ['forensic context'] };
81
96
  const success = { raw: { stop_reason: 'end_turn' } } as unknown as ProviderResponse;
@@ -84,4 +99,46 @@ describe('LoggingAnthropicAdapter request logging', () => {
84
99
  expect(internals.refusalRawRequest(success, rawRequest)).toBeUndefined();
85
100
  expect(internals.refusalRawRequest(refusal, rawRequest)).toBe(rawRequest);
86
101
  });
102
+
103
+ test('forwards authoritative billing buckets from the provider response', () => {
104
+ const calls: ProviderCallRecord[] = [];
105
+ const observed = new LoggingAnthropicAdapter(
106
+ { apiKey: 'test' },
107
+ '/dev/null',
108
+ undefined,
109
+ (call) => calls.push(call),
110
+ ) as unknown as {
111
+ observeCall(
112
+ kind: 'complete' | 'stream',
113
+ timestamp: string,
114
+ durationMs: number,
115
+ request: ProviderRequest,
116
+ rawRequest: unknown,
117
+ response: ProviderResponse,
118
+ ): void;
119
+ };
120
+ const response = {
121
+ usage: { inputTokens: 2, outputTokens: 10, cacheCreationTokens: 100, cacheReadTokens: 50 },
122
+ raw: {
123
+ usage: {
124
+ cache_creation: {
125
+ ephemeral_5m_input_tokens: 25,
126
+ ephemeral_1h_input_tokens: 75,
127
+ },
128
+ service_tier: 'standard',
129
+ inference_geo: 'global',
130
+ },
131
+ },
132
+ } as unknown as ProviderResponse;
133
+
134
+ observed.observeCall('stream', '2026-07-13T00:00:00Z', 10, baseRequest, {}, response);
135
+ expect(calls[0]).toMatchObject({
136
+ cacheWriteTokens: 100,
137
+ cacheWrite5mTokens: 25,
138
+ cacheWrite1hTokens: 75,
139
+ cacheWriteBucketsAuthoritative: true,
140
+ serviceTier: 'standard',
141
+ inferenceGeo: 'global',
142
+ });
143
+ });
87
144
  });
@@ -4,7 +4,7 @@
4
4
  * inference).
5
5
  */
6
6
  import { describe, test, expect } from 'bun:test';
7
- import { validateRecipe } from '../src/recipe.js';
7
+ import { DEFAULT_RECIPE, validateRecipe } from '../src/recipe.js';
8
8
 
9
9
  function recipeWithCacheTtl(cacheTtl?: unknown) {
10
10
  return {
@@ -17,6 +17,10 @@ function recipeWithCacheTtl(cacheTtl?: unknown) {
17
17
  }
18
18
 
19
19
  describe('recipe agent.cacheTtl validation', () => {
20
+ test('the shipped default recipe explicitly uses "1h"', () => {
21
+ expect(DEFAULT_RECIPE.agent.cacheTtl).toBe('1h');
22
+ });
23
+
20
24
  test('accepts "5m"', () => {
21
25
  expect(validateRecipe(recipeWithCacheTtl('5m')).agent.cacheTtl).toBe('5m');
22
26
  });
@@ -25,8 +29,8 @@ describe('recipe agent.cacheTtl validation', () => {
25
29
  expect(validateRecipe(recipeWithCacheTtl('1h')).agent.cacheTtl).toBe('1h');
26
30
  });
27
31
 
28
- test('accepts unset (provider default applies)', () => {
29
- expect(validateRecipe(recipeWithCacheTtl()).agent.cacheTtl).toBeUndefined();
32
+ test('defaults unset cacheTtl to "1h"', () => {
33
+ expect(validateRecipe(recipeWithCacheTtl()).agent.cacheTtl).toBe('1h');
30
34
  });
31
35
 
32
36
  test('rejects a typo\'d TTL at load time', () => {
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { validateRecipe } from '../src/recipe.js';
3
+
4
+ function recipe(agent: Record<string, unknown> = {}) {
5
+ return { name: 'provider-test', agent: { systemPrompt: 'sys', ...agent } };
6
+ }
7
+
8
+ describe('recipe provider validation', () => {
9
+ test('preserves Anthropic as the omitted provider and accepts Responses', () => {
10
+ expect(validateRecipe(recipe()).agent.provider).toBeUndefined();
11
+ expect(validateRecipe(recipe({ provider: 'openai-responses' })).agent.provider)
12
+ .toBe('openai-responses');
13
+ });
14
+
15
+ test('accepts Responses reasoning and compaction settings', () => {
16
+ expect(validateRecipe(recipe({
17
+ provider: 'openai-responses',
18
+ responses: {
19
+ reasoningEffort: 'xhigh',
20
+ reasoningContext: 'all_turns',
21
+ compactThreshold: 100_000,
22
+ },
23
+ })).agent.responses).toEqual({
24
+ reasoningEffort: 'xhigh',
25
+ reasoningContext: 'all_turns',
26
+ compactThreshold: 100_000,
27
+ });
28
+ });
29
+
30
+ test('rejects unknown providers and malformed Responses settings', () => {
31
+ expect(() => validateRecipe(recipe({ provider: 'openai-chat' }))).toThrow(/agent.provider/);
32
+ expect(() => validateRecipe(recipe({ responses: { reasoningEffort: 'ultra' } }))).toThrow(/reasoningEffort/);
33
+ expect(() => validateRecipe(recipe({ responses: { reasoningContext: 'previous_turn' } }))).toThrow(/reasoningContext/);
34
+ expect(() => validateRecipe(recipe({ responses: { compactThreshold: 0 } }))).toThrow(/compactThreshold/);
35
+ });
36
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { validateRecipe } from '../src/recipe.js';
3
+
4
+ function recipe(timezone?: unknown) {
5
+ return { name: 'tz', agent: { systemPrompt: 'test', ...(timezone === undefined ? {} : { timezone }) } };
6
+ }
7
+
8
+ describe('recipe agent.timezone validation', () => {
9
+ test('accepts an IANA timezone', () => {
10
+ expect(validateRecipe(recipe('America/Los_Angeles')).agent.timezone).toBe('America/Los_Angeles');
11
+ });
12
+
13
+ test('allows the setting to be omitted', () => {
14
+ expect(validateRecipe(recipe()).agent.timezone).toBeUndefined();
15
+ });
16
+
17
+ test('rejects invalid timezone names', () => {
18
+ expect(() => validateRecipe(recipe('Pacific/Definitely_Not'))).toThrow(/valid IANA time zone/);
19
+ });
20
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { formatNow } from '../src/modules/time-module.js';
3
+
4
+ describe('TimeModule presentation timezone', () => {
5
+ test('formats current time independently of the host timezone', () => {
6
+ expect(formatNow(new Date('2026-07-15T12:34:56.789Z'), 'America/Los_Angeles')).toEqual({
7
+ iso: '2026-07-15T05:34:56.789-07:00',
8
+ local: '2026-07-15T05:34:56.789-07:00 [America/Los_Angeles]',
9
+ timezone: 'America/Los_Angeles',
10
+ unixMs: 1_784_118_896_789,
11
+ });
12
+ });
13
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { buildContextCoverageSnapshot } from '../src/modules/web-ui-module.js';
3
+
4
+ function contextManager(strategy: Record<string, unknown>) {
5
+ return {
6
+ currentBranch: () => ({ name: 'test-branch' }),
7
+ getStrategy: () => strategy,
8
+ getPendingWork: () => ({ description: 'Compressing chunk 2' }),
9
+ };
10
+ }
11
+
12
+ describe('context coverage snapshot', () => {
13
+ test('reports unbounded summary depth, selected depth, and queued work without text', () => {
14
+ const summaries = [
15
+ { id: 'L1-1', level: 1, content: 'secret one', tokens: 100, mergedInto: 'L2-3' },
16
+ { id: 'L1-2', level: 1, content: 'secret two', tokens: 120, mergedInto: 'L2-3' },
17
+ { id: 'L2-3', level: 2, content: 'secret parent', tokens: 80, mergedInto: 'L4-4' },
18
+ { id: 'L4-4', level: 4, content: 'secret root', tokens: 40 },
19
+ ];
20
+ const chunks = [
21
+ { index: 0, tokens: 300, compressed: true, summaryId: 'L1-1', messages: [{ id: 'm1' }, { id: 'm2' }] },
22
+ { index: 1, tokens: 200, compressed: true, summaryId: 'L1-2', messages: [{ id: 'm3' }] },
23
+ { index: 2, tokens: 250, compressed: false, messages: [{ id: 'm4' }] },
24
+ ];
25
+ const snapshot = buildContextCoverageSnapshot('fable', contextManager({
26
+ summaries,
27
+ chunks,
28
+ compressionQueue: [2],
29
+ mergeQueue: [{ level: 3, sourceIds: ['L2-a', 'L2-b'] }],
30
+ resolutions: new Map([['m1', 4], ['m2', 2], ['m3', 1]]),
31
+ pendingCompression: Promise.resolve(),
32
+ }));
33
+
34
+ expect(snapshot.branch).toBe('test-branch');
35
+ expect(snapshot.levels.map(level => level.level)).toEqual([1, 2, 4]);
36
+ expect(snapshot.chunks[0]).toMatchObject({ maxLevel: 4, selectedMin: 2, selectedMax: 4 });
37
+ expect(snapshot.chunks[2]).toMatchObject({ maxLevel: 0, queued: true });
38
+ expect(snapshot.queue).toMatchObject({
39
+ inFlight: true,
40
+ pending: 'Compressing chunk 2',
41
+ l1: [2],
42
+ merges: [{ targetLevel: 3, sourceCount: 2 }],
43
+ });
44
+ expect(JSON.stringify(snapshot)).not.toContain('secret');
45
+ expect(JSON.stringify(snapshot)).not.toContain('content');
46
+ });
47
+
48
+ test('stops coverage traversal at a dangling parent', () => {
49
+ const snapshot = buildContextCoverageSnapshot('fable', contextManager({
50
+ summaries: [{ id: 'L1-1', level: 1, tokens: 100, mergedInto: 'missing-L2' }],
51
+ chunks: [{ index: 0, tokens: 200, compressed: true, summaryId: 'L1-1', messages: [{ id: 'm1' }] }],
52
+ compressionQueue: [],
53
+ mergeQueue: [],
54
+ resolutions: new Map(),
55
+ pendingCompression: null,
56
+ }));
57
+
58
+ expect(snapshot.chunks[0].maxLevel).toBe(1);
59
+ expect(snapshot.queue.inFlight).toBe(false);
60
+ });
61
+ });