@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.
- package/dist/action-policy.d.ts +28 -1
- package/dist/action-policy.js +167 -7
- package/dist/action-policy.test.js +104 -0
- package/dist/adversarial.test.js +4 -2
- package/dist/architecture-boundary.test.js +4 -12
- package/dist/capability.d.ts +11 -2
- package/dist/capability.js +8 -1
- package/dist/capability.test.js +43 -1
- package/dist/context-retriever.d.ts +8 -5
- package/dist/context-retriever.js +44 -15
- package/dist/database.d.ts +1 -0
- package/dist/database.js +17 -0
- package/dist/index.d.ts +46 -7
- package/dist/index.js +2 -0
- package/dist/prompt-compiler.d.ts +1 -0
- package/dist/prompt-compiler.js +7 -1
- package/dist/runtime.d.ts +12 -5
- package/dist/runtime.js +26 -8
- package/dist/schema-validator.test.js +2 -2
- package/dist/siduri-db.d.ts +138 -0
- package/dist/siduri-db.js +687 -0
- package/dist/siduri-db.test.d.ts +1 -0
- package/dist/siduri-db.test.js +863 -0
- package/dist/sqlite-action-store.d.ts +3 -2
- package/dist/sqlite-action-store.js +27 -4
- package/dist/sqlite-action-store.test.js +1 -0
- package/package.json +6 -6
|
@@ -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
|
+
});
|