@kernel.chat/kbot 3.50.0 → 3.52.0

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 (84) hide show
  1. package/README.md +43 -9
  2. package/dist/agent-protocol.test.d.ts +2 -0
  3. package/dist/agent-protocol.test.d.ts.map +1 -0
  4. package/dist/agent-protocol.test.js +730 -0
  5. package/dist/agent-protocol.test.js.map +1 -0
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +34 -10
  8. package/dist/agent.js.map +1 -1
  9. package/dist/auth.js +3 -3
  10. package/dist/auth.js.map +1 -1
  11. package/dist/bench.d.ts +64 -0
  12. package/dist/bench.d.ts.map +1 -0
  13. package/dist/bench.js +973 -0
  14. package/dist/bench.js.map +1 -0
  15. package/dist/cli.js +144 -29
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud-agent.d.ts +77 -0
  18. package/dist/cloud-agent.d.ts.map +1 -0
  19. package/dist/cloud-agent.js +743 -0
  20. package/dist/cloud-agent.js.map +1 -0
  21. package/dist/context.test.d.ts +2 -0
  22. package/dist/context.test.d.ts.map +1 -0
  23. package/dist/context.test.js +561 -0
  24. package/dist/context.test.js.map +1 -0
  25. package/dist/evolution.d.ts.map +1 -1
  26. package/dist/evolution.js +4 -1
  27. package/dist/evolution.js.map +1 -1
  28. package/dist/github-release.d.ts +61 -0
  29. package/dist/github-release.d.ts.map +1 -0
  30. package/dist/github-release.js +451 -0
  31. package/dist/github-release.js.map +1 -0
  32. package/dist/graph-memory.test.d.ts +2 -0
  33. package/dist/graph-memory.test.d.ts.map +1 -0
  34. package/dist/graph-memory.test.js +946 -0
  35. package/dist/graph-memory.test.js.map +1 -0
  36. package/dist/init-science.d.ts +43 -0
  37. package/dist/init-science.d.ts.map +1 -0
  38. package/dist/init-science.js +477 -0
  39. package/dist/init-science.js.map +1 -0
  40. package/dist/lab.d.ts +45 -0
  41. package/dist/lab.d.ts.map +1 -0
  42. package/dist/lab.js +1020 -0
  43. package/dist/lab.js.map +1 -0
  44. package/dist/lsp-deep.d.ts +101 -0
  45. package/dist/lsp-deep.d.ts.map +1 -0
  46. package/dist/lsp-deep.js +689 -0
  47. package/dist/lsp-deep.js.map +1 -0
  48. package/dist/memory.test.d.ts +2 -0
  49. package/dist/memory.test.d.ts.map +1 -0
  50. package/dist/memory.test.js +369 -0
  51. package/dist/memory.test.js.map +1 -0
  52. package/dist/multi-session.d.ts +164 -0
  53. package/dist/multi-session.d.ts.map +1 -0
  54. package/dist/multi-session.js +885 -0
  55. package/dist/multi-session.js.map +1 -0
  56. package/dist/self-eval.d.ts.map +1 -1
  57. package/dist/self-eval.js +5 -2
  58. package/dist/self-eval.js.map +1 -1
  59. package/dist/streaming.d.ts.map +1 -1
  60. package/dist/streaming.js +0 -1
  61. package/dist/streaming.js.map +1 -1
  62. package/dist/teach.d.ts +136 -0
  63. package/dist/teach.d.ts.map +1 -0
  64. package/dist/teach.js +915 -0
  65. package/dist/teach.js.map +1 -0
  66. package/dist/telemetry.d.ts +1 -1
  67. package/dist/telemetry.d.ts.map +1 -1
  68. package/dist/telemetry.js.map +1 -1
  69. package/dist/tools/ableton.d.ts.map +1 -1
  70. package/dist/tools/ableton.js +255 -1
  71. package/dist/tools/ableton.js.map +1 -1
  72. package/dist/tools/browser-agent.js +2 -2
  73. package/dist/tools/browser-agent.js.map +1 -1
  74. package/dist/tools/forge.d.ts.map +1 -1
  75. package/dist/tools/forge.js +15 -26
  76. package/dist/tools/forge.js.map +1 -1
  77. package/dist/tools/git.d.ts.map +1 -1
  78. package/dist/tools/git.js +10 -7
  79. package/dist/tools/git.js.map +1 -1
  80. package/dist/voice-realtime.d.ts +54 -0
  81. package/dist/voice-realtime.d.ts.map +1 -0
  82. package/dist/voice-realtime.js +805 -0
  83. package/dist/voice-realtime.js.map +1 -0
  84. package/package.json +10 -3
