-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathtest-phase5e.ts
More file actions
588 lines (506 loc) · 25.6 KB
/
test-phase5e.ts
File metadata and controls
588 lines (506 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
/**
* Phase 5 Part 5 — Configurable Consistency Levels Tests
*
* Tests for lib/consistency.ts:
* - ConsistencyLevel type
* - FlushableBackend interface + isFlushable() guard
* - ConsistentBackend (eventual / session / strong)
* - Integration with NamedBlackboardOptions.consistency
*
* No real Redis, file I/O, or network connections — all in-process.
* Run with: npx ts-node test-phase5e.ts
*/
import {
ConsistentBackend,
isFlushable,
type ConsistencyLevel,
type FlushableBackend,
} from './lib/consistency';
import { MemoryBackend } from './lib/blackboard-backend';
import type { BlackboardBackend } from './lib/blackboard-backend';
// ============================================================================
// TEST HARNESS
// ============================================================================
let passed = 0;
let failed = 0;
const failures: string[] = [];
function assert(condition: boolean, message: string): void {
if (condition) {
passed++;
console.log(` [PASS] ${message}`);
} else {
failed++;
failures.push(message);
console.log(` [FAIL] ${message}`);
}
}
function section(title: string): void {
console.log(`\n${title}`);
}
// ============================================================================
// HELPERS
// ============================================================================
/** A mock FlushableBackend that records flush() calls. */
class MockFlushableBackend extends MemoryBackend implements FlushableBackend {
flushCallCount = 0;
async flush(): Promise<void> {
this.flushCallCount++;
}
}
/** A plain MemoryBackend with no flush() method. */
function makeNonFlushable(): BlackboardBackend {
return new MemoryBackend();
}
async function main(): Promise<void> {
// ==========================================================================
// 1. isFlushable type guard
// ==========================================================================
section('1. isFlushable type guard');
{
const flushable = new MockFlushableBackend();
const nonFlushable = makeNonFlushable();
assert(isFlushable(flushable) === true, 'MockFlushableBackend is flushable');
assert(isFlushable(nonFlushable) === false,'plain MemoryBackend is not flushable');
// Duck-typing: any object with flush() is flushable
const duckFlushable = { flush: async () => {} } as unknown as BlackboardBackend;
assert(isFlushable(duckFlushable) === true, 'duck-typed flush() is detected');
const noFlush = {} as BlackboardBackend;
assert(isFlushable(noFlush) === false, 'empty object is not flushable');
}
// ==========================================================================
// 2. Construction — defaults
// ==========================================================================
section('2. Construction and defaults');
{
const backend = new ConsistentBackend(new MemoryBackend());
assert(backend.consistencyLevel === 'eventual', 'default level is eventual');
assert(backend.sessionSize === 0, 'sessionSize is 0 initially');
const session = new ConsistentBackend(new MemoryBackend(), 'session');
assert(session.consistencyLevel === 'session', 'session level stored correctly');
const strong = new ConsistentBackend(new MemoryBackend(), 'strong');
assert(strong.consistencyLevel === 'strong', 'strong level stored correctly');
}
// ==========================================================================
// 3. backend getter
// ==========================================================================
section('3. ConsistentBackend.backend getter');
{
const inner = new MemoryBackend();
const wrapper = new ConsistentBackend(inner, 'eventual');
assert(wrapper.backend === inner, 'backend getter returns inner backend');
}
// ==========================================================================
// 4. Eventual — read/write/delete delegate to backend
// ==========================================================================
section('4. Eventual: delegation');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'eventual');
assert(backend.read('missing') === null, 'eventual read returns null for missing key');
const entry = backend.write('k', 'v', 'agent-1');
assert(entry.key === 'k', 'write returns entry with correct key');
assert(entry.value === 'v', 'write returns entry with correct value');
assert(entry.source_agent === 'agent-1','write returns entry with correct agent');
assert(entry.version === 1, 'first write is version 1');
const read = backend.read('k');
assert(read?.value === 'v', 'eventual read returns written value');
const del = backend.delete('k');
assert(del === true, 'delete returns true for existing key');
assert(backend.read('k') === null, 'read returns null after delete');
assert(backend.delete('k') === false, 'delete returns false for missing key');
}
// ==========================================================================
// 5. Eventual — listKeys / getSnapshot delegate
// ==========================================================================
section('5. Eventual: listKeys and getSnapshot');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'eventual');
backend.write('a', 1, 'ag');
backend.write('b', 2, 'ag');
const keys = backend.listKeys().sort();
assert(keys.length === 2, 'eventual listKeys returns 2 keys');
assert(keys[0] === 'a', 'eventual listKeys has a');
assert(keys[1] === 'b', 'eventual listKeys has b');
const snap = backend.getSnapshot();
assert(Object.keys(snap).length === 2, 'eventual snapshot has 2 entries');
}
// ==========================================================================
// 6. Eventual — sessionSize is always 0
// ==========================================================================
section('6. Eventual: sessionSize stays 0');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'eventual');
backend.write('k', 'v', 'ag');
backend.write('k2', 'v2', 'ag');
assert(backend.sessionSize === 0, 'eventual sessionSize is always 0');
}
// ==========================================================================
// 7. Eventual — writeAsync completes immediately (no flush on MemoryBackend)
// ==========================================================================
section('7. Eventual: writeAsync resolves without flush');
{
const flushable = new MockFlushableBackend();
const backend = new ConsistentBackend(flushable, 'eventual');
const entry = await backend.writeAsync('k', 'v', 'ag');
assert(entry.value === 'v', 'writeAsync returns correct entry');
assert(flushable.flushCallCount === 0, 'eventual writeAsync does NOT call flush()');
}
// ==========================================================================
// 8. Session — read-your-writes basic
// ==========================================================================
section('8. Session: read-your-writes');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'session');
backend.write('task', 'done', 'agent-1');
assert(backend.read('task')?.value === 'done', 'session: read returns own write immediately');
assert(backend.sessionSize === 1, 'session: sessionSize=1 after one write');
}
// ==========================================================================
// 9. Session — overwrite updates session cache
// ==========================================================================
section('9. Session: overwrite updates session cache');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'session');
backend.write('k', 'v1', 'ag');
backend.write('k', 'v2', 'ag');
assert(backend.read('k')?.value === 'v2', 'session: second write overrides first in cache');
assert(backend.sessionSize === 1, 'session: sessionSize stays 1 for same key');
}
// ==========================================================================
// 10. Session — version increments
// ==========================================================================
section('10. Session: version increments');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'session');
const e1 = backend.write('k', 'v1', 'ag');
const e2 = backend.write('k', 'v2', 'ag');
assert(e1.version === 1, 'first write version=1');
assert(e2.version === 2, 'second write version=2');
}
// ==========================================================================
// 11. Session — delete creates tombstone
// ==========================================================================
section('11. Session: delete tombstone');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'session');
backend.write('item', 'value', 'ag');
const del = backend.delete('item');
assert(del === true, 'delete returns true for existing key');
assert(backend.read('item') === null, 'session read returns null after delete');
assert(backend.sessionSize === 1, 'session tombstone is in session cache (size=1)');
}
// ==========================================================================
// 12. Session — delete of never-written key returns false
// ==========================================================================
section('12. Session: delete of never-written key');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'session');
const del = backend.delete('ghost');
assert(del === false, 'delete returns false for never-written key');
}
// ==========================================================================
// 13. Session — delete hides backend entry
// ==========================================================================
section('13. Session: delete hides backend entry written via inner backend');
{
const inner = new MemoryBackend();
inner.write('existing', 'old', 'ag'); // write directly to inner backend
const backend = new ConsistentBackend(inner, 'session');
// Without any session write, backend entry is visible
assert(backend.read('existing')?.value === 'old', 'pre-existing backend entry is visible');
// Session-delete it
backend.delete('existing');
assert(backend.read('existing') === null, 'session delete hides backend entry from read');
}
// ==========================================================================
// 14. Session — clearSession() resets
// ==========================================================================
section('14. Session: clearSession()');
{
const inner = new MemoryBackend();
const backend = new ConsistentBackend(inner, 'session');
backend.write('k', 'session-value', 'ag');
assert(backend.read('k')?.value === 'session-value', 'session value visible before clear');
assert(backend.sessionSize === 1, 'sessionSize=1 before clear');
backend.clearSession();
assert(backend.sessionSize === 0, 'sessionSize=0 after clearSession()');
// After clearing session, read falls through to inner backend
assert(backend.read('k')?.value === 'session-value',
'after clearSession, falls through to backend (backend has the write)');
}
// ==========================================================================
// 15. Session — clearSession() restores deletions to backend state
// ==========================================================================
section('15. Session: clearSession() reveals backend after session-delete');
{
const inner = new MemoryBackend();
inner.write('k', 'backend-value', 'ag');
const backend = new ConsistentBackend(inner, 'session');
backend.delete('k');
assert(backend.read('k') === null, 'session-deleted key is invisible');
backend.clearSession();
// delete() propagates to the backend, so the entry is gone even after clearSession
assert(backend.read('k') === null,
'after clearSession, deleted key is still gone (delete was write-through)');
}
// ==========================================================================
// 16. Session — listKeys: session writes added
// ==========================================================================
section('16. Session: listKeys includes session-only writes');
{
const inner = new MemoryBackend();
inner.write('backend-key', 'b', 'ag');
const backend = new ConsistentBackend(inner, 'session');
backend.write('session-key', 's', 'ag');
const keys = backend.listKeys().sort();
assert(keys.length === 2, 'listKeys has 2 keys (backend + session)');
assert(keys.includes('backend-key'), 'listKeys includes backend key');
assert(keys.includes('session-key'), 'listKeys includes session key');
}
// ==========================================================================
// 17. Session — listKeys: session deletes removed
// ==========================================================================
section('17. Session: listKeys excludes session-deleted keys');
{
const inner = new MemoryBackend();
inner.write('a', 1, 'ag');
inner.write('b', 2, 'ag');
const backend = new ConsistentBackend(inner, 'session');
backend.delete('a');
const keys = backend.listKeys();
assert(keys.length === 1, 'listKeys has 1 key after session-delete');
assert(keys[0] === 'b', 'listKeys has b');
assert(!keys.includes('a'), 'listKeys excludes session-deleted a');
}
// ==========================================================================
// 18. Session — getSnapshot: session writes overlay backend
// ==========================================================================
section('18. Session: getSnapshot overlays session writes');
{
const inner = new MemoryBackend();
inner.write('k', 'backend-v', 'ag');
const backend = new ConsistentBackend(inner, 'session');
backend.write('k', 'session-v', 'ag'); // override via session
backend.write('new', 'only-session', 'ag'); // only in session
const snap = backend.getSnapshot();
assert(snap['k']?.value === 'session-v', 'snapshot: session value overrides backend');
assert(snap['new']?.value === 'only-session', 'snapshot: session-only key present');
assert(Object.keys(snap).length === 2, 'snapshot has 2 entries total');
}
// ==========================================================================
// 19. Session — getSnapshot: session deletes remove backend entries
// ==========================================================================
section('19. Session: getSnapshot excludes session-deleted keys');
{
const inner = new MemoryBackend();
inner.write('a', 1, 'ag');
inner.write('b', 2, 'ag');
const backend = new ConsistentBackend(inner, 'session');
backend.delete('a');
const snap = backend.getSnapshot();
assert(snap['b'] !== undefined, 'snapshot has b');
assert(snap['a'] === undefined, 'snapshot excludes session-deleted a');
assert(Object.keys(snap).length === 1, 'snapshot has 1 entry');
}
// ==========================================================================
// 20. Session — writeAsync does not call flush
// ==========================================================================
section('20. Session: writeAsync does not flush');
{
const flushable = new MockFlushableBackend();
const backend = new ConsistentBackend(flushable, 'session');
const entry = await backend.writeAsync('k', 'v', 'ag');
assert(entry.value === 'v', 'writeAsync returns correct entry');
assert(flushable.flushCallCount === 0, 'session writeAsync does NOT call flush()');
assert(backend.read('k')?.value === 'v','session: writeAsync result visible via read');
}
// ==========================================================================
// 21. Strong — writeAsync calls flush on FlushableBackend
// ==========================================================================
section('21. Strong: writeAsync flushes FlushableBackend');
{
const flushable = new MockFlushableBackend();
const backend = new ConsistentBackend(flushable, 'strong');
const entry = await backend.writeAsync('k', 'v', 'ag');
assert(entry.value === 'v', 'writeAsync returns correct entry');
assert(flushable.flushCallCount === 1, 'strong writeAsync calls flush() once');
await backend.writeAsync('k2', 'v2', 'ag');
assert(flushable.flushCallCount === 2, 'each strong writeAsync flushes once');
}
// ==========================================================================
// 22. Strong — writeAsync on non-flushable backend: no error
// ==========================================================================
section('22. Strong: writeAsync on non-flushable backend is safe');
{
const nonFlushable = makeNonFlushable();
const backend = new ConsistentBackend(nonFlushable, 'strong');
let threw = false;
try {
const entry = await backend.writeAsync('k', 'v', 'ag');
assert(entry.value === 'v', 'strong + non-flushable writeAsync returns entry');
} catch {
threw = true;
}
assert(threw === false, 'strong writeAsync does not throw for non-flushable backend');
}
// ==========================================================================
// 23. Strong — sync write() works normally (no flush)
// ==========================================================================
section('23. Strong: sync write() does not flush');
{
const flushable = new MockFlushableBackend();
const backend = new ConsistentBackend(flushable, 'strong');
backend.write('k', 'v', 'ag');
assert(flushable.flushCallCount === 0, 'strong sync write() does NOT auto-flush');
assert(backend.read('k')?.value === 'v', 'strong: read returns written value');
}
// ==========================================================================
// 24. Strong — sessionSize is always 0
// ==========================================================================
section('24. Strong: sessionSize is 0');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'strong');
backend.write('k', 'v', 'ag');
assert(backend.sessionSize === 0, 'strong sessionSize is always 0');
}
// ==========================================================================
// 25. Strong — listKeys delegates to backend
// ==========================================================================
section('25. Strong: listKeys delegates to backend');
{
const backend = new ConsistentBackend(new MemoryBackend(), 'strong');
backend.write('x', 1, 'ag');
backend.write('y', 2, 'ag');
const keys = backend.listKeys().sort();
assert(keys.length === 2, 'strong listKeys returns 2 keys');
assert(keys[0] === 'x', 'strong listKeys has x');
assert(keys[1] === 'y', 'strong listKeys has y');
}
// ==========================================================================
// 26. TTL expiry — eventual
// ==========================================================================
section('26. TTL expiry: eventual');
{
// MemoryBackend handles TTL natively — verify delegation works via ConsistentBackend
const inner2 = new MemoryBackend();
const b2 = new ConsistentBackend(inner2, 'eventual');
b2.write('k', 'v', 'ag', 3600);
assert(b2.read('k')?.value === 'v', 'eventual: unexpired entry readable');
}
// ==========================================================================
// 27. TTL expiry — session: expired session entry falls through to backend
// ==========================================================================
section('27. TTL expiry: session cache respects TTL');
{
const inner = new MemoryBackend();
inner.write('k', 'backend-value', 'ag');
const backend = new ConsistentBackend(inner, 'session');
// Write with TTL to backend via wrapper (cached in session too)
backend.write('k', 'session-value', 'ag', 1);
// Manually expire the session cache entry by back-dating
// Since we can't easily backdating via the public API, we test via clearSession instead
// and confirm behavior when session entry is absent
backend.clearSession();
// write() is write-through: backend now has 'session-value', not 'backend-value'
assert(backend.read('k')?.value === 'session-value',
'after session clear, reads fall back to backend value');
}
// ==========================================================================
// 28. Multiple independent ConsistentBackend instances share no state
// ==========================================================================
section('28. Multiple ConsistentBackend instances are independent');
{
const inner = new MemoryBackend();
const b1 = new ConsistentBackend(inner, 'session');
const b2 = new ConsistentBackend(inner, 'session');
b1.write('k', 'from-b1', 'ag');
// b2 shares the same inner backend but has its own session cache
// b2's session cache is empty, so it reads from inner (which has 'from-b1' via b1 write)
assert(b1.read('k')?.value === 'from-b1', 'b1 sees its own session write');
assert(b2.read('k')?.value === 'from-b1', 'b2 reads from shared backend (different session cache)');
assert(b2.sessionSize === 0, 'b2 session cache is empty');
b2.write('k', 'from-b2', 'ag');
assert(b2.read('k')?.value === 'from-b2', 'b2 sees its own session write');
assert(b1.read('k')?.value === 'from-b1', 'b1 session cache still has from-b1');
}
// ==========================================================================
// 29. clearSession on eventual/strong is a no-op (no error)
// ==========================================================================
section('29. clearSession() on eventual/strong is safe no-op');
{
const eventual = new ConsistentBackend(new MemoryBackend(), 'eventual');
const strong = new ConsistentBackend(new MemoryBackend(), 'strong');
let threw = false;
try {
eventual.clearSession();
strong.clearSession();
} catch {
threw = true;
}
assert(threw === false, 'clearSession() on eventual/strong does not throw');
assert(eventual.sessionSize === 0, 'eventual sessionSize still 0 after clearSession');
assert(strong.sessionSize === 0, 'strong sessionSize still 0 after clearSession');
}
// ==========================================================================
// 30. NamedBlackboardOptions.consistency integration via index.ts
// ==========================================================================
section('30. NamedBlackboardOptions has consistency field');
{
// eslint-disable-next-line @typescript-eslint/no-require-imports
const idx = require('./index');
// Verify ConsistentBackend and helpers are exported from index
assert(typeof idx.ConsistentBackend === 'function', 'index.ts exports ConsistentBackend');
assert(typeof idx.isFlushable === 'function', 'index.ts exports isFlushable');
}
// ==========================================================================
// 31. Export verification — lib/consistency.ts
// ==========================================================================
section('31. Export verification');
{
// eslint-disable-next-line @typescript-eslint/no-require-imports
const lib = require('./lib/consistency');
assert(typeof lib.ConsistentBackend === 'function', 'lib exports ConsistentBackend');
assert(typeof lib.isFlushable === 'function', 'lib exports isFlushable');
// Verify the three consistency level values are valid strings
const levels: ConsistencyLevel[] = ['eventual', 'session', 'strong'];
assert(levels.length === 3, 'three consistency levels defined');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const idx = require('./index');
assert(typeof idx.ConsistentBackend === 'function', 'index.ts re-exports ConsistentBackend');
assert(typeof idx.isFlushable === 'function', 'index.ts re-exports isFlushable');
}
// ==========================================================================
// 32. getBlackboard with consistency option
// ==========================================================================
section('32. getBlackboard() wraps backend with ConsistentBackend when consistency supplied');
{
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { SwarmOrchestrator, ConsistentBackend: CB, MemoryBackend: MB } = require('./index');
const orchestrator = new SwarmOrchestrator('./data/test-phase5e-tmp');
const inner = new MB();
const board = orchestrator.getBlackboard('test-consistency-e', {
backend: new CB(inner, 'session'),
});
board.registerAgent('ag', 'tok', ['*']);
board.write('hello', 'world', 'ag', undefined, 'tok');
const entry = board.read('hello');
assert(entry?.value === 'world', 'getBlackboard with ConsistentBackend works end-to-end');
// Clean up
orchestrator.destroyBlackboard('test-consistency-e');
}
// ==========================================================================
// SUMMARY
// ==========================================================================
console.log('\n' + '='.repeat(60));
if (failed === 0) {
console.log(`ALL ${passed} PHASE 5e TESTS PASSED`);
} else {
console.log(`${passed} passed, ${failed} FAILED`);
console.log('\nFailed tests:');
failures.forEach(f => console.log(` - ${f}`));
process.exit(1);
}
}
main().catch(err => {
console.error('Unexpected error:', err);
process.exit(1);
});