@kernel.chat/kbot 3.51.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.
- package/README.md +43 -9
- package/dist/agent-protocol.test.d.ts +2 -0
- package/dist/agent-protocol.test.d.ts.map +1 -0
- package/dist/agent-protocol.test.js +730 -0
- package/dist/agent-protocol.test.js.map +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +34 -10
- package/dist/agent.js.map +1 -1
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/bench.d.ts +64 -0
- package/dist/bench.d.ts.map +1 -0
- package/dist/bench.js +973 -0
- package/dist/bench.js.map +1 -0
- package/dist/cli.js +144 -29
- package/dist/cli.js.map +1 -1
- package/dist/cloud-agent.d.ts +77 -0
- package/dist/cloud-agent.d.ts.map +1 -0
- package/dist/cloud-agent.js +743 -0
- package/dist/cloud-agent.js.map +1 -0
- package/dist/context.test.d.ts +2 -0
- package/dist/context.test.d.ts.map +1 -0
- package/dist/context.test.js +561 -0
- package/dist/context.test.js.map +1 -0
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +4 -1
- package/dist/evolution.js.map +1 -1
- package/dist/github-release.d.ts +61 -0
- package/dist/github-release.d.ts.map +1 -0
- package/dist/github-release.js +451 -0
- package/dist/github-release.js.map +1 -0
- package/dist/graph-memory.test.d.ts +2 -0
- package/dist/graph-memory.test.d.ts.map +1 -0
- package/dist/graph-memory.test.js +946 -0
- package/dist/graph-memory.test.js.map +1 -0
- package/dist/init-science.d.ts +43 -0
- package/dist/init-science.d.ts.map +1 -0
- package/dist/init-science.js +477 -0
- package/dist/init-science.js.map +1 -0
- package/dist/lab.d.ts +45 -0
- package/dist/lab.d.ts.map +1 -0
- package/dist/lab.js +1020 -0
- package/dist/lab.js.map +1 -0
- package/dist/lsp-deep.d.ts +101 -0
- package/dist/lsp-deep.d.ts.map +1 -0
- package/dist/lsp-deep.js +689 -0
- package/dist/lsp-deep.js.map +1 -0
- package/dist/memory.test.d.ts +2 -0
- package/dist/memory.test.d.ts.map +1 -0
- package/dist/memory.test.js +369 -0
- package/dist/memory.test.js.map +1 -0
- package/dist/multi-session.d.ts +164 -0
- package/dist/multi-session.d.ts.map +1 -0
- package/dist/multi-session.js +885 -0
- package/dist/multi-session.js.map +1 -0
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +5 -2
- package/dist/self-eval.js.map +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +0 -1
- package/dist/streaming.js.map +1 -1
- package/dist/teach.d.ts +136 -0
- package/dist/teach.d.ts.map +1 -0
- package/dist/teach.js +915 -0
- package/dist/teach.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/browser-agent.js +2 -2
- package/dist/tools/browser-agent.js.map +1 -1
- package/dist/tools/forge.d.ts.map +1 -1
- package/dist/tools/forge.js +15 -26
- package/dist/tools/forge.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +10 -7
- package/dist/tools/git.js.map +1 -1
- package/dist/voice-realtime.d.ts +54 -0
- package/dist/voice-realtime.d.ts.map +1 -0
- package/dist/voice-realtime.js +805 -0
- package/dist/voice-realtime.js.map +1 -0
- 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
|