@unrdf/kgc-runtime 26.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * WorkItem System Tests - TDD Approach
3
+ * Tests async work item system with deterministic scheduling
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from 'vitest';
7
+ import { WorkItemExecutor, WORK_ITEM_STATES } from '../src/work-item.mjs';
8
+
9
+ describe('WorkItemExecutor - Async Work Item System', () => {
10
+ let executor;
11
+
12
+ beforeEach(() => {
13
+ executor = new WorkItemExecutor();
14
+ });
15
+
16
+ describe('Executor Initialization', () => {
17
+ it('should create executor instance', () => {
18
+ expect(executor).toBeDefined();
19
+ expect(typeof executor).toBe('object');
20
+ });
21
+
22
+ it('should initialize with empty work queue', () => {
23
+ const order = executor.getExecutionOrder();
24
+ expect(Array.isArray(order)).toBe(true);
25
+ expect(order.length).toBe(0);
26
+ });
27
+ });
28
+
29
+ describe('enqueueWorkItem() - Add Work to Queue', () => {
30
+ it('should enqueue work item with goal', async () => {
31
+ const workItemId = await executor.enqueueWorkItem('Process data');
32
+
33
+ expect(workItemId).toBeDefined();
34
+ expect(typeof workItemId).toBe('string');
35
+ expect(workItemId.length).toBeGreaterThan(0);
36
+ });
37
+
38
+ it('should enqueue work item with goal and bounds', async () => {
39
+ const bounds = { timeout: 5000, maxRetries: 3 };
40
+ const workItemId = await executor.enqueueWorkItem('Calculate sum', bounds);
41
+
42
+ expect(workItemId).toBeDefined();
43
+ expect(typeof workItemId).toBe('string');
44
+ });
45
+
46
+ it('should return unique IDs for each work item', async () => {
47
+ const id1 = await executor.enqueueWorkItem('Task 1');
48
+ const id2 = await executor.enqueueWorkItem('Task 2');
49
+ const id3 = await executor.enqueueWorkItem('Task 3');
50
+
51
+ expect(id1).not.toBe(id2);
52
+ expect(id2).not.toBe(id3);
53
+ expect(id1).not.toBe(id3);
54
+ });
55
+
56
+ it('should add enqueued items to execution order', async () => {
57
+ await executor.enqueueWorkItem('Task A');
58
+ await executor.enqueueWorkItem('Task B');
59
+
60
+ const order = executor.getExecutionOrder();
61
+ expect(order.length).toBe(2);
62
+ });
63
+
64
+ it('should initialize work item in queued state', async () => {
65
+ const workItemId = await executor.enqueueWorkItem('Initial task');
66
+ const workItem = await executor.pollWorkItem(workItemId);
67
+
68
+ expect(workItem.status).toBe(WORK_ITEM_STATES.QUEUED);
69
+ });
70
+
71
+ it('should record creation timestamp', async () => {
72
+ const workItemId = await executor.enqueueWorkItem('Timestamped task');
73
+ const workItem = await executor.pollWorkItem(workItemId);
74
+
75
+ expect(workItem.created_ns).toBeDefined();
76
+ expect(typeof workItem.created_ns).toBe('string');
77
+ expect(BigInt(workItem.created_ns)).toBeGreaterThan(0n);
78
+ });
79
+ });
80
+
81
+ describe('pollWorkItem() - Query Work Item Status', () => {
82
+ it('should poll work item by ID', async () => {
83
+ const workItemId = await executor.enqueueWorkItem('Pollable task');
84
+ const workItem = await executor.pollWorkItem(workItemId);
85
+
86
+ expect(workItem).toBeDefined();
87
+ expect(workItem.id).toBe(workItemId);
88
+ });
89
+
90
+ it('should return work item with all required fields', async () => {
91
+ const workItemId = await executor.enqueueWorkItem('Complete task');
92
+ const workItem = await executor.pollWorkItem(workItemId);
93
+
94
+ expect(workItem.id).toBeDefined();
95
+ expect(workItem.goal).toBe('Complete task');
96
+ expect(workItem.status).toBe(WORK_ITEM_STATES.QUEUED);
97
+ expect(workItem.receipt_log).toBeDefined();
98
+ expect(Array.isArray(workItem.receipt_log)).toBe(true);
99
+ expect(workItem.created_ns).toBeDefined();
100
+ expect(workItem.started_ns).toBeNull();
101
+ expect(workItem.finished_ns).toBeNull();
102
+ });
103
+
104
+ it('should return null for non-existent work item', async () => {
105
+ const workItem = await executor.pollWorkItem('non-existent-id');
106
+ expect(workItem).toBeNull();
107
+ });
108
+
109
+ it('should reflect updated status after state transition', async () => {
110
+ const workItemId = await executor.enqueueWorkItem('Transitioning task');
111
+
112
+ // Initial state
113
+ let workItem = await executor.pollWorkItem(workItemId);
114
+ expect(workItem.status).toBe(WORK_ITEM_STATES.QUEUED);
115
+
116
+ // Transition to running
117
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
118
+ workItem = await executor.pollWorkItem(workItemId);
119
+ expect(workItem.status).toBe(WORK_ITEM_STATES.RUNNING);
120
+ });
121
+ });
122
+
123
+ describe('State Transitions', () => {
124
+ it('should transition from queued to running', async () => {
125
+ const workItemId = await executor.enqueueWorkItem('Task to run');
126
+
127
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
128
+ const workItem = await executor.pollWorkItem(workItemId);
129
+
130
+ expect(workItem.status).toBe(WORK_ITEM_STATES.RUNNING);
131
+ expect(workItem.started_ns).toBeDefined();
132
+ expect(workItem.started_ns).not.toBeNull();
133
+ });
134
+
135
+ it('should transition from running to succeeded', async () => {
136
+ const workItemId = await executor.enqueueWorkItem('Task to succeed');
137
+
138
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
139
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.SUCCEEDED);
140
+
141
+ const workItem = await executor.pollWorkItem(workItemId);
142
+ expect(workItem.status).toBe(WORK_ITEM_STATES.SUCCEEDED);
143
+ expect(workItem.finished_ns).toBeDefined();
144
+ expect(workItem.finished_ns).not.toBeNull();
145
+ });
146
+
147
+ it('should transition from running to failed', async () => {
148
+ const workItemId = await executor.enqueueWorkItem('Task to fail');
149
+
150
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
151
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.FAILED);
152
+
153
+ const workItem = await executor.pollWorkItem(workItemId);
154
+ expect(workItem.status).toBe(WORK_ITEM_STATES.FAILED);
155
+ expect(workItem.finished_ns).toBeDefined();
156
+ });
157
+
158
+ it('should transition to denied from queued', async () => {
159
+ const workItemId = await executor.enqueueWorkItem('Task to deny');
160
+
161
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.DENIED);
162
+
163
+ const workItem = await executor.pollWorkItem(workItemId);
164
+ expect(workItem.status).toBe(WORK_ITEM_STATES.DENIED);
165
+ expect(workItem.finished_ns).toBeDefined();
166
+ });
167
+
168
+ it('should count all state transitions', async () => {
169
+ const id1 = await executor.enqueueWorkItem('Task 1');
170
+ const id2 = await executor.enqueueWorkItem('Task 2');
171
+ const id3 = await executor.enqueueWorkItem('Task 3');
172
+
173
+ // 3 enqueues = 3 transitions to QUEUED
174
+ let count = executor.getStateTransitionCount();
175
+ expect(count).toBe(3);
176
+
177
+ await executor.transitionWorkItem(id1, WORK_ITEM_STATES.RUNNING);
178
+ count = executor.getStateTransitionCount();
179
+ expect(count).toBe(4);
180
+
181
+ await executor.transitionWorkItem(id1, WORK_ITEM_STATES.SUCCEEDED);
182
+ await executor.transitionWorkItem(id2, WORK_ITEM_STATES.RUNNING);
183
+ await executor.transitionWorkItem(id3, WORK_ITEM_STATES.DENIED);
184
+
185
+ count = executor.getStateTransitionCount();
186
+ expect(count).toBe(7);
187
+ });
188
+
189
+ it('should enforce valid state transitions', async () => {
190
+ const workItemId = await executor.enqueueWorkItem('Invalid transition');
191
+
192
+ // Cannot go from QUEUED directly to SUCCEEDED (must go through RUNNING)
193
+ await expect(
194
+ executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.SUCCEEDED)
195
+ ).rejects.toThrow();
196
+ });
197
+
198
+ it('should prevent transitions from terminal states', async () => {
199
+ const workItemId = await executor.enqueueWorkItem('Terminal state');
200
+
201
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
202
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.SUCCEEDED);
203
+
204
+ // Cannot transition from SUCCEEDED (terminal state)
205
+ await expect(
206
+ executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING)
207
+ ).rejects.toThrow();
208
+ });
209
+ });
210
+
211
+ describe('finalizeWorkItem() - Complete Work', () => {
212
+ it('should finalize work item with result and receipt', async () => {
213
+ const workItemId = await executor.enqueueWorkItem('Task to finalize');
214
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
215
+
216
+ const result = { output: 'Success', value: 42 };
217
+ const receipt = { id: 'receipt-1', timestamp: Date.now() };
218
+
219
+ const finalizedItem = await executor.finalizeWorkItem(workItemId, result, receipt);
220
+
221
+ expect(finalizedItem.status).toBe(WORK_ITEM_STATES.SUCCEEDED);
222
+ expect(finalizedItem.finished_ns).toBeDefined();
223
+ expect(finalizedItem.receipt_log.length).toBeGreaterThan(0);
224
+ });
225
+
226
+ it('should append receipt to receipt log', async () => {
227
+ const workItemId = await executor.enqueueWorkItem('Receipts task');
228
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
229
+
230
+ const receipt1 = { step: 1, action: 'start' };
231
+ const receipt2 = { step: 2, action: 'process' };
232
+ const receipt3 = { step: 3, action: 'complete' };
233
+
234
+ await executor.addReceipt(workItemId, receipt1);
235
+ await executor.addReceipt(workItemId, receipt2);
236
+
237
+ const finalizedItem = await executor.finalizeWorkItem(
238
+ workItemId,
239
+ { success: true },
240
+ receipt3
241
+ );
242
+
243
+ expect(finalizedItem.receipt_log.length).toBe(3);
244
+ expect(finalizedItem.receipt_log[0]).toEqual(receipt1);
245
+ expect(finalizedItem.receipt_log[1]).toEqual(receipt2);
246
+ expect(finalizedItem.receipt_log[2]).toEqual(receipt3);
247
+ });
248
+
249
+ it('should calculate execution duration', async () => {
250
+ const workItemId = await executor.enqueueWorkItem('Timed task');
251
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
252
+
253
+ // Small delay to ensure measurable duration
254
+ await new Promise(resolve => setTimeout(resolve, 10));
255
+
256
+ const finalizedItem = await executor.finalizeWorkItem(
257
+ workItemId,
258
+ { done: true },
259
+ { final: true }
260
+ );
261
+
262
+ const started = BigInt(finalizedItem.started_ns);
263
+ const finished = BigInt(finalizedItem.finished_ns);
264
+ const duration = finished - started;
265
+
266
+ expect(duration).toBeGreaterThan(0n);
267
+ });
268
+ });
269
+
270
+ describe('Receipt Logging', () => {
271
+ it('should support append-only receipt logs', async () => {
272
+ const workItemId = await executor.enqueueWorkItem('Logged task');
273
+
274
+ await executor.addReceipt(workItemId, { event: 'created' });
275
+ await executor.addReceipt(workItemId, { event: 'validated' });
276
+ await executor.addReceipt(workItemId, { event: 'queued' });
277
+
278
+ const workItem = await executor.pollWorkItem(workItemId);
279
+ expect(workItem.receipt_log.length).toBe(3);
280
+ expect(workItem.receipt_log[0]).toEqual({ event: 'created' });
281
+ expect(workItem.receipt_log[2]).toEqual({ event: 'queued' });
282
+ });
283
+
284
+ it('should preserve receipt order', async () => {
285
+ const workItemId = await executor.enqueueWorkItem('Ordered receipts');
286
+
287
+ for (let i = 0; i < 10; i++) {
288
+ await executor.addReceipt(workItemId, { index: i, timestamp: Date.now() });
289
+ }
290
+
291
+ const workItem = await executor.pollWorkItem(workItemId);
292
+ expect(workItem.receipt_log.length).toBe(10);
293
+
294
+ for (let i = 0; i < 10; i++) {
295
+ expect(workItem.receipt_log[i].index).toBe(i);
296
+ }
297
+ });
298
+
299
+ it('should not allow modification of existing receipts', async () => {
300
+ const workItemId = await executor.enqueueWorkItem('Immutable receipts');
301
+ const receipt = { data: 'original' };
302
+
303
+ await executor.addReceipt(workItemId, receipt);
304
+
305
+ // Modify the original object
306
+ receipt.data = 'modified';
307
+
308
+ const workItem = await executor.pollWorkItem(workItemId);
309
+ // Receipt log should have original value (deep copy)
310
+ expect(workItem.receipt_log[0].data).toBe('original');
311
+ });
312
+ });
313
+
314
+ describe('getExecutionOrder() - Deterministic Scheduling', () => {
315
+ it('should return work items in FIFO order by default', async () => {
316
+ const id1 = await executor.enqueueWorkItem('First');
317
+ const id2 = await executor.enqueueWorkItem('Second');
318
+ const id3 = await executor.enqueueWorkItem('Third');
319
+
320
+ const order = executor.getExecutionOrder();
321
+ expect(order).toEqual([id1, id2, id3]);
322
+ });
323
+
324
+ it('should maintain consistent order across multiple calls', async () => {
325
+ await executor.enqueueWorkItem('A');
326
+ await executor.enqueueWorkItem('B');
327
+ await executor.enqueueWorkItem('C');
328
+
329
+ const order1 = executor.getExecutionOrder();
330
+ const order2 = executor.getExecutionOrder();
331
+ const order3 = executor.getExecutionOrder();
332
+
333
+ expect(order1).toEqual(order2);
334
+ expect(order2).toEqual(order3);
335
+ });
336
+
337
+ it('should only include pending and queued items in execution order', async () => {
338
+ const id1 = await executor.enqueueWorkItem('Pending task');
339
+ const id2 = await executor.enqueueWorkItem('Running task');
340
+ const id3 = await executor.enqueueWorkItem('Another pending');
341
+
342
+ await executor.transitionWorkItem(id2, WORK_ITEM_STATES.RUNNING);
343
+
344
+ const order = executor.getExecutionOrder();
345
+ expect(order).toContain(id1);
346
+ expect(order).not.toContain(id2); // Running items not in execution order
347
+ expect(order).toContain(id3);
348
+ });
349
+
350
+ it('should exclude completed items from execution order', async () => {
351
+ const id1 = await executor.enqueueWorkItem('To complete');
352
+ const id2 = await executor.enqueueWorkItem('To keep pending');
353
+
354
+ await executor.transitionWorkItem(id1, WORK_ITEM_STATES.RUNNING);
355
+ await executor.transitionWorkItem(id1, WORK_ITEM_STATES.SUCCEEDED);
356
+
357
+ const order = executor.getExecutionOrder();
358
+ expect(order).not.toContain(id1);
359
+ expect(order).toContain(id2);
360
+ });
361
+
362
+ it('should support priority-based ordering', async () => {
363
+ const id1 = await executor.enqueueWorkItem('Low priority', { priority: 1 });
364
+ const id2 = await executor.enqueueWorkItem('High priority', { priority: 10 });
365
+ const id3 = await executor.enqueueWorkItem('Medium priority', { priority: 5 });
366
+
367
+ const order = executor.getExecutionOrder();
368
+ // Higher priority first
369
+ expect(order[0]).toBe(id2);
370
+ expect(order[1]).toBe(id3);
371
+ expect(order[2]).toBe(id1);
372
+ });
373
+
374
+ it('should be deterministic with same input', async () => {
375
+ // Create multiple executors with same sequence
376
+ const exec1 = new WorkItemExecutor();
377
+ const exec2 = new WorkItemExecutor();
378
+
379
+ const tasks = ['Task A', 'Task B', 'Task C'];
380
+
381
+ for (const task of tasks) {
382
+ await exec1.enqueueWorkItem(task);
383
+ await exec2.enqueueWorkItem(task);
384
+ }
385
+
386
+ const order1 = exec1.getExecutionOrder();
387
+ const order2 = exec2.getExecutionOrder();
388
+
389
+ // Orders should be deterministic based on creation time
390
+ expect(order1.length).toBe(order2.length);
391
+ });
392
+ });
393
+
394
+ describe('Concurrent Item Handling', () => {
395
+ it('should handle multiple concurrent enqueues', async () => {
396
+ const promises = [];
397
+ for (let i = 0; i < 20; i++) {
398
+ promises.push(executor.enqueueWorkItem(`Concurrent task ${i}`));
399
+ }
400
+
401
+ const ids = await Promise.all(promises);
402
+
403
+ expect(ids.length).toBe(20);
404
+ // All IDs should be unique
405
+ const uniqueIds = new Set(ids);
406
+ expect(uniqueIds.size).toBe(20);
407
+ });
408
+
409
+ it('should handle concurrent polling', async () => {
410
+ const workItemIds = [];
411
+ for (let i = 0; i < 10; i++) {
412
+ workItemIds.push(await executor.enqueueWorkItem(`Task ${i}`));
413
+ }
414
+
415
+ const pollPromises = workItemIds.map(id => executor.pollWorkItem(id));
416
+ const items = await Promise.all(pollPromises);
417
+
418
+ expect(items.length).toBe(10);
419
+ items.forEach((item, i) => {
420
+ expect(item.id).toBe(workItemIds[i]);
421
+ expect(item.status).toBe(WORK_ITEM_STATES.QUEUED);
422
+ });
423
+ });
424
+
425
+ it('should handle concurrent state transitions', async () => {
426
+ const ids = [];
427
+ for (let i = 0; i < 5; i++) {
428
+ ids.push(await executor.enqueueWorkItem(`Task ${i}`));
429
+ }
430
+
431
+ const transitionPromises = ids.map(id =>
432
+ executor.transitionWorkItem(id, WORK_ITEM_STATES.RUNNING)
433
+ );
434
+
435
+ await Promise.all(transitionPromises);
436
+
437
+ const items = await Promise.all(ids.map(id => executor.pollWorkItem(id)));
438
+ items.forEach(item => {
439
+ expect(item.status).toBe(WORK_ITEM_STATES.RUNNING);
440
+ expect(item.started_ns).not.toBeNull();
441
+ });
442
+ });
443
+
444
+ it('should maintain data consistency under concurrent operations', async () => {
445
+ const id = await executor.enqueueWorkItem('Concurrent ops task');
446
+
447
+ // Concurrent operations on same work item
448
+ const ops = [
449
+ executor.addReceipt(id, { op: 1 }),
450
+ executor.addReceipt(id, { op: 2 }),
451
+ executor.addReceipt(id, { op: 3 }),
452
+ ];
453
+
454
+ await Promise.all(ops);
455
+
456
+ const item = await executor.pollWorkItem(id);
457
+ expect(item.receipt_log.length).toBe(3);
458
+ });
459
+ });
460
+
461
+ describe('Storage in O (Triple Store)', () => {
462
+ it('should store work items in RDF triple store', async () => {
463
+ const workItemId = await executor.enqueueWorkItem('RDF storage test');
464
+
465
+ // Query the store directly
466
+ const items = await executor.queryWorkItems(`
467
+ SELECT ?item ?goal WHERE {
468
+ ?item <http://kgc.io/goal> ?goal .
469
+ }
470
+ `);
471
+
472
+ expect(items.length).toBeGreaterThan(0);
473
+ });
474
+
475
+ it('should store work items under ./var/kgc/work-items/ namespace', async () => {
476
+ const workItemId = await executor.enqueueWorkItem('Namespaced item');
477
+ const workItem = await executor.pollWorkItem(workItemId);
478
+
479
+ expect(workItem.id).toContain('work-item');
480
+ });
481
+
482
+ it('should persist state transitions in triple store', async () => {
483
+ const workItemId = await executor.enqueueWorkItem('Persistent state');
484
+ await executor.transitionWorkItem(workItemId, WORK_ITEM_STATES.RUNNING);
485
+
486
+ // Query should reflect current state
487
+ const items = await executor.queryWorkItems(`
488
+ SELECT ?item ?status WHERE {
489
+ ?item <http://kgc.io/status> ?status .
490
+ FILTER(?status = "running")
491
+ }
492
+ `);
493
+
494
+ expect(items.length).toBeGreaterThan(0);
495
+ });
496
+ });
497
+
498
+ describe('Edge Cases and Error Handling', () => {
499
+ it('should handle empty goal string', async () => {
500
+ const workItemId = await executor.enqueueWorkItem('');
501
+ expect(workItemId).toBeDefined();
502
+
503
+ const workItem = await executor.pollWorkItem(workItemId);
504
+ expect(workItem.goal).toBe('');
505
+ });
506
+
507
+ it('should handle very long goal strings', async () => {
508
+ const longGoal = 'x'.repeat(10000);
509
+ const workItemId = await executor.enqueueWorkItem(longGoal);
510
+
511
+ const workItem = await executor.pollWorkItem(workItemId);
512
+ expect(workItem.goal).toBe(longGoal);
513
+ });
514
+
515
+ it('should handle complex bounds objects', async () => {
516
+ const bounds = {
517
+ timeout: 5000,
518
+ maxRetries: 3,
519
+ priority: 5,
520
+ metadata: {
521
+ nested: { deeply: { value: true } }
522
+ }
523
+ };
524
+
525
+ const workItemId = await executor.enqueueWorkItem('Complex bounds', bounds);
526
+ expect(workItemId).toBeDefined();
527
+ });
528
+
529
+ it('should throw error on invalid state transition', async () => {
530
+ const workItemId = await executor.enqueueWorkItem('Invalid transition');
531
+
532
+ await expect(
533
+ executor.transitionWorkItem(workItemId, 'INVALID_STATE')
534
+ ).rejects.toThrow();
535
+ });
536
+
537
+ it('should handle polling non-existent item gracefully', async () => {
538
+ const item = await executor.pollWorkItem('does-not-exist');
539
+ expect(item).toBeNull();
540
+ });
541
+
542
+ it('should handle adding receipt to non-existent item', async () => {
543
+ await expect(
544
+ executor.addReceipt('does-not-exist', { data: 'test' })
545
+ ).rejects.toThrow();
546
+ });
547
+ });
548
+ });