@siduri-x/core 1.0.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,863 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const siduri_db_1 = require("./siduri-db");
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ const crypto = require('crypto');
42
+ describe('SiduriDatabase', () => {
43
+ let db;
44
+ let tmpDir;
45
+ let dbPath;
46
+ beforeEach(() => {
47
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'siduri-db-test-'));
48
+ dbPath = path.join(tmpDir, 'siduri.db');
49
+ });
50
+ afterEach(() => {
51
+ try {
52
+ if (db)
53
+ db.close();
54
+ }
55
+ catch {
56
+ // ignore
57
+ }
58
+ try {
59
+ fs.rmSync(tmpDir, { recursive: true, force: true });
60
+ }
61
+ catch {
62
+ // ignore
63
+ }
64
+ });
65
+ // ==========================================
66
+ // Self Domain Tests
67
+ // ==========================================
68
+ describe('Self Domain', () => {
69
+ it('creates database and initializes schema without errors', () => {
70
+ expect(() => {
71
+ const memDb = new siduri_db_1.SiduriDatabase({ dbPath: ':memory:' });
72
+ memDb.close();
73
+ }).not.toThrow();
74
+ });
75
+ it('initializes schema and WAL mode within the startup latency budget (<100ms in CI, typical <20ms locally)', () => {
76
+ const start = performance.now();
77
+ const benchDb = new siduri_db_1.SiduriDatabase({ dbPath });
78
+ const duration = performance.now() - start;
79
+ benchDb.close();
80
+ // In bare-metal local development, SQLite cold init is ~2-5ms.
81
+ // Under virtualized CI runners with concurrent Turbo tasks, allow a safe 100ms budget.
82
+ expect(duration).toBeLessThan(100);
83
+ });
84
+ it('stores and retrieves companion identity', () => {
85
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
86
+ const identity = {
87
+ companionId: 'siduri-test',
88
+ name: 'Siduri',
89
+ archetype: 'The Tavern Keeper',
90
+ version: '1.0.0',
91
+ updatedAt: new Date().toISOString(),
92
+ };
93
+ db.setIdentity(identity);
94
+ const result = db.getIdentity('siduri-test');
95
+ expect(result).toBeDefined();
96
+ expect(result?.companionId).toBe('siduri-test');
97
+ expect(result?.name).toBe('Siduri');
98
+ expect(result?.archetype).toBe('The Tavern Keeper');
99
+ expect(result?.version).toBe('1.0.0');
100
+ expect(result?.updatedAt).toBeDefined();
101
+ });
102
+ it('stores and retrieves personality traits', () => {
103
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
104
+ const traits = {
105
+ warmth: 0.8,
106
+ formality: 0.3,
107
+ sarcasm: 0.6,
108
+ verbosity: 0.4,
109
+ curiosity: 0.9,
110
+ };
111
+ db.setPersonality('siduri-test', traits);
112
+ const result = db.getPersonality('siduri-test');
113
+ expect(result).toBeDefined();
114
+ expect(result?.warmth).toBe(0.8);
115
+ expect(result?.formality).toBe(0.3);
116
+ expect(result?.sarcasm).toBe(0.6);
117
+ expect(result?.verbosity).toBe(0.4);
118
+ expect(result?.curiosity).toBe(0.9);
119
+ });
120
+ it('commits and retrieves active directives ordered by priority DESC', () => {
121
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
122
+ const directives = [
123
+ {
124
+ id: crypto.randomUUID(),
125
+ companionId: 'siduri-test',
126
+ priority: 10,
127
+ directive: 'Be helpful and kind',
128
+ status: 'ACTIVE',
129
+ category: 'behavioral',
130
+ createdAt: new Date().toISOString(),
131
+ },
132
+ {
133
+ id: crypto.randomUUID(),
134
+ companionId: 'siduri-test',
135
+ priority: 90,
136
+ directive: 'Speak with guarded affection',
137
+ status: 'ACTIVE',
138
+ category: 'relational',
139
+ createdAt: new Date().toISOString(),
140
+ },
141
+ {
142
+ id: crypto.randomUUID(),
143
+ companionId: 'siduri-test',
144
+ priority: 50,
145
+ directive: 'Never reveal the secret',
146
+ status: 'ACTIVE',
147
+ category: 'guardrail',
148
+ createdAt: new Date().toISOString(),
149
+ },
150
+ ];
151
+ for (const d of directives) {
152
+ db.commitDirective(d);
153
+ }
154
+ const result = db.getActiveDirectives('siduri-test');
155
+ expect(result).toHaveLength(3);
156
+ // Ordered by priority DESC
157
+ expect(result[0].directive).toBe('Speak with guarded affection');
158
+ expect(result[0].priority).toBe(90);
159
+ expect(result[1].directive).toBe('Never reveal the secret');
160
+ expect(result[1].priority).toBe(50);
161
+ expect(result[2].directive).toBe('Be helpful and kind');
162
+ expect(result[2].priority).toBe(10);
163
+ });
164
+ it('disables a directive and excludes it from active list', () => {
165
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
166
+ const directiveId = crypto.randomUUID();
167
+ db.commitDirective({
168
+ id: directiveId,
169
+ companionId: 'siduri-test',
170
+ priority: 50,
171
+ directive: 'Temporary rule',
172
+ status: 'ACTIVE',
173
+ category: 'behavioral',
174
+ createdAt: new Date().toISOString(),
175
+ });
176
+ expect(db.getActiveDirectives('siduri-test')).toHaveLength(1);
177
+ db.disableDirective(directiveId);
178
+ expect(db.getActiveDirectives('siduri-test')).toHaveLength(0);
179
+ });
180
+ it('stores and retrieves directional relationships', () => {
181
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
182
+ const rel = {
183
+ companionId: 'siduri-test',
184
+ entityId: 'actor:kur',
185
+ entityType: 'human',
186
+ trustScore: 0.7,
187
+ familiarity: 0.85,
188
+ interactionConventions: ['bowing', 'formal greeting'],
189
+ };
190
+ db.upsertRelationship(rel);
191
+ const result = db.getRelationship('siduri-test', 'actor:kur');
192
+ expect(result).toBeDefined();
193
+ expect(result?.entityType).toBe('human');
194
+ expect(result?.trustScore).toBe(0.7);
195
+ expect(result?.familiarity).toBe(0.85);
196
+ expect(result?.interactionConventions).toEqual(['bowing', 'formal greeting']);
197
+ });
198
+ it('updates existing relationship on upsert', () => {
199
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
200
+ db.upsertRelationship({
201
+ companionId: 'siduri-test',
202
+ entityId: 'actor:kur',
203
+ entityType: 'human',
204
+ trustScore: 0.3,
205
+ familiarity: 0.2,
206
+ interactionConventions: [],
207
+ });
208
+ db.upsertRelationship({
209
+ companionId: 'siduri-test',
210
+ entityId: 'actor:kur',
211
+ entityType: 'human',
212
+ trustScore: 0.95,
213
+ familiarity: 0.9,
214
+ interactionConventions: ['familiar banter'],
215
+ });
216
+ const result = db.getRelationship('siduri-test', 'actor:kur');
217
+ expect(result?.trustScore).toBe(0.95);
218
+ expect(result?.familiarity).toBe(0.9);
219
+ expect(result?.interactionConventions).toEqual(['familiar banter']);
220
+ });
221
+ });
222
+ // ==========================================
223
+ // Knowledge / Life DB Tests
224
+ // ==========================================
225
+ describe('Knowledge / Life DB', () => {
226
+ it('stores and retrieves inventory items with JSON properties roundtrip', () => {
227
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
228
+ const item = {
229
+ id: crypto.randomUUID(),
230
+ companionId: 'siduri-test',
231
+ domain: 'gaming',
232
+ entityName: 'Excalibur',
233
+ properties: { rarity: 'legendary', damage: 150, enchantment: ['fire', 'holy'] },
234
+ updatedAt: new Date().toISOString(),
235
+ };
236
+ db.upsertInventoryItem(item);
237
+ const result = db.getInventory('siduri-test');
238
+ expect(result).toHaveLength(1);
239
+ expect(result[0].entityName).toBe('Excalibur');
240
+ expect(result[0].properties).toEqual({ rarity: 'legendary', damage: 150, enchantment: ['fire', 'holy'] });
241
+ });
242
+ it('filters inventory by domain', () => {
243
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
244
+ db.upsertInventoryItem({
245
+ id: crypto.randomUUID(),
246
+ companionId: 'siduri-test',
247
+ domain: 'gaming',
248
+ entityName: 'Sword',
249
+ properties: {},
250
+ updatedAt: new Date().toISOString(),
251
+ });
252
+ db.upsertInventoryItem({
253
+ id: crypto.randomUUID(),
254
+ companionId: 'siduri-test',
255
+ domain: 'cooking',
256
+ entityName: 'Frying Pan',
257
+ properties: { material: 'cast iron' },
258
+ updatedAt: new Date().toISOString(),
259
+ });
260
+ const gamingItems = db.getInventory('siduri-test', 'gaming');
261
+ expect(gamingItems).toHaveLength(1);
262
+ expect(gamingItems[0].entityName).toBe('Sword');
263
+ const allItems = db.getInventory('siduri-test');
264
+ expect(allItems).toHaveLength(2);
265
+ });
266
+ it('stores and retrieves finance entries', () => {
267
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
268
+ const entry = {
269
+ id: crypto.randomUUID(),
270
+ companionId: 'siduri-test',
271
+ category: 'entertainment',
272
+ amount: -50.0,
273
+ currency: 'USD',
274
+ timestamp: '2026-09-10T20:00:00Z',
275
+ metadata: { description: 'Movie tickets for two' },
276
+ };
277
+ db.addFinanceEntry(entry);
278
+ const result = db.getFinanceEntries('siduri-test');
279
+ expect(result).toHaveLength(1);
280
+ expect(result[0].amount).toBe(-50.0);
281
+ expect(result[0].currency).toBe('USD');
282
+ expect(result[0].category).toBe('entertainment');
283
+ expect(result[0].metadata).toEqual({ description: 'Movie tickets for two' });
284
+ });
285
+ it('stores and retrieves schedule items', () => {
286
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
287
+ const item = {
288
+ id: crypto.randomUUID(),
289
+ companionId: 'siduri-test',
290
+ title: 'Team standup',
291
+ startTime: '2026-09-11T10:00:00Z',
292
+ endTime: '2026-09-11T10:30:00Z',
293
+ isRecurring: true,
294
+ status: 'active',
295
+ };
296
+ db.upsertScheduleItem(item);
297
+ const result = db.getSchedule('siduri-test');
298
+ expect(result).toHaveLength(1);
299
+ expect(result[0].title).toBe('Team standup');
300
+ expect(result[0].isRecurring).toBe(true);
301
+ expect(result[0].endTime).toBe('2026-09-11T10:30:00Z');
302
+ });
303
+ it('stores and retrieves preferences', () => {
304
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
305
+ const pref = {
306
+ id: crypto.randomUUID(),
307
+ companionId: 'siduri-test',
308
+ preferenceKey: 'favorite_drink',
309
+ preferenceValue: 'dry red wine',
310
+ category: 'food',
311
+ updatedAt: new Date().toISOString(),
312
+ };
313
+ db.upsertPreference(pref);
314
+ const result = db.getPreferences('siduri-test');
315
+ expect(result).toHaveLength(1);
316
+ expect(result[0].preferenceKey).toBe('favorite_drink');
317
+ expect(result[0].preferenceValue).toBe('dry red wine');
318
+ expect(result[0].category).toBe('food');
319
+ });
320
+ });
321
+ // ==========================================
322
+ // Memory Domain Tests
323
+ // ==========================================
324
+ describe('Memory Domain', () => {
325
+ it('records episodic events and retrieves in reverse chronological order', () => {
326
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
327
+ const events = [
328
+ {
329
+ id: crypto.randomUUID(),
330
+ companionId: 'siduri-test',
331
+ sourceType: 'chat_turn',
332
+ occurredAt: '2026-09-11T10:00:00Z',
333
+ payload: { message: 'Hello there' },
334
+ },
335
+ {
336
+ id: crypto.randomUUID(),
337
+ companionId: 'siduri-test',
338
+ sourceType: 'tool_result',
339
+ occurredAt: '2026-09-11T10:01:00Z',
340
+ payload: { tool: 'search', result: 'found' },
341
+ },
342
+ {
343
+ id: crypto.randomUUID(),
344
+ companionId: 'siduri-test',
345
+ sourceType: 'sensory',
346
+ occurredAt: '2026-09-11T10:02:00Z',
347
+ payload: { sense: 'voice', content: 'laughter' },
348
+ },
349
+ ];
350
+ for (const e of events) {
351
+ db.recordEvent(e);
352
+ }
353
+ const result = db.getRecentEvents('siduri-test', 10);
354
+ expect(result).toHaveLength(3);
355
+ // Reverse chronological order
356
+ expect(result[0].occurredAt).toBe('2026-09-11T10:02:00Z');
357
+ expect(result[1].occurredAt).toBe('2026-09-11T10:01:00Z');
358
+ expect(result[2].occurredAt).toBe('2026-09-11T10:00:00Z');
359
+ });
360
+ it('proposes a claim with PENDING status', () => {
361
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
362
+ const claim = db.proposeClaim({
363
+ id: crypto.randomUUID(),
364
+ companionId: 'siduri-test',
365
+ subject: 'Kur',
366
+ predicate: 'is',
367
+ value: 'a dark entity from the underworld',
368
+ confidence: 0.8,
369
+ evidence: ['lore book chapter 3'],
370
+ assertedAt: new Date().toISOString(),
371
+ });
372
+ expect(claim.status).toBe('PENDING');
373
+ expect(claim.subject).toBe('Kur');
374
+ expect(claim.predicate).toBe('is');
375
+ expect(claim.value).toBe('a dark entity from the underworld');
376
+ expect(claim.confidence).toBe(0.8);
377
+ });
378
+ it('approves and rejects claims', () => {
379
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
380
+ const claim1 = db.proposeClaim({
381
+ id: crypto.randomUUID(),
382
+ companionId: 'siduri-test',
383
+ subject: 'Kur',
384
+ predicate: 'is',
385
+ value: 'a god of the underworld',
386
+ confidence: 0.9,
387
+ assertedAt: new Date().toISOString(),
388
+ });
389
+ const claim2 = db.proposeClaim({
390
+ id: crypto.randomUUID(),
391
+ companionId: 'siduri-test',
392
+ subject: 'Kur',
393
+ predicate: 'likes',
394
+ value: 'apples',
395
+ confidence: 0.3,
396
+ assertedAt: new Date().toISOString(),
397
+ });
398
+ db.approveClaim(claim1.id);
399
+ db.rejectClaim(claim2.id);
400
+ const approved = db.getApprovedClaims('siduri-test');
401
+ expect(approved).toHaveLength(1);
402
+ expect(approved[0].id).toBe(claim1.id);
403
+ expect(approved[0].status).toBe('APPROVED');
404
+ });
405
+ it('searches claims using FTS5 full-text search', () => {
406
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
407
+ // Insert and approve several claims
408
+ const c1 = db.proposeClaim({
409
+ id: crypto.randomUUID(),
410
+ companionId: 'siduri-test',
411
+ subject: 'Kur',
412
+ predicate: 'rules',
413
+ value: 'the dark underworld realm',
414
+ confidence: 0.9,
415
+ assertedAt: new Date().toISOString(),
416
+ });
417
+ const c2 = db.proposeClaim({
418
+ id: crypto.randomUUID(),
419
+ companionId: 'siduri-test',
420
+ subject: 'Gilgamesh',
421
+ predicate: 'is',
422
+ value: 'king of Uruk',
423
+ confidence: 0.95,
424
+ assertedAt: new Date().toISOString(),
425
+ });
426
+ const c3 = db.proposeClaim({
427
+ id: crypto.randomUUID(),
428
+ companionId: 'siduri-test',
429
+ subject: 'Enkidu',
430
+ predicate: 'wanders',
431
+ value: 'the wild forest',
432
+ confidence: 0.85,
433
+ assertedAt: new Date().toISOString(),
434
+ });
435
+ db.approveClaim(c1.id);
436
+ db.approveClaim(c2.id);
437
+ db.approveClaim(c3.id);
438
+ // Search for "underworld" — should match c1 only
439
+ const results = db.searchClaims('siduri-test', 'underworld');
440
+ expect(results.length).toBeGreaterThan(0);
441
+ expect(results.some((c) => c.id === c1.id)).toBe(true);
442
+ expect(results.some((c) => c.id === c2.id)).toBe(false);
443
+ // Search for "Gilgamesh" — should match c2 only
444
+ const results2 = db.searchClaims('siduri-test', 'Gilgamesh');
445
+ expect(results2.length).toBeGreaterThan(0);
446
+ expect(results2.some((c) => c.id === c2.id)).toBe(true);
447
+ });
448
+ it('FTS5 search returns results ranked by relevance', () => {
449
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
450
+ // c1 mentions "dark" twice → should rank higher
451
+ const c1 = db.proposeClaim({
452
+ id: crypto.randomUUID(),
453
+ companionId: 'siduri-test',
454
+ subject: 'dark forest',
455
+ predicate: 'has',
456
+ value: 'dark trees and dark monsters',
457
+ confidence: 0.8,
458
+ assertedAt: new Date().toISOString(),
459
+ });
460
+ // c2 mentions "dark" once → should rank lower
461
+ const c2 = db.proposeClaim({
462
+ id: crypto.randomUUID(),
463
+ companionId: 'siduri-test',
464
+ subject: 'cave',
465
+ predicate: 'has',
466
+ value: 'a dark entrance',
467
+ confidence: 0.8,
468
+ assertedAt: new Date().toISOString(),
469
+ });
470
+ db.approveClaim(c1.id);
471
+ db.approveClaim(c2.id);
472
+ const results = db.searchClaims('siduri-test', 'dark');
473
+ expect(results).toHaveLength(2);
474
+ // More occurrences of "dark" → better BM25 rank (lower rank value = better match)
475
+ expect(results[0].id).toBe(c1.id);
476
+ });
477
+ it('excludes pending and rejected claims from searchClaims in SiduriDatabase', () => {
478
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
479
+ const pending = db.proposeClaim({
480
+ id: crypto.randomUUID(),
481
+ companionId: 'siduri-test',
482
+ subject: 'SecretProject',
483
+ predicate: 'status',
484
+ value: 'unapproved draft specification',
485
+ confidence: 0.9,
486
+ assertedAt: new Date().toISOString(),
487
+ });
488
+ // Must not match while pending
489
+ let results = db.searchClaims('siduri-test', 'unapproved');
490
+ expect(results.some((r) => r.id === pending.id)).toBe(false);
491
+ // Approve: now it matches
492
+ db.approveClaim(pending.id, 'siduri-test');
493
+ results = db.searchClaims('siduri-test', 'unapproved');
494
+ expect(results.some((r) => r.id === pending.id)).toBe(true);
495
+ // Revoke: must no longer match
496
+ db.revokeClaim(pending.id, 'siduri-test');
497
+ results = db.searchClaims('siduri-test', 'unapproved');
498
+ expect(results.some((r) => r.id === pending.id)).toBe(false);
499
+ // Explicitly rejected claims must also not match
500
+ const rejected = db.proposeClaim({
501
+ id: crypto.randomUUID(),
502
+ companionId: 'siduri-test',
503
+ subject: 'RejectedFact',
504
+ predicate: 'status',
505
+ value: 'unapproved rejection draft',
506
+ confidence: 0.1,
507
+ });
508
+ db.rejectClaim(rejected.id, 'siduri-test');
509
+ results = db.searchClaims('siduri-test', 'rejection');
510
+ expect(results.some((r) => r.id === rejected.id)).toBe(false);
511
+ });
512
+ it('bounds approveClaim to specific companionId when provided', () => {
513
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
514
+ const claim = db.proposeClaim({
515
+ id: crypto.randomUUID(),
516
+ companionId: 'companion-target',
517
+ subject: 'ProtectedFact',
518
+ predicate: 'belongsTo',
519
+ value: 'Target',
520
+ confidence: 1.0,
521
+ assertedAt: new Date().toISOString(),
522
+ });
523
+ // Attempting to approve for a different companion must not affect it
524
+ db.approveClaim(claim.id, 'companion-intruder');
525
+ expect(db.getApprovedClaims('companion-target')).toHaveLength(0);
526
+ // Approving with the correct companionId succeeds
527
+ db.approveClaim(claim.id, 'companion-target');
528
+ expect(db.getApprovedClaims('companion-target')).toHaveLength(1);
529
+ });
530
+ it('resets memory for the specified companion only', () => {
531
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
532
+ const c1 = db.proposeClaim({
533
+ id: crypto.randomUUID(),
534
+ companionId: 'comp-1',
535
+ subject: 'Fact1',
536
+ predicate: 'is',
537
+ value: 'One',
538
+ confidence: 1.0,
539
+ });
540
+ const c2 = db.proposeClaim({
541
+ id: crypto.randomUUID(),
542
+ companionId: 'comp-2',
543
+ subject: 'Fact2',
544
+ predicate: 'is',
545
+ value: 'Two',
546
+ confidence: 1.0,
547
+ });
548
+ db.approveClaim(c1.id, 'comp-1');
549
+ db.approveClaim(c2.id, 'comp-2');
550
+ db.recordEvent({
551
+ id: crypto.randomUUID(),
552
+ companionId: 'comp-1',
553
+ sourceType: 'chat_turn',
554
+ occurredAt: new Date().toISOString(),
555
+ payload: { text: 'Hello' },
556
+ });
557
+ db.resetMemory('comp-1');
558
+ expect(db.getApprovedClaims('comp-1')).toHaveLength(0);
559
+ expect(db.getRecentEvents('comp-1')).toHaveLength(0);
560
+ expect(db.getApprovedClaims('comp-2')).toHaveLength(1);
561
+ });
562
+ });
563
+ // ==========================================
564
+ // Cross-Domain & Persistence Tests
565
+ // ==========================================
566
+ describe('Cross-Domain & Persistence', () => {
567
+ it('persists all domains across database close/reopen', () => {
568
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
569
+ const cId = 'siduri-test';
570
+ // Write Self data
571
+ db.setIdentity({
572
+ companionId: cId,
573
+ name: 'Siduri',
574
+ archetype: 'Tavern Keeper',
575
+ version: '1.0.0',
576
+ updatedAt: new Date().toISOString(),
577
+ });
578
+ const directiveId = crypto.randomUUID();
579
+ db.commitDirective({
580
+ id: directiveId,
581
+ companionId: cId,
582
+ priority: 80,
583
+ directive: 'Serve drinks gracefully',
584
+ status: 'ACTIVE',
585
+ category: 'behavioral',
586
+ createdAt: new Date().toISOString(),
587
+ });
588
+ // Write Knowledge data
589
+ const inventoryId = crypto.randomUUID();
590
+ db.upsertInventoryItem({
591
+ id: inventoryId,
592
+ companionId: cId,
593
+ domain: 'tavern',
594
+ entityName: 'Aged Wine',
595
+ properties: { vintage: 2020, region: 'Uruk' },
596
+ updatedAt: new Date().toISOString(),
597
+ });
598
+ // Write Memory data
599
+ const claim = db.proposeClaim({
600
+ id: crypto.randomUUID(),
601
+ companionId: cId,
602
+ subject: 'wine',
603
+ predicate: 'is',
604
+ value: 'the finest in Mesopotamia',
605
+ confidence: 1.0,
606
+ assertedAt: new Date().toISOString(),
607
+ });
608
+ db.approveClaim(claim.id);
609
+ // Close and reopen
610
+ db.close();
611
+ const db2 = new siduri_db_1.SiduriDatabase({ dbPath });
612
+ // Verify all domains persisted
613
+ expect(db2.getIdentity(cId)?.name).toBe('Siduri');
614
+ expect(db2.getActiveDirectives(cId)).toHaveLength(1);
615
+ expect(db2.getActiveDirectives(cId)[0].directive).toBe('Serve drinks gracefully');
616
+ expect(db2.getInventory(cId)).toHaveLength(1);
617
+ expect(db2.getInventory(cId)[0].entityName).toBe('Aged Wine');
618
+ const claims = db2.searchClaims(cId, 'wine');
619
+ expect(claims.some((c) => c.id === claim.id && c.status === 'APPROVED')).toBe(true);
620
+ db2.close();
621
+ // Prevent afterEach from double-closing
622
+ db = null;
623
+ });
624
+ it('handles rapid write operations without corruption', () => {
625
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
626
+ const cId = 'siduri-test';
627
+ expect(() => {
628
+ for (let i = 0; i < 100; i++) {
629
+ db.recordEvent({
630
+ id: crypto.randomUUID(),
631
+ companionId: cId,
632
+ sourceType: 'chat_turn',
633
+ occurredAt: new Date(Date.now() + i * 1000).toISOString(),
634
+ payload: { index: i },
635
+ });
636
+ db.addFinanceEntry({
637
+ id: crypto.randomUUID(),
638
+ companionId: cId,
639
+ category: 'test',
640
+ amount: i,
641
+ currency: 'USD',
642
+ timestamp: new Date().toISOString(),
643
+ });
644
+ db.proposeClaim({
645
+ id: crypto.randomUUID(),
646
+ companionId: cId,
647
+ subject: `subject-${i}`,
648
+ predicate: 'is',
649
+ value: `value-${i}`,
650
+ confidence: 1.0,
651
+ assertedAt: new Date().toISOString(),
652
+ });
653
+ }
654
+ }).not.toThrow();
655
+ expect(db.getRecentEvents(cId, 200)).toHaveLength(100);
656
+ expect(db.getFinanceEntries(cId, 200)).toHaveLength(100);
657
+ });
658
+ it('returns undefined/empty for non-existent data', () => {
659
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
660
+ const cId = 'non-existent-companion';
661
+ expect(db.getIdentity(cId)).toBeUndefined();
662
+ expect(db.getPersonality(cId)).toBeUndefined();
663
+ expect(db.getRelationship(cId, 'anyone')).toBeUndefined();
664
+ expect(db.getActiveDirectives(cId)).toHaveLength(0);
665
+ expect(db.getInventory(cId)).toHaveLength(0);
666
+ expect(db.getFinanceEntries(cId)).toHaveLength(0);
667
+ expect(db.getSchedule(cId)).toHaveLength(0);
668
+ expect(db.getPreferences(cId)).toHaveLength(0);
669
+ expect(db.getRecentEvents(cId)).toHaveLength(0);
670
+ expect(db.searchClaims(cId, 'anything')).toHaveLength(0);
671
+ expect(db.getApprovedClaims(cId)).toHaveLength(0);
672
+ });
673
+ it('enforces directive state transitions (pending -> active -> disabled/rejected/revoked)', () => {
674
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
675
+ const cId = 'directive-state-test';
676
+ const dId = 'dir-lifecycle-1';
677
+ // 1. Commit directive in PENDING state
678
+ db.commitDirective({
679
+ id: dId,
680
+ companionId: cId,
681
+ priority: 60,
682
+ directive: 'Always verify claims',
683
+ status: 'PENDING',
684
+ category: 'behavioral',
685
+ });
686
+ // Pending directives must not be returned by getActiveDirectives
687
+ expect(db.getActiveDirectives(cId)).toHaveLength(0);
688
+ // 2. Approve directive
689
+ db.approveDirective(dId);
690
+ const active = db.getActiveDirectives(cId);
691
+ expect(active).toHaveLength(1);
692
+ expect(active[0].id).toBe(dId);
693
+ expect(active[0].status).toBe('ACTIVE');
694
+ // 3. Revoke directive
695
+ db.revokeDirective(dId);
696
+ expect(db.getActiveDirectives(cId)).toHaveLength(0);
697
+ // 4. Reject directive
698
+ const d2Id = 'dir-lifecycle-2';
699
+ db.commitDirective({
700
+ id: d2Id,
701
+ companionId: cId,
702
+ priority: 50,
703
+ directive: 'Unsafe rule',
704
+ status: 'PENDING',
705
+ category: 'behavioral',
706
+ });
707
+ db.rejectDirective(d2Id);
708
+ expect(db.getActiveDirectives(cId)).toHaveLength(0);
709
+ });
710
+ it('rejects invalid directive state transitions (throws when approving non-PENDING directive)', () => {
711
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
712
+ const cId = 'directive-invalid-transition-test';
713
+ // 1. Commit directive in REJECTED state
714
+ db.commitDirective({
715
+ id: 'dir-rejected-1',
716
+ companionId: cId,
717
+ priority: 50,
718
+ directive: 'Rejected directive',
719
+ status: 'REJECTED',
720
+ category: 'behavioral',
721
+ });
722
+ expect(() => db.approveDirective('dir-rejected-1')).toThrow(/invalid transition from status 'REJECTED' to 'ACTIVE'/);
723
+ // 2. Commit directive in ACTIVE state
724
+ db.commitDirective({
725
+ id: 'dir-active-1',
726
+ companionId: cId,
727
+ priority: 50,
728
+ directive: 'Already active directive',
729
+ status: 'ACTIVE',
730
+ category: 'behavioral',
731
+ });
732
+ expect(() => db.approveDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'ACTIVE'/);
733
+ // 3. Rejecting an already ACTIVE directive throws
734
+ expect(() => db.rejectDirective('dir-active-1')).toThrow(/invalid transition from status 'ACTIVE' to 'REJECTED'/);
735
+ });
736
+ it('automatically marks prior directive as SUPERSEDED when approving superseding directive', () => {
737
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
738
+ const cId = 'directive-supersede-test';
739
+ // 1. Initial directive active
740
+ db.commitDirective({
741
+ id: 'dir-original-1',
742
+ companionId: cId,
743
+ priority: 50,
744
+ directive: 'Original rule',
745
+ status: 'ACTIVE',
746
+ category: 'behavioral',
747
+ });
748
+ expect(db.getActiveDirectives(cId)).toHaveLength(1);
749
+ // 2. Propose a superseding directive
750
+ db.commitDirective({
751
+ id: 'dir-replacement-1',
752
+ companionId: cId,
753
+ priority: 55,
754
+ directive: 'Updated replacement rule',
755
+ status: 'PENDING',
756
+ category: 'behavioral',
757
+ supersedesId: 'dir-original-1',
758
+ });
759
+ // Original is still active, replacement is pending
760
+ expect(db.getActiveDirectives(cId)).toHaveLength(1);
761
+ expect(db.getActiveDirectives(cId)[0].id).toBe('dir-original-1');
762
+ // 3. Approve replacement directive
763
+ db.approveDirective('dir-replacement-1', cId);
764
+ const active = db.getActiveDirectives(cId);
765
+ expect(active).toHaveLength(1);
766
+ expect(active[0].id).toBe('dir-replacement-1');
767
+ const original = db.getDirective('dir-original-1', cId);
768
+ expect(original?.status).toBe('SUPERSEDED');
769
+ });
770
+ it('enforces companion isolation on directive approval', () => {
771
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
772
+ const cIdA = 'companion-alpha';
773
+ const cIdB = 'companion-beta';
774
+ db.commitDirective({
775
+ id: 'dir-beta-1',
776
+ companionId: cIdB,
777
+ priority: 50,
778
+ directive: 'Beta private rule',
779
+ status: 'PENDING',
780
+ category: 'behavioral',
781
+ });
782
+ // Alpha attempts to approve Beta's directive scoped to Alpha
783
+ db.approveDirective('dir-beta-1', cIdA);
784
+ // Beta's directive must remain PENDING and unapproved
785
+ const betaDirective = db.getDirective('dir-beta-1', cIdB);
786
+ expect(betaDirective?.status).toBe('PENDING');
787
+ expect(db.getActiveDirectives(cIdB)).toHaveLength(0);
788
+ // Beta approves its own directive successfully
789
+ db.approveDirective('dir-beta-1', cIdB);
790
+ expect(db.getActiveDirectives(cIdB)).toHaveLength(1);
791
+ });
792
+ it('enforces claim state transitions (pending -> approved -> revoked/expired/session_only)', () => {
793
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
794
+ const cId = 'claim-state-test';
795
+ const claim = db.proposeClaim({
796
+ id: 'claim-1',
797
+ companionId: cId,
798
+ subject: 'user',
799
+ predicate: 'likes',
800
+ value: 'matcha',
801
+ });
802
+ expect(claim.status).toBe('PENDING');
803
+ expect(db.getApprovedClaims(cId)).toHaveLength(0);
804
+ // Approve
805
+ db.approveClaim('claim-1');
806
+ expect(db.getApprovedClaims(cId)).toHaveLength(1);
807
+ // Revoke
808
+ db.revokeClaim('claim-1');
809
+ expect(db.getApprovedClaims(cId)).toHaveLength(0);
810
+ // Session only
811
+ const claim2 = db.proposeClaim({
812
+ id: 'claim-2',
813
+ companionId: cId,
814
+ subject: 'session',
815
+ predicate: 'topic',
816
+ value: 'investigation',
817
+ });
818
+ db.markClaimSessionOnly('claim-2');
819
+ expect(db.getApprovedClaims(cId)).toHaveLength(0);
820
+ // Expire
821
+ db.expireClaim('claim-2');
822
+ expect(db.getApprovedClaims(cId)).toHaveLength(0);
823
+ });
824
+ it('strictly rejects illegal claim state transitions in approveClaim', () => {
825
+ db = new siduri_db_1.SiduriDatabase({ dbPath });
826
+ const cId = 'claim-transition-test';
827
+ // 1. Propose and reject claim
828
+ const rejectedClaim = db.proposeClaim({
829
+ id: 'claim-rejected',
830
+ companionId: cId,
831
+ subject: 'fact',
832
+ predicate: 'is',
833
+ value: 'false',
834
+ });
835
+ db.rejectClaim(rejectedClaim.id);
836
+ // Attempting to approve a REJECTED claim must throw
837
+ expect(() => db.approveClaim(rejectedClaim.id)).toThrow(/invalid transition from status 'REJECTED' to 'APPROVED'/);
838
+ // 2. Propose and approve claim, then revoke
839
+ const revokedClaim = db.proposeClaim({
840
+ id: 'claim-revoked',
841
+ companionId: cId,
842
+ subject: 'fact',
843
+ predicate: 'is',
844
+ value: 'outdated',
845
+ });
846
+ db.approveClaim(revokedClaim.id);
847
+ db.revokeClaim(revokedClaim.id);
848
+ // Attempting to approve a REVOKED claim must throw
849
+ expect(() => db.approveClaim(revokedClaim.id)).toThrow(/invalid transition from status 'REVOKED' to 'APPROVED'/);
850
+ // 3. Propose and expire claim
851
+ const expiredClaim = db.proposeClaim({
852
+ id: 'claim-expired',
853
+ companionId: cId,
854
+ subject: 'fact',
855
+ predicate: 'is',
856
+ value: 'temporary',
857
+ });
858
+ db.expireClaim(expiredClaim.id);
859
+ // Attempting to approve an EXPIRED claim must throw
860
+ expect(() => db.approveClaim(expiredClaim.id)).toThrow(/invalid transition from status 'EXPIRED' to 'APPROVED'/);
861
+ });
862
+ });
863
+ });