@@ -0,0 +1,730 @@
1
+ // Tests for kbot Agent Collaboration Protocol
2
+ // Covers: Handoff, Blackboard, Negotiation, Trust Delegation
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ // Mock external dependencies before importing module under test
5
+ vi.mock('fs', () => ({
6
+ readFileSync: vi.fn(),
7
+ writeFileSync: vi.fn(),
8
+ mkdirSync: vi.fn(),
9
+ }));
10
+ vi.mock('os', () => ({
11
+ homedir: vi.fn(() => '/mock-home'),
12
+ }));
13
+ vi.mock('./tools/index.js', () => ({
14
+ registerTool: vi.fn(),
15
+ }));
16
+ import { createHandoff, acceptHandoff, rejectHandoff, completeHandoff, getActiveHandoffs, getHandoffHistory, blackboardWrite, blackboardRead, blackboardQuery, blackboardSubscribe, blackboardGetDecisions, blackboardClear, propose, vote, resolveProposal, getConsensus, getTrust, updateTrust, getMostTrusted, getTrustReport, registerAgentProtocolTools, } from './agent-protocol.js';
17
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
18
+ import { registerTool } from './tools/index.js';
19
+ const mockedReadFileSync = vi.mocked(readFileSync);
20
+ const mockedWriteFileSync = vi.mocked(writeFileSync);
21
+ const mockedMkdirSync = vi.mocked(mkdirSync);
22
+ const mockedRegisterTool = vi.mocked(registerTool);
23
+ // ── Helpers ──
24
+ /** Clear all in-memory state between tests by exploiting the module's maps.
25
+ * Since handoffs/proposals/blackboard are module-level Maps, we need to
26
+ * drain them via the public API or accept stale state across tests.
27
+ * We clear blackboard explicitly and rely on unique IDs for handoffs/proposals. */
28
+ function clearBlackboard() {
29
+ blackboardClear();
30
+ }
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ clearBlackboard();
34
+ });
35
+ // ─── 1. Handoff Protocol ────────────────────────────────────────────────────
36
+ describe('createHandoff', () => {
37
+ it('creates a handoff with correct fields', () => {
38
+ const h = createHandoff('coder', 'researcher', 'Need research', 'Find papers on X');
39
+ expect(h.from).toBe('coder');
40
+ expect(h.to).toBe('researcher');
41
+ expect(h.reason).toBe('Need research');
42
+ expect(h.context).toBe('Find papers on X');
43
+ expect(h.artifacts).toEqual([]);
44
+ expect(h.priority).toBe('normal');
45
+ expect(h.status).toBe('pending');
46
+ expect(h.id).toHaveLength(8); // shortId = 4 bytes = 8 hex chars
47
+ expect(h.created).toBeTruthy();
48
+ expect(h.updated).toBeTruthy();
49
+ expect(h.result).toBeUndefined();
50
+ expect(h.rejectionReason).toBeUndefined();
51
+ });
52
+ it('accepts optional artifacts and priority', () => {
53
+ const h = createHandoff('a', 'b', 'r', 'c', ['file.ts', 'data.json'], 'critical');
54
+ expect(h.artifacts).toEqual(['file.ts', 'data.json']);
55
+ expect(h.priority).toBe('critical');
56
+ });
57
+ it('generates unique IDs for each handoff', () => {
58
+ const h1 = createHandoff('a', 'b', 'r', 'c');
59
+ const h2 = createHandoff('a', 'b', 'r', 'c');
60
+ expect(h1.id).not.toBe(h2.id);
61
+ });
62
+ it('handles empty strings for from, to, reason, context', () => {
63
+ const h = createHandoff('', '', '', '');
64
+ expect(h.from).toBe('');
65
+ expect(h.to).toBe('');
66
+ expect(h.reason).toBe('');
67
+ expect(h.context).toBe('');
68
+ expect(h.status).toBe('pending');
69
+ });
70
+ });
71
+ describe('acceptHandoff', () => {
72
+ it('accepts a pending handoff', () => {
73
+ const h = createHandoff('coder', 'writer', 'docs needed', 'Write API docs');
74
+ const accepted = acceptHandoff(h.id);
75
+ expect(accepted.status).toBe('accepted');
76
+ expect(accepted.id).toBe(h.id);
77
+ });
78
+ it('throws if handoff not found', () => {
79
+ expect(() => acceptHandoff('nonexistent')).toThrow('Handoff nonexistent not found');
80
+ });
81
+ it('throws if handoff is not pending', () => {
82
+ const h = createHandoff('a', 'b', 'r', 'c');
83
+ acceptHandoff(h.id);
84
+ expect(() => acceptHandoff(h.id)).toThrow(`Handoff ${h.id} is accepted, cannot accept`);
85
+ });
86
+ it('throws if handoff was already rejected', () => {
87
+ const h = createHandoff('a', 'b', 'r', 'c');
88
+ rejectHandoff(h.id, 'busy');
89
+ expect(() => acceptHandoff(h.id)).toThrow(`Handoff ${h.id} is rejected, cannot accept`);
90
+ });
91
+ });
92
+ describe('rejectHandoff', () => {
93
+ it('rejects a pending handoff with a reason', () => {
94
+ const h = createHandoff('coder', 'writer', 'docs', 'ctx');
95
+ const rejected = rejectHandoff(h.id, 'Not my domain');
96
+ expect(rejected.status).toBe('rejected');
97
+ expect(rejected.rejectionReason).toBe('Not my domain');
98
+ });
99
+ it('throws if handoff not found', () => {
100
+ expect(() => rejectHandoff('ghost', 'nope')).toThrow('Handoff ghost not found');
101
+ });
102
+ it('throws if handoff is not pending', () => {
103
+ const h = createHandoff('a', 'b', 'r', 'c');
104
+ acceptHandoff(h.id);
105
+ expect(() => rejectHandoff(h.id, 'late')).toThrow(`Handoff ${h.id} is accepted, cannot reject`);
106
+ });
107
+ });
108
+ describe('completeHandoff', () => {
109
+ it('completes an accepted handoff with result', () => {
110
+ const h = createHandoff('a', 'b', 'r', 'c');
111
+ acceptHandoff(h.id);
112
+ const completed = completeHandoff(h.id, 'Task done successfully');
113
+ expect(completed.status).toBe('completed');
114
+ expect(completed.result).toBe('Task done successfully');
115
+ });
116
+ it('throws if handoff not found', () => {
117
+ expect(() => completeHandoff('nope', 'result')).toThrow('Handoff nope not found');
118
+ });
119
+ it('throws if handoff is still pending (not accepted)', () => {
120
+ const h = createHandoff('a', 'b', 'r', 'c');
121
+ expect(() => completeHandoff(h.id, 'result')).toThrow(`Handoff ${h.id} is pending, must be accepted first`);
122
+ });
123
+ it('throws if handoff was rejected', () => {
124
+ const h = createHandoff('a', 'b', 'r', 'c');
125
+ rejectHandoff(h.id, 'nope');
126
+ expect(() => completeHandoff(h.id, 'result')).toThrow(`Handoff ${h.id} is rejected, must be accepted first`);
127
+ });
128
+ it('updates trust for the completing agent', () => {
129
+ const h = createHandoff('a', 'b', 'r', 'c');
130
+ acceptHandoff(h.id);
131
+ // completeHandoff calls updateTrust(h.to, 'handoff', true) internally
132
+ // which triggers saveTrust -> writeFileSync
133
+ completeHandoff(h.id, 'done');
134
+ expect(mockedWriteFileSync).toHaveBeenCalled();
135
+ });
136
+ });
137
+ describe('getActiveHandoffs', () => {
138
+ it('returns empty array when filtering by agent with no pending handoffs', () => {
139
+ // Use a unique agent name to avoid pollution from other tests
140
+ const uniqueTarget = `agent-none-${Date.now()}`;
141
+ const active = getActiveHandoffs(uniqueTarget);
142
+ expect(active).toEqual([]);
143
+ });
144
+ it('returns only pending handoffs', () => {
145
+ const uniqueTarget = `target-${Date.now()}-pending`;
146
+ const h1 = createHandoff('a', uniqueTarget, 'r1', 'c1');
147
+ const h2 = createHandoff('a', uniqueTarget, 'r2', 'c2');
148
+ acceptHandoff(h2.id);
149
+ const active = getActiveHandoffs(uniqueTarget);
150
+ expect(active).toHaveLength(1);
151
+ expect(active[0].id).toBe(h1.id);
152
+ });
153
+ it('filters by target agent', () => {
154
+ const agentX = `agent-x-${Date.now()}`;
155
+ const agentY = `agent-y-${Date.now()}`;
156
+ createHandoff('a', agentX, 'r', 'c');
157
+ createHandoff('a', agentY, 'r', 'c');
158
+ const active = getActiveHandoffs(agentX);
159
+ expect(active).toHaveLength(1);
160
+ expect(active[0].to).toBe(agentX);
161
+ });
162
+ it('returns all pending handoffs when no agentId filter', () => {
163
+ const h1 = createHandoff('a', 'unfiltered-1', 'r', 'c', [], 'low');
164
+ const h2 = createHandoff('a', 'unfiltered-2', 'r', 'c', [], 'high');
165
+ const active = getActiveHandoffs();
166
+ // At least these two (plus any pending from earlier tests)
167
+ const ids = active.map(h => h.id);
168
+ expect(ids).toContain(h1.id);
169
+ expect(ids).toContain(h2.id);
170
+ });
171
+ it('sorts by priority: critical > high > normal > low', () => {
172
+ const hLow = createHandoff('a', 'z', 'r', 'c', [], 'low');
173
+ const hCrit = createHandoff('a', 'z', 'r', 'c', [], 'critical');
174
+ const hNorm = createHandoff('a', 'z', 'r', 'c', [], 'normal');
175
+ const hHigh = createHandoff('a', 'z', 'r', 'c', [], 'high');
176
+ const active = getActiveHandoffs('z');
177
+ const priorities = active.map(h => h.priority);
178
+ // critical(0) should come before high(1) before normal(2) before low(3)
179
+ const idxCrit = active.findIndex(h => h.id === hCrit.id);
180
+ const idxHigh = active.findIndex(h => h.id === hHigh.id);
181
+ const idxNorm = active.findIndex(h => h.id === hNorm.id);
182
+ const idxLow = active.findIndex(h => h.id === hLow.id);
183
+ expect(idxCrit).toBeLessThan(idxHigh);
184
+ expect(idxHigh).toBeLessThan(idxNorm);
185
+ expect(idxNorm).toBeLessThan(idxLow);
186
+ });
187
+ });
188
+ describe('getHandoffHistory', () => {
189
+ it('returns handoffs including the ones we created', () => {
190
+ const h1 = createHandoff('hist-from', 'hist-to', 'first', 'c');
191
+ const h2 = createHandoff('hist-from', 'hist-to', 'second', 'c');
192
+ const history = getHandoffHistory();
193
+ const ids = history.map(h => h.id);
194
+ // Both should be present in history
195
+ expect(ids).toContain(h1.id);
196
+ expect(ids).toContain(h2.id);
197
+ // History is sorted newest first — both have same-millisecond timestamps
198
+ // so relative order among them is implementation-dependent, but the sort
199
+ // itself should not throw
200
+ expect(history.length).toBeGreaterThanOrEqual(2);
201
+ });
202
+ it('includes handoffs of all statuses', () => {
203
+ const h1 = createHandoff('hist-a', 'hist-b', 'r', 'c');
204
+ const h2 = createHandoff('hist-a', 'hist-b', 'r', 'c');
205
+ acceptHandoff(h1.id);
206
+ rejectHandoff(h2.id, 'no');
207
+ const history = getHandoffHistory();
208
+ const statuses = history.map(h => h.status);
209
+ expect(statuses).toContain('accepted');
210
+ expect(statuses).toContain('rejected');
211
+ });
212
+ });
213
+ // ─── 2. Blackboard (Shared Working Memory) ──────────────────────────────────
214
+ describe('blackboardWrite', () => {
215
+ it('writes an entry with correct fields', () => {
216
+ const entry = blackboardWrite('arch-decision', 'Use microservices', 'architect', 'decision', 0.9);
217
+ expect(entry.key).toBe('arch-decision');
218
+ expect(entry.value).toBe('Use microservices');
219
+ expect(entry.author).toBe('architect');
220
+ expect(entry.type).toBe('decision');
221
+ expect(entry.confidence).toBe(0.9);
222
+ expect(entry.timestamp).toBeTruthy();
223
+ expect(entry.subscribers).toEqual([]);
224
+ });
225
+ it('defaults confidence to 1.0', () => {
226
+ const entry = blackboardWrite('k', 'v', 'a', 'fact');
227
+ expect(entry.confidence).toBe(1.0);
228
+ });
229
+ it('clamps confidence to [0, 1] range', () => {
230
+ const over = blackboardWrite('k1', 'v', 'a', 'fact', 1.5);
231
+ expect(over.confidence).toBe(1.0);
232
+ clearBlackboard();
233
+ const under = blackboardWrite('k2', 'v', 'a', 'fact', -0.5);
234
+ expect(under.confidence).toBe(0.0);
235
+ });
236
+ it('overwrites existing entry for same key', () => {
237
+ blackboardWrite('key', 'old value', 'a', 'fact');
238
+ const updated = blackboardWrite('key', 'new value', 'b', 'hypothesis', 0.7);
239
+ expect(updated.value).toBe('new value');
240
+ expect(updated.author).toBe('b');
241
+ expect(updated.type).toBe('hypothesis');
242
+ const read = blackboardRead('key');
243
+ expect(read?.value).toBe('new value');
244
+ });
245
+ it('preserves subscribers when overwriting', () => {
246
+ blackboardWrite('sub-key', 'v1', 'a', 'fact');
247
+ blackboardSubscribe('sub-key', 'watcher-1');
248
+ const updated = blackboardWrite('sub-key', 'v2', 'b', 'fact');
249
+ expect(updated.subscribers).toContain('watcher-1');
250
+ });
251
+ it('notifies subscribers on write', () => {
252
+ blackboardWrite('notify-key', 'initial', 'a', 'fact');
253
+ const callback = vi.fn();
254
+ blackboardSubscribe('notify-key', 'watcher', callback);
255
+ blackboardWrite('notify-key', 'updated', 'b', 'fact');
256
+ expect(callback).toHaveBeenCalledTimes(1);
257
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ key: 'notify-key', value: 'updated' }));
258
+ });
259
+ it('does not propagate subscriber callback errors', () => {
260
+ blackboardWrite('err-key', 'v', 'a', 'fact');
261
+ const badCallback = vi.fn(() => { throw new Error('boom'); });
262
+ blackboardSubscribe('err-key', 'watcher', badCallback);
263
+ // Should not throw
264
+ expect(() => blackboardWrite('err-key', 'v2', 'b', 'fact')).not.toThrow();
265
+ expect(badCallback).toHaveBeenCalledTimes(1);
266
+ });
267
+ it('accepts non-string values (objects, arrays, numbers)', () => {
268
+ const objEntry = blackboardWrite('obj', { foo: 'bar' }, 'a', 'artifact');
269
+ expect(objEntry.value).toEqual({ foo: 'bar' });
270
+ const arrEntry = blackboardWrite('arr', [1, 2, 3], 'a', 'artifact');
271
+ expect(arrEntry.value).toEqual([1, 2, 3]);
272
+ const numEntry = blackboardWrite('num', 42, 'a', 'fact');
273
+ expect(numEntry.value).toBe(42);
274
+ });
275
+ });
276
+ describe('blackboardRead', () => {
277
+ it('returns undefined for nonexistent key', () => {
278
+ expect(blackboardRead('does-not-exist')).toBeUndefined();
279
+ });
280
+ it('returns the entry for an existing key', () => {
281
+ blackboardWrite('read-test', 'hello', 'a', 'fact');
282
+ const entry = blackboardRead('read-test');
283
+ expect(entry).toBeDefined();
284
+ expect(entry.value).toBe('hello');
285
+ });
286
+ });
287
+ describe('blackboardQuery', () => {
288
+ it('returns all entries when no type filter', () => {
289
+ blackboardWrite('q1', 'v', 'a', 'fact');
290
+ blackboardWrite('q2', 'v', 'a', 'hypothesis');
291
+ const all = blackboardQuery();
292
+ expect(all.length).toBeGreaterThanOrEqual(2);
293
+ });
294
+ it('filters entries by type', () => {
295
+ blackboardWrite('fact-1', 'v', 'a', 'fact');
296
+ blackboardWrite('hyp-1', 'v', 'a', 'hypothesis');
297
+ blackboardWrite('fact-2', 'v', 'a', 'fact');
298
+ const facts = blackboardQuery('fact');
299
+ const allFact = facts.every(e => e.type === 'fact');
300
+ expect(allFact).toBe(true);
301
+ expect(facts.length).toBeGreaterThanOrEqual(2);
302
+ });
303
+ it('returns empty array when no entries match type', () => {
304
+ // After clearBlackboard in beforeEach, only things we write exist
305
+ blackboardWrite('only-fact', 'v', 'a', 'fact');
306
+ const questions = blackboardQuery('question');
307
+ expect(questions).toEqual([]);
308
+ });
309
+ });
310
+ describe('blackboardSubscribe', () => {
311
+ it('adds agentId to subscribers list of an existing entry', () => {
312
+ blackboardWrite('sub-entry', 'v', 'a', 'fact');
313
+ blackboardSubscribe('sub-entry', 'agent-1');
314
+ const entry = blackboardRead('sub-entry');
315
+ expect(entry.subscribers).toContain('agent-1');
316
+ });
317
+ it('does not duplicate subscriber if already present', () => {
318
+ blackboardWrite('dup-entry', 'v', 'a', 'fact');
319
+ blackboardSubscribe('dup-entry', 'agent-1');
320
+ blackboardSubscribe('dup-entry', 'agent-1');
321
+ const entry = blackboardRead('dup-entry');
322
+ const count = entry.subscribers.filter(s => s === 'agent-1').length;
323
+ expect(count).toBe(1);
324
+ });
325
+ it('does nothing if key does not exist yet (no entry to add subscriber to)', () => {
326
+ // Subscribe to a non-existent key — should not throw
327
+ expect(() => blackboardSubscribe('ghost-key', 'agent-1')).not.toThrow();
328
+ });
329
+ it('registers callback that fires on subsequent writes', () => {
330
+ blackboardWrite('cb-key', 'v', 'a', 'fact');
331
+ const cb = vi.fn();
332
+ blackboardSubscribe('cb-key', 'watcher', cb);
333
+ // Write twice
334
+ blackboardWrite('cb-key', 'v2', 'b', 'fact');
335
+ blackboardWrite('cb-key', 'v3', 'c', 'fact');
336
+ expect(cb).toHaveBeenCalledTimes(2);
337
+ });
338
+ });
339
+ describe('blackboardGetDecisions', () => {
340
+ it('returns only decision-type entries', () => {
341
+ blackboardWrite('d1', 'use postgres', 'a', 'decision');
342
+ blackboardWrite('f1', 'some fact', 'a', 'fact');
343
+ blackboardWrite('d2', 'use redis', 'b', 'decision');
344
+ const decisions = blackboardGetDecisions();
345
+ expect(decisions.length).toBeGreaterThanOrEqual(2);
346
+ expect(decisions.every(e => e.type === 'decision')).toBe(true);
347
+ });
348
+ });
349
+ describe('blackboardClear', () => {
350
+ it('removes all entries', () => {
351
+ blackboardWrite('clear-1', 'v', 'a', 'fact');
352
+ blackboardWrite('clear-2', 'v', 'a', 'fact');
353
+ blackboardClear();
354
+ expect(blackboardQuery()).toEqual([]);
355
+ });
356
+ it('clears subscriptions too', () => {
357
+ blackboardWrite('sub-clear', 'v', 'a', 'fact');
358
+ const cb = vi.fn();
359
+ blackboardSubscribe('sub-clear', 'watcher', cb);
360
+ blackboardClear();
361
+ // Rewrite same key — callback should NOT fire because subscriptions were cleared
362
+ blackboardWrite('sub-clear', 'v2', 'b', 'fact');
363
+ expect(cb).not.toHaveBeenCalled();
364
+ });
365
+ it('is safe to call when already empty', () => {
366
+ blackboardClear();
367
+ expect(() => blackboardClear()).not.toThrow();
368
+ expect(blackboardQuery()).toEqual([]);
369
+ });
370
+ });
371
+ // ─── 3. Negotiation ─────────────────────────────────────────────────────────
372
+ describe('propose', () => {
373
+ it('creates a proposal with correct fields', () => {
374
+ const p = propose('coder', 'Use TypeScript strict mode', 'Catches more bugs');
375
+ expect(p.author).toBe('coder');
376
+ expect(p.description).toBe('Use TypeScript strict mode');
377
+ expect(p.rationale).toBe('Catches more bugs');
378
+ expect(p.status).toBe('open');
379
+ expect(p.id).toHaveLength(8);
380
+ expect(p.created).toBeTruthy();
381
+ });
382
+ it('author implicitly votes agree', () => {
383
+ const p = propose('coder', 'desc', 'rationale');
384
+ expect(p.votes.size).toBe(1);
385
+ expect(p.votes.get('coder')).toEqual({ vote: 'agree' });
386
+ });
387
+ it('generates unique IDs', () => {
388
+ const p1 = propose('a', 'd1', 'r1');
389
+ const p2 = propose('a', 'd2', 'r2');
390
+ expect(p1.id).not.toBe(p2.id);
391
+ });
392
+ it('handles empty description and rationale', () => {
393
+ const p = propose('a', '', '');
394
+ expect(p.description).toBe('');
395
+ expect(p.rationale).toBe('');
396
+ expect(p.status).toBe('open');
397
+ });
398
+ });
399
+ describe('vote', () => {
400
+ it('records an agree vote', () => {
401
+ const p = propose('author', 'desc', 'rationale');
402
+ const updated = vote(p.id, 'reviewer', 'agree', 'Good idea');
403
+ expect(updated.votes.get('reviewer')).toEqual({ vote: 'agree', reason: 'Good idea' });
404
+ });
405
+ it('records a disagree vote', () => {
406
+ const p = propose('author', 'desc', 'rationale');
407
+ const updated = vote(p.id, 'critic', 'disagree', 'Too complex');
408
+ expect(updated.votes.get('critic')).toEqual({ vote: 'disagree', reason: 'Too complex' });
409
+ });
410
+ it('records an abstain vote', () => {
411
+ const p = propose('author', 'desc', 'rationale');
412
+ vote(p.id, 'neutral', 'abstain');
413
+ expect(p.votes.get('neutral')).toEqual({ vote: 'abstain', reason: undefined });
414
+ });
415
+ it('allows changing vote by same agent', () => {
416
+ const p = propose('author', 'desc', 'rationale');
417
+ vote(p.id, 'reviewer', 'agree');
418
+ vote(p.id, 'reviewer', 'disagree', 'Changed mind');
419
+ expect(p.votes.get('reviewer')).toEqual({ vote: 'disagree', reason: 'Changed mind' });
420
+ });
421
+ it('throws if proposal not found', () => {
422
+ expect(() => vote('invalid', 'a', 'agree')).toThrow('Proposal invalid not found');
423
+ });
424
+ it('throws if proposal is not open', () => {
425
+ const p = propose('author', 'desc', 'rationale');
426
+ resolveProposal(p.id); // close it
427
+ expect(() => vote(p.id, 'late', 'agree')).toThrow(`Proposal ${p.id} is accepted, voting closed`);
428
+ });
429
+ });
430
+ describe('resolveProposal', () => {
431
+ it('accepts when agrees > disagrees', () => {
432
+ const p = propose('a', 'desc', 'rationale'); // a: agree
433
+ vote(p.id, 'b', 'agree');
434
+ vote(p.id, 'c', 'disagree');
435
+ // 2 agrees, 1 disagree
436
+ const resolved = resolveProposal(p.id);
437
+ expect(resolved.status).toBe('accepted');
438
+ expect(resolved.resolution).toContain('Accepted');
439
+ expect(resolved.resolution).toContain('2 agree');
440
+ expect(resolved.resolution).toContain('1 disagree');
441
+ });
442
+ it('rejects when disagrees > agrees', () => {
443
+ const p = propose('a', 'desc', 'rationale'); // a: agree
444
+ vote(p.id, 'b', 'disagree');
445
+ vote(p.id, 'c', 'disagree');
446
+ // 1 agree, 2 disagree
447
+ const resolved = resolveProposal(p.id);
448
+ expect(resolved.status).toBe('rejected');
449
+ expect(resolved.resolution).toContain('Rejected');
450
+ });
451
+ it('handles tie with trust-weighted tiebreaking', () => {
452
+ // Use fresh agent names that have no accumulated trust (both get DEFAULT_TRUST 0.5)
453
+ const freshA = `tie-agree-${Date.now()}`;
454
+ const freshB = `tie-disagree-${Date.now()}`;
455
+ const p = propose(freshA, 'desc', 'rationale'); // freshA: agree
456
+ vote(p.id, freshB, 'disagree');
457
+ // 1 agree, 1 disagree — tie
458
+ const resolved = resolveProposal(p.id);
459
+ // Both have default trust 0.5, so agreeWeight == disagreeWeight
460
+ // Code: if (agreeWeight >= disagreeWeight) → accepted
461
+ expect(resolved.status).toBe('accepted');
462
+ expect(resolved.resolution).toContain('trust-weighted tiebreak');
463
+ });
464
+ it('abstain votes do not count toward majority', () => {
465
+ const p = propose('a', 'desc', 'rationale'); // a: agree
466
+ vote(p.id, 'b', 'abstain');
467
+ vote(p.id, 'c', 'abstain');
468
+ // 1 agree, 0 disagree, 2 abstain
469
+ const resolved = resolveProposal(p.id);
470
+ expect(resolved.status).toBe('accepted');
471
+ });
472
+ it('throws if proposal not found', () => {
473
+ expect(() => resolveProposal('ghost')).toThrow('Proposal ghost not found');
474
+ });
475
+ it('throws if proposal already resolved', () => {
476
+ const p = propose('a', 'desc', 'rationale');
477
+ resolveProposal(p.id);
478
+ expect(() => resolveProposal(p.id)).toThrow(`Proposal ${p.id} is already accepted`);
479
+ });
480
+ it('uses trust scores for tie resolution when trust differs', () => {
481
+ // Give agent 'high-trust' a higher trust score
482
+ updateTrust('high-trust', 'coding', true); // 0.5 + 0.05 = 0.55
483
+ updateTrust('high-trust', 'coding', true); // 0.55 + 0.05 = 0.60
484
+ updateTrust('low-trust', 'coding', false); // 0.5 - 0.10 = 0.40
485
+ const p = propose('high-trust', 'my plan', 'it is good'); // high-trust: agree
486
+ vote(p.id, 'low-trust', 'disagree');
487
+ // Tie: 1v1
488
+ // high-trust overall > low-trust overall
489
+ const resolved = resolveProposal(p.id);
490
+ expect(resolved.status).toBe('accepted');
491
+ expect(resolved.resolution).toContain('trust-weighted tiebreak');
492
+ });
493
+ });
494
+ describe('getConsensus', () => {
495
+ it('returns all proposals including those we created', () => {
496
+ const p1 = propose('cons-a', 'first proposal', 'r');
497
+ const p2 = propose('cons-a', 'second proposal', 'r');
498
+ const all = getConsensus();
499
+ const ids = all.map(p => p.id);
500
+ expect(ids).toContain(p1.id);
501
+ expect(ids).toContain(p2.id);
502
+ // Sorted by created desc — both may share the same timestamp
503
+ expect(all.length).toBeGreaterThanOrEqual(2);
504
+ });
505
+ it('includes proposals of all statuses', () => {
506
+ const p1 = propose('a', 'open one', 'r');
507
+ const p2 = propose('a', 'resolved one', 'r');
508
+ resolveProposal(p2.id);
509
+ const all = getConsensus();
510
+ const statuses = all.map(p => p.status);
511
+ expect(statuses).toContain('open');
512
+ expect(statuses).toContain('accepted');
513
+ });
514
+ });
515
+ // ─── 4. Trust Delegation ────────────────────────────────────────────────────
516
+ describe('getTrust', () => {
517
+ it('returns DEFAULT_TRUST (0.5) for unknown agent', () => {
518
+ const score = getTrust('brand-new-agent');
519
+ expect(score).toBe(0.5);
520
+ });
521
+ it('returns DEFAULT_TRUST for unknown domain on known agent', () => {
522
+ updateTrust('known-agent', 'coding', true); // creates profile
523
+ const score = getTrust('known-agent', 'unknown-domain');
524
+ expect(score).toBe(0.5);
525
+ });
526
+ it('returns overall trust when no domain specified', () => {
527
+ updateTrust('agent-o', 'coding', true); // domain coding: 0.55, overall: 0.55
528
+ const score = getTrust('agent-o');
529
+ expect(score).toBe(0.55);
530
+ });
531
+ it('returns domain-specific trust when domain specified', () => {
532
+ updateTrust('agent-d', 'coding', true); // coding: 0.55
533
+ updateTrust('agent-d', 'research', false); // research: 0.40
534
+ const codingTrust = getTrust('agent-d', 'coding');
535
+ const researchTrust = getTrust('agent-d', 'research');
536
+ expect(codingTrust).toBe(0.55);
537
+ expect(researchTrust).toBe(0.4);
538
+ });
539
+ });
540
+ describe('updateTrust', () => {
541
+ it('increments trust by 0.05 on success', () => {
542
+ updateTrust('inc-agent', 'security', true);
543
+ expect(getTrust('inc-agent', 'security')).toBeCloseTo(0.55, 2);
544
+ });
545
+ it('decrements trust by 0.10 on failure', () => {
546
+ updateTrust('dec-agent', 'security', false);
547
+ expect(getTrust('dec-agent', 'security')).toBeCloseTo(0.40, 2);
548
+ });
549
+ it('caps trust at 1.0 maximum', () => {
550
+ // Start at 0.5, increment 11 times: 0.5 + 11*0.05 = 1.05 → capped at 1.0
551
+ for (let i = 0; i < 11; i++) {
552
+ updateTrust('max-agent', 'domain', true);
553
+ }
554
+ expect(getTrust('max-agent', 'domain')).toBe(1.0);
555
+ });
556
+ it('floors trust at 0.0 minimum', () => {
557
+ // Start at 0.5, decrement 6 times: 0.5 - 6*0.10 = -0.1 → floored at 0.0
558
+ for (let i = 0; i < 6; i++) {
559
+ updateTrust('min-agent', 'domain', false);
560
+ }
561
+ expect(getTrust('min-agent', 'domain')).toBe(0.0);
562
+ });
563
+ it('recalculates overall trust as average of all domains', () => {
564
+ updateTrust('avg-agent', 'a', true); // a: 0.55
565
+ updateTrust('avg-agent', 'b', false); // b: 0.40
566
+ // overall = (0.55 + 0.40) / 2 = 0.475
567
+ expect(getTrust('avg-agent')).toBeCloseTo(0.475, 3);
568
+ });
569
+ it('persists trust to file via writeFileSync', () => {
570
+ updateTrust('persist-agent', 'coding', true);
571
+ expect(mockedMkdirSync).toHaveBeenCalledWith('/mock-home/.kbot', { recursive: true });
572
+ expect(mockedWriteFileSync).toHaveBeenCalledWith('/mock-home/.kbot/trust.json', expect.any(String));
573
+ });
574
+ it('records history entries', () => {
575
+ // We can verify indirectly by checking the report
576
+ updateTrust('history-agent', 'coding', true);
577
+ updateTrust('history-agent', 'coding', false);
578
+ const report = getTrustReport();
579
+ expect(report).toContain('history-agent');
580
+ expect(report).toContain('Recent:');
581
+ expect(report).toContain('+-');
582
+ });
583
+ it('does not crash if writeFileSync fails', () => {
584
+ mockedWriteFileSync.mockImplementation(() => { throw new Error('EACCES'); });
585
+ expect(() => updateTrust('write-fail-agent', 'domain', true)).not.toThrow();
586
+ });
587
+ });
588
+ describe('getMostTrusted', () => {
589
+ it('returns the agent with highest trust in a domain', () => {
590
+ updateTrust('low-trust-mt', 'writing', false); // 0.40
591
+ updateTrust('high-trust-mt', 'writing', true); // 0.55
592
+ updateTrust('high-trust-mt', 'writing', true); // 0.60
593
+ const best = getMostTrusted('writing');
594
+ expect(best).not.toBeNull();
595
+ expect(best.agentId).toBe('high-trust-mt');
596
+ expect(best.trust).toBeCloseTo(0.60, 2);
597
+ });
598
+ it('returns null when no trust profiles exist at all', () => {
599
+ // This test relies on the module state — if profiles exist from other tests,
600
+ // it won't return null. Instead, verify it returns a result with default trust
601
+ // for an obscure domain where no one has data.
602
+ const result = getMostTrusted('completely-obscure-domain-xyz');
603
+ // With existing profiles, it will return one of them with default 0.5
604
+ // The function returns null only if trustProfiles.size === 0
605
+ if (result) {
606
+ expect(result.trust).toBe(0.5);
607
+ }
608
+ });
609
+ });
610
+ describe('getTrustReport', () => {
611
+ it('includes header and agent info', () => {
612
+ updateTrust('report-agent', 'coding', true);
613
+ const report = getTrustReport();
614
+ expect(report).toContain('Agent Trust Report');
615
+ expect(report).toContain('report-agent');
616
+ expect(report).toContain('coding');
617
+ });
618
+ it('shows visual bar representation', () => {
619
+ updateTrust('bar-agent', 'testing', true);
620
+ const report = getTrustReport();
621
+ // Bar uses block chars
622
+ expect(report).toContain('█');
623
+ expect(report).toContain('░');
624
+ });
625
+ it('shows recent history as +/- symbols', () => {
626
+ updateTrust('hist-agent', 'design', true);
627
+ updateTrust('hist-agent', 'design', false);
628
+ updateTrust('hist-agent', 'design', true);
629
+ const report = getTrustReport();
630
+ expect(report).toContain('Recent: +-+');
631
+ });
632
+ });
633
+ // ─── 5. Tool Registration ───────────────────────────────────────────────────
634
+ describe('registerAgentProtocolTools', () => {
635
+ it('registers 5 tools', () => {
636
+ registerAgentProtocolTools();
637
+ expect(mockedRegisterTool).toHaveBeenCalledTimes(5);
638
+ });
639
+ it('registers agent_handoff tool', () => {
640
+ registerAgentProtocolTools();
641
+ const calls = mockedRegisterTool.mock.calls.map(c => c[0].name);
642
+ expect(calls).toContain('agent_handoff');
643
+ });
644
+ it('registers blackboard_write tool', () => {
645
+ registerAgentProtocolTools();
646
+ const calls = mockedRegisterTool.mock.calls.map(c => c[0].name);
647
+ expect(calls).toContain('blackboard_write');
648
+ });
649
+ it('registers blackboard_read tool', () => {
650
+ registerAgentProtocolTools();
651
+ const calls = mockedRegisterTool.mock.calls.map(c => c[0].name);
652
+ expect(calls).toContain('blackboard_read');
653
+ });
654
+ it('registers agent_propose tool', () => {
655
+ registerAgentProtocolTools();
656
+ const calls = mockedRegisterTool.mock.calls.map(c => c[0].name);
657
+ expect(calls).toContain('agent_propose');
658
+ });
659
+ it('registers agent_trust tool', () => {
660
+ registerAgentProtocolTools();
661
+ const calls = mockedRegisterTool.mock.calls.map(c => c[0].name);
662
+ expect(calls).toContain('agent_trust');
663
+ });
664
+ it('all registered tools have tier set to free', () => {
665
+ registerAgentProtocolTools();
666
+ for (const call of mockedRegisterTool.mock.calls) {
667
+ expect(call[0].tier).toBe('free');
668
+ }
669
+ });
670
+ it('all registered tools have execute functions', () => {
671
+ registerAgentProtocolTools();
672
+ for (const call of mockedRegisterTool.mock.calls) {
673
+ expect(typeof call[0].execute).toBe('function');
674
+ }
675
+ });
676
+ });
677
+ // ─── 6. Edge Cases ──────────────────────────────────────────────────────────
678
+ describe('edge cases', () => {
679
+ it('handles unicode in handoff fields', () => {
680
+ const h = createHandoff('codeur', 'chercheur', 'Besoin de recherche 🔍', 'Contexte: données françaises');
681
+ expect(h.reason).toBe('Besoin de recherche 🔍');
682
+ expect(h.context).toBe('Contexte: données françaises');
683
+ });
684
+ it('handles very long context in handoff', () => {
685
+ const longContext = 'x'.repeat(100_000);
686
+ const h = createHandoff('a', 'b', 'reason', longContext);
687
+ expect(h.context).toHaveLength(100_000);
688
+ });
689
+ it('handles null-ish values in blackboard', () => {
690
+ const e1 = blackboardWrite('null-val', null, 'a', 'fact');
691
+ expect(e1.value).toBeNull();
692
+ const e2 = blackboardWrite('undef-val', undefined, 'a', 'fact');
693
+ expect(e2.value).toBeUndefined();
694
+ const e3 = blackboardWrite('zero-val', 0, 'a', 'fact');
695
+ expect(e3.value).toBe(0);
696
+ const e4 = blackboardWrite('false-val', false, 'a', 'fact');
697
+ expect(e4.value).toBe(false);
698
+ const e5 = blackboardWrite('empty-str', '', 'a', 'fact');
699
+ expect(e5.value).toBe('');
700
+ });
701
+ it('full handoff lifecycle: create -> accept -> complete', () => {
702
+ const h = createHandoff('coder', 'writer', 'Write docs', 'API docs needed', ['api.ts'], 'high');
703
+ expect(h.status).toBe('pending');
704
+ const accepted = acceptHandoff(h.id);
705
+ expect(accepted.status).toBe('accepted');
706
+ const completed = completeHandoff(h.id, 'Docs written and committed');
707
+ expect(completed.status).toBe('completed');
708
+ expect(completed.result).toBe('Docs written and committed');
709
+ });
710
+ it('full negotiation lifecycle: propose -> vote -> resolve', () => {
711
+ const p = propose('architect', 'Monorepo structure', 'Simplifies dependencies');
712
+ vote(p.id, 'coder', 'agree', 'Makes imports cleaner');
713
+ vote(p.id, 'devops', 'agree', 'Easier CI/CD');
714
+ vote(p.id, 'analyst', 'disagree', 'Migration cost too high');
715
+ const resolved = resolveProposal(p.id);
716
+ // 3 agree (architect + coder + devops) vs 1 disagree (analyst)
717
+ expect(resolved.status).toBe('accepted');
718
+ expect(resolved.resolution).toContain('3 agree');
719
+ expect(resolved.resolution).toContain('1 disagree');
720
+ });
721
+ it('confidence boundary values', () => {
722
+ const exact0 = blackboardWrite('c0', 'v', 'a', 'fact', 0);
723
+ expect(exact0.confidence).toBe(0);
724
+ const exact1 = blackboardWrite('c1', 'v', 'a', 'fact', 1);
725
+ expect(exact1.confidence).toBe(1);
726
+ const tiny = blackboardWrite('ct', 'v', 'a', 'fact', 0.001);
727
+ expect(tiny.confidence).toBeCloseTo(0.001, 3);
728
+ });
729
+ });
730
+ //# sourceMappingURL=agent-protocol.test.js.map