claude-flow 3.56.7 → 3.56.9

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 (47) hide show
  1. package/.claude/helpers/hook-handler.cjs +109 -23
  2. package/README.md +9 -3
  3. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  4. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  5. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  6. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  7. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  8. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  9. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  10. package/node_modules/@claude-flow/codex/package.json +1 -1
  11. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  12. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  13. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  14. package/node_modules/@claude-flow/security/package.json +1 -1
  15. package/package.json +5 -5
  16. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +40 -29
  17. package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.d.ts +29 -0
  18. package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.js +247 -0
  19. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +9 -180
  20. package/v3/@claude-flow/cli/dist/src/commands/tui.js +2 -2
  21. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +109 -23
  22. package/v3/@claude-flow/cli/dist/src/parser.js +32 -22
  23. package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +1 -0
  24. package/v3/@claude-flow/cli/dist/src/production/monitoring.js +19 -6
  25. package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +5 -4
  26. package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -0
  27. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +3 -3
  28. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +20 -2
  29. package/v3/@claude-flow/cli/package.json +5 -5
  30. package/v3/@claude-flow/shared/dist/events/rvf-event-log.d.ts +9 -0
  31. package/v3/@claude-flow/shared/dist/events/rvf-event-log.js +44 -3
  32. package/v3/@claude-flow/shared/dist/mcp/transport/http.js +11 -1
  33. package/v3/@claude-flow/shared/dist/resilience/rate-limiter.js +3 -3
  34. package/v3/@claude-flow/shared/dist/resilience/retry.js +3 -1
  35. package/v3/@claude-flow/shared/package.json +12 -9
  36. package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.d.ts +0 -2
  37. package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.js +0 -32
  38. package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +0 -8
  39. package/v3/@claude-flow/shared/dist/events/event-store.test.js +0 -313
  40. package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +0 -9
  41. package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +0 -322
  42. package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +0 -9
  43. package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +0 -93
  44. package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.d.ts +0 -2
  45. package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.js +0 -33
  46. package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.d.ts +0 -2
  47. package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.js +0 -38
@@ -15,7 +15,7 @@
15
15
  * @module v3/shared/events/rvf-event-log
16
16
  */
17
17
  import { EventEmitter } from 'node:events';
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, renameSync } from 'node:fs';
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, renameSync, truncateSync } from 'node:fs';
19
19
  import { dirname } from 'node:path';
20
20
  /** Validate a file path is safe */
21
21
  function validatePath(p) {
@@ -26,6 +26,7 @@ const DEFAULT_CONFIG = {
26
26
  logPath: 'events.rvf',
27
27
  verbose: false,
28
28
  snapshotThreshold: 100,
29
+ repairOversizedTail: false,
29
30
  };
30
31
  // =============================================================================
31
32
  // Constants
@@ -34,12 +35,17 @@ const DEFAULT_CONFIG = {
34
35
  const MAGIC = Buffer.from('RVFL');
35
36
  const MAGIC_LENGTH = 4;
36
37
  const LENGTH_PREFIX_BYTES = 4;
38
+ /** Largest record replayFile() will read; appendRecord() refuses anything bigger so no accepted append is unreadable. */
39
+ const MAX_PAYLOAD_SIZE = 100 * 1024 * 1024; // 100MB safety limit
37
40
  // =============================================================================
38
41
  // RvfEventLog Implementation
39
42
  // =============================================================================
40
43
  export class RvfEventLog extends EventEmitter {
41
44
  config;
42
45
  initialized = false;
46
+ /** Files whose incomplete tails could not be repaired because they are read-only. */
47
+ /** Files that hold bytes we could not repair; appends to them are refused (value: why). */
48
+ readOnlyFiles = new Map();
43
49
  /**
44
50
  * All events kept in insertion order.
45
51
  * Rebuilt from the file on initialize().
@@ -115,6 +121,7 @@ export class RvfEventLog extends EventEmitter {
115
121
  this.aggregateIndex.clear();
116
122
  this.aggregateVersions.clear();
117
123
  this.snapshots.clear();
124
+ this.readOnlyFiles.clear();
118
125
  this.initialized = false;
119
126
  this.emit('shutdown');
120
127
  }
@@ -273,22 +280,24 @@ export class RvfEventLog extends EventEmitter {
273
280
  throw new Error(`[RvfEventLog] Invalid file header in ${filePath}`);
274
281
  }
275
282
  let offset = MAGIC_LENGTH;
276
- const MAX_PAYLOAD_SIZE = 100 * 1024 * 1024; // 100MB safety limit
283
+ let oversizedAt = -1;
277
284
  while (offset + LENGTH_PREFIX_BYTES <= buf.length) {
278
285
  const payloadLength = buf.readUInt32BE(offset);
279
286
  offset += LENGTH_PREFIX_BYTES;
280
287
  if (payloadLength > MAX_PAYLOAD_SIZE) {
288
+ oversizedAt = offset - LENGTH_PREFIX_BYTES;
281
289
  if (this.config.verbose) {
282
290
  console.warn(`[RvfEventLog] Payload size ${payloadLength} exceeds safety limit`);
283
291
  }
284
292
  break;
285
293
  }
286
294
  if (offset + payloadLength > buf.length) {
287
- // Truncated record — stop reading (crash recovery).
295
+ // Remove only the incomplete record before accepting future appends.
288
296
  if (this.config.verbose) {
289
297
  console.warn(`[RvfEventLog] Truncated record at offset ${offset - LENGTH_PREFIX_BYTES} — ` +
290
298
  `expected ${payloadLength} bytes, have ${buf.length - offset}`);
291
299
  }
300
+ offset -= LENGTH_PREFIX_BYTES;
292
301
  break;
293
302
  }
294
303
  const json = buf.subarray(offset, offset + payloadLength).toString('utf8');
@@ -303,12 +312,44 @@ export class RvfEventLog extends EventEmitter {
303
312
  }
304
313
  }
305
314
  }
315
+ // An oversized length prefix is not a torn write: keep the bytes unless the caller opted in to repair.
316
+ let keepUntil = offset;
317
+ if (oversizedAt >= 0) {
318
+ if (!this.config.repairOversizedTail) {
319
+ this.readOnlyFiles.set(filePath, 'a record length prefix exceeds the safety limit');
320
+ return;
321
+ }
322
+ keepUntil = oversizedAt;
323
+ }
324
+ if (keepUntil < buf.length) {
325
+ try {
326
+ truncateSync(filePath, keepUntil);
327
+ }
328
+ catch (error) {
329
+ const code = error.code;
330
+ if (code !== 'EACCES' && code !== 'EROFS' && code !== 'EPERM')
331
+ throw error;
332
+ // Preserve readable history but never append behind an unreadable tail.
333
+ this.readOnlyFiles.set(filePath, 'the file is not writable, so its damaged tail could not be repaired');
334
+ if (this.config.verbose) {
335
+ console.warn(`[RvfEventLog] Read-only log has an unrepaired tail: ${filePath}`);
336
+ }
337
+ }
338
+ }
306
339
  }
307
340
  /** Append a single record to an RVF file. */
308
341
  appendRecord(filePath, record) {
342
+ const refused = this.readOnlyFiles.get(filePath);
343
+ if (refused !== undefined) {
344
+ throw new Error(`[RvfEventLog] Read-only until repaired: unrepaired tail (${refused}); append refused: ${filePath}`);
345
+ }
309
346
  const json = JSON.stringify(record);
310
347
  const persisted = JSON.parse(json);
311
348
  const payload = Buffer.from(json, 'utf8');
349
+ if (payload.length > MAX_PAYLOAD_SIZE) {
350
+ // replayFile() refuses records this large, so writing one would be an append that can never be read back.
351
+ throw new Error(`[RvfEventLog] Record of ${payload.length} bytes exceeds the ${MAX_PAYLOAD_SIZE} byte limit; append refused`);
352
+ }
312
353
  const lengthBuf = Buffer.allocUnsafe(LENGTH_PREFIX_BYTES);
313
354
  lengthBuf.writeUInt32BE(payload.length, 0);
314
355
  appendFileSync(filePath, Buffer.concat([lengthBuf, payload]));
@@ -270,6 +270,15 @@ export class HttpTransport extends EventEmitter {
270
270
  });
271
271
  // Error handler
272
272
  this.app.use((err, req, res, next) => {
273
+ // body-parser marks a malformed JSON body: that is the client's JSON-RPC parse error, not ours.
274
+ if (err.type === 'entity.parse.failed') {
275
+ res.status(400).json({
276
+ jsonrpc: '2.0',
277
+ id: null,
278
+ error: { code: -32700, message: 'Parse error' },
279
+ });
280
+ return;
281
+ }
273
282
  this.logger.error('Express error', { error: err });
274
283
  this.errors++;
275
284
  res.status(500).json({
@@ -335,7 +344,8 @@ export class HttpTransport extends EventEmitter {
335
344
  // No auth configured but auth is required - warn and continue (development mode)
336
345
  this.logger.warn('No authentication configured - running in development mode');
337
346
  }
338
- const message = req.body;
347
+ // Express 5 leaves req.body undefined when no JSON body was parsed (Express 4 gave {}).
348
+ const message = req.body ?? {};
339
349
  // Validate JSON-RPC format
340
350
  if (message.jsonrpc !== '2.0') {
341
351
  res.status(400).json({
@@ -120,7 +120,7 @@ export class SlidingWindowRateLimiter {
120
120
  if (!entries)
121
121
  return;
122
122
  const cutoff = Date.now() - this.options.windowMs;
123
- const filtered = entries.filter((e) => e.timestamp >= cutoff);
123
+ const filtered = entries.filter((e) => e.timestamp > cutoff);
124
124
  if (filtered.length === 0) {
125
125
  this.requests.delete(key);
126
126
  }
@@ -190,7 +190,7 @@ export class TokenBucketRateLimiter {
190
190
  allowed: bucket.tokens >= 1,
191
191
  remaining: Math.floor(bucket.tokens),
192
192
  resetAt: new Date(bucket.lastRefill + this.options.windowMs),
193
- retryAfter: bucket.tokens >= 1 ? 0 : this.options.windowMs,
193
+ retryAfter: bucket.tokens >= 1 ? 0 : Math.max(0, bucket.lastRefill + this.options.windowMs - Date.now()),
194
194
  total: this.options.maxRequests,
195
195
  used: this.options.maxRequests - Math.floor(bucket.tokens),
196
196
  };
@@ -206,7 +206,7 @@ export class TokenBucketRateLimiter {
206
206
  allowed: false,
207
207
  remaining: 0,
208
208
  resetAt: new Date(bucket.lastRefill + this.options.windowMs),
209
- retryAfter: this.options.windowMs,
209
+ retryAfter: Math.max(0, bucket.lastRefill + this.options.windowMs - Date.now()),
210
210
  total: this.options.maxRequests,
211
211
  used: this.options.maxRequests,
212
212
  };
@@ -144,7 +144,9 @@ export const RetryableErrors = {
144
144
  /** Network errors (ECONNRESET, ETIMEDOUT, etc.) */
145
145
  network: (error) => {
146
146
  const networkCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN'];
147
- return networkCodes.some((code) => error.message.includes(code));
147
+ const code = error.code;
148
+ return (typeof code === 'string' && networkCodes.includes(code)) ||
149
+ networkCodes.some((code) => error.message.includes(code));
148
150
  },
149
151
  /** Rate limit errors (429) */
150
152
  rateLimit: (error) => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/shared",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "type": "module",
5
5
  "description": "Shared module - common types, events, utilities, core interfaces",
6
6
  "main": "dist/index.js",
@@ -18,6 +18,9 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
+ "!dist/**/*.test.*",
22
+ "!dist/**/*.spec.*",
23
+ "!dist/**/__tests__",
21
24
  "README.md"
22
25
  ],
23
26
  "scripts": {
@@ -25,20 +28,20 @@
25
28
  "build": "tsc"
26
29
  },
27
30
  "dependencies": {
28
- "sql.js": "^1.10.3"
31
+ "@types/ws": "^8.5.10",
32
+ "cors": "^2.8.5",
33
+ "express": "^4.22.2 || ^5.0.0",
34
+ "helmet": "^7.2.0",
35
+ "sql.js": "^1.10.3",
36
+ "ws": "^8.21.0",
37
+ "zod": "^3.22.4"
29
38
  },
30
39
  "devDependencies": {
31
40
  "@types/cors": "^2.8.17",
32
41
  "@types/express": "^4.17.21",
33
42
  "@types/node": "^20.0.0",
34
43
  "@types/sql.js": "^1.4.9",
35
- "@types/ws": "^8.5.10",
36
- "cors": "^2.8.5",
37
- "express": "^4.21.0",
38
- "helmet": "^7.1.0",
39
- "vitest": "^4.1.0",
40
- "ws": "^8.16.0",
41
- "zod": "^3.22.4"
44
+ "vitest": "^4.1.0"
42
45
  },
43
46
  "publishConfig": {
44
47
  "access": "public"
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=event-store-pagination.test.d.ts.map
@@ -1,32 +0,0 @@
1
- import { beforeEach, afterEach, describe, it, expect } from 'vitest';
2
- import { EventStore } from './event-store.js';
3
- import { createAgentSpawnedEvent } from './domain-events.js';
4
- describe('SQLite event query pagination', () => {
5
- let store;
6
- beforeEach(async () => {
7
- store = new EventStore({
8
- databasePath: ':memory:',
9
- autoPersistInterval: 0,
10
- });
11
- await store.initialize();
12
- for (let i = 0; i < 3; i++) {
13
- const event = createAgentSpawnedEvent(`agent-${i}`, 'coder', 'core', []);
14
- event.timestamp = 100 + i;
15
- await store.append(event);
16
- }
17
- });
18
- afterEach(async () => {
19
- await store.shutdown();
20
- });
21
- it('supports offsets without a caller-specified limit', async () => {
22
- expect((await store.query({ offset: 1 })).map((event) => event.aggregateId)).toEqual(['agent-1', 'agent-2']);
23
- });
24
- it('returns no rows for an explicit zero limit', async () => {
25
- expect(await store.query({ limit: 0 })).toEqual([]);
26
- expect(await store.query({ limit: 0, offset: 1 })).toEqual([]);
27
- });
28
- it('retains bounded page ordering', async () => {
29
- expect((await store.query({ limit: 1, offset: 1 })).map((event) => event.aggregateId)).toEqual(['agent-1']);
30
- });
31
- });
32
- //# sourceMappingURL=event-store-pagination.test.js.map
@@ -1,8 +0,0 @@
1
- /**
2
- * Event Store Tests
3
- *
4
- * Comprehensive tests for the event sourcing implementation.
5
- * Tests event storage, retrieval, filtering, snapshots, and projections.
6
- */
7
- export {};
8
- //# sourceMappingURL=event-store.test.d.ts.map
@@ -1,313 +0,0 @@
1
- /**
2
- * Event Store Tests
3
- *
4
- * Comprehensive tests for the event sourcing implementation.
5
- * Tests event storage, retrieval, filtering, snapshots, and projections.
6
- */
7
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
- import { EventStore } from './event-store.js';
9
- import { StateReconstructor, TaskAggregate } from './state-reconstructor.js';
10
- import { AgentStateProjection, TaskHistoryProjection, MemoryIndexProjection } from './projections.js';
11
- import { createAgentSpawnedEvent, createAgentStartedEvent, createAgentTaskAssignedEvent, createAgentTaskCompletedEvent, createTaskCreatedEvent, createTaskStartedEvent, createTaskCompletedEvent, createMemoryStoredEvent, createMemoryRetrievedEvent, createMemoryDeletedEvent, } from './domain-events.js';
12
- describe('EventStore', () => {
13
- let eventStore;
14
- beforeEach(async () => {
15
- eventStore = new EventStore({ databasePath: ':memory:', verbose: false });
16
- await eventStore.initialize();
17
- });
18
- afterEach(async () => {
19
- await eventStore.shutdown();
20
- });
21
- describe('Event Appending', () => {
22
- it('should append events to the store', async () => {
23
- const event = createAgentSpawnedEvent('agent-1', 'coder', 'core', ['coding', 'testing']);
24
- await eventStore.append(event);
25
- const events = await eventStore.getEvents('agent-1');
26
- expect(events).toHaveLength(1);
27
- expect(events[0].type).toBe('agent:spawned');
28
- expect(events[0].version).toBe(1);
29
- });
30
- it('should increment version for each event', async () => {
31
- const event1 = createAgentSpawnedEvent('agent-1', 'coder', 'core', []);
32
- const event2 = createAgentStartedEvent('agent-1');
33
- await eventStore.append(event1);
34
- await eventStore.append(event2);
35
- const events = await eventStore.getEvents('agent-1');
36
- expect(events[0].version).toBe(1);
37
- expect(events[1].version).toBe(2);
38
- });
39
- it('should maintain version per aggregate', async () => {
40
- const agent1Event = createAgentSpawnedEvent('agent-1', 'coder', 'core', []);
41
- const agent2Event = createAgentSpawnedEvent('agent-2', 'tester', 'quality', []);
42
- await eventStore.append(agent1Event);
43
- await eventStore.append(agent2Event);
44
- const agent1Events = await eventStore.getEvents('agent-1');
45
- const agent2Events = await eventStore.getEvents('agent-2');
46
- expect(agent1Events[0].version).toBe(1);
47
- expect(agent2Events[0].version).toBe(1);
48
- });
49
- });
50
- describe('Event Retrieval', () => {
51
- beforeEach(async () => {
52
- // Setup: Create multiple events
53
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
54
- await eventStore.append(createAgentStartedEvent('agent-1'));
55
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Build feature', 'Description', 'high', []));
56
- await eventStore.append(createTaskStartedEvent('task-1', 'agent-1'));
57
- });
58
- it('should retrieve events by aggregate ID', async () => {
59
- const events = await eventStore.getEvents('agent-1');
60
- expect(events).toHaveLength(2);
61
- expect(events[0].type).toBe('agent:spawned');
62
- expect(events[1].type).toBe('agent:started');
63
- });
64
- it('should retrieve events from specific version', async () => {
65
- const events = await eventStore.getEvents('agent-1', 2);
66
- expect(events).toHaveLength(1);
67
- expect(events[0].type).toBe('agent:started');
68
- });
69
- it('should retrieve events by type', async () => {
70
- const events = await eventStore.getEventsByType('task:created');
71
- expect(events).toHaveLength(1);
72
- expect(events[0].aggregateId).toBe('task-1');
73
- });
74
- });
75
- describe('Event Filtering', () => {
76
- beforeEach(async () => {
77
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
78
- await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
79
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task 1', 'Desc', 'high', []));
80
- await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
81
- });
82
- it('should filter by aggregate types', async () => {
83
- const events = await eventStore.query({ aggregateTypes: ['agent'] });
84
- expect(events).toHaveLength(2);
85
- expect(events.every((e) => e.aggregateType === 'agent')).toBe(true);
86
- });
87
- it('should filter by event types', async () => {
88
- const events = await eventStore.query({ eventTypes: ['agent:spawned'] });
89
- expect(events).toHaveLength(2);
90
- expect(events.every((e) => e.type === 'agent:spawned')).toBe(true);
91
- });
92
- it('should filter by aggregate IDs', async () => {
93
- const events = await eventStore.query({ aggregateIds: ['agent-1', 'task-1'] });
94
- expect(events).toHaveLength(2);
95
- });
96
- it('should support pagination', async () => {
97
- const page1 = await eventStore.query({ limit: 2, offset: 0 });
98
- const page2 = await eventStore.query({ limit: 2, offset: 2 });
99
- expect(page1).toHaveLength(2);
100
- expect(page2).toHaveLength(2);
101
- expect(page1[0].id).not.toBe(page2[0].id);
102
- });
103
- });
104
- describe('Event Replay', () => {
105
- it('should replay all events', async () => {
106
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
107
- await eventStore.append(createAgentStartedEvent('agent-1'));
108
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
109
- const events = [];
110
- for await (const event of eventStore.replay()) {
111
- events.push(event);
112
- }
113
- expect(events).toHaveLength(3);
114
- });
115
- it('should replay from specific version', async () => {
116
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
117
- await eventStore.append(createAgentStartedEvent('agent-1'));
118
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
119
- const events = [];
120
- for await (const event of eventStore.replay(2)) {
121
- events.push(event);
122
- }
123
- expect(events.length).toBeGreaterThanOrEqual(2);
124
- });
125
- });
126
- describe('Snapshots', () => {
127
- it('should save snapshots', async () => {
128
- const snapshot = {
129
- aggregateId: 'agent-1',
130
- aggregateType: 'agent',
131
- version: 5,
132
- state: { status: 'active', tasks: ['task-1'] },
133
- timestamp: Date.now(),
134
- };
135
- await eventStore.saveSnapshot(snapshot);
136
- const retrieved = await eventStore.getSnapshot('agent-1');
137
- expect(retrieved).not.toBeNull();
138
- expect(retrieved?.version).toBe(5);
139
- expect(retrieved?.state).toEqual({ status: 'active', tasks: ['task-1'] });
140
- });
141
- it('should return latest snapshot', async () => {
142
- const snapshot1 = {
143
- aggregateId: 'agent-1',
144
- aggregateType: 'agent',
145
- version: 5,
146
- state: { status: 'active' },
147
- timestamp: Date.now(),
148
- };
149
- const snapshot2 = {
150
- aggregateId: 'agent-1',
151
- aggregateType: 'agent',
152
- version: 10,
153
- state: { status: 'completed' },
154
- timestamp: Date.now(),
155
- };
156
- await eventStore.saveSnapshot(snapshot1);
157
- await eventStore.saveSnapshot(snapshot2);
158
- const retrieved = await eventStore.getSnapshot('agent-1');
159
- expect(retrieved?.version).toBe(10);
160
- });
161
- it('should return null when no snapshot exists for the aggregate', async () => {
162
- await expect(eventStore.getSnapshot('missing')).resolves.toBeNull();
163
- });
164
- it('should return null for an unknown aggregate when other snapshots exist', async () => {
165
- await eventStore.saveSnapshot({
166
- aggregateId: 'agent-1',
167
- aggregateType: 'agent',
168
- version: 1,
169
- state: {},
170
- timestamp: Date.now(),
171
- });
172
- await expect(eventStore.getSnapshot('other')).resolves.toBeNull();
173
- });
174
- it('should reconstruct from the event log when no snapshot exists', async () => {
175
- await eventStore.append(createTaskCreatedEvent('task-1', 'impl', 'Write code', 'desc', 'normal', []));
176
- const reconstructor = new StateReconstructor(eventStore);
177
- const task = await reconstructor.reconstruct('task-1', (id) => new TaskAggregate(id));
178
- expect(task.version).toBe(1);
179
- });
180
- });
181
- describe('Statistics', () => {
182
- it('should provide event store statistics', async () => {
183
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
184
- await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
185
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
186
- const stats = await eventStore.getStats();
187
- expect(stats.totalEvents).toBe(3);
188
- expect(stats.eventsByType['agent:spawned']).toBe(2);
189
- expect(stats.eventsByType['task:created']).toBe(1);
190
- expect(stats.eventsByAggregate['agent-1']).toBe(1);
191
- expect(stats.eventsByAggregate['agent-2']).toBe(1);
192
- });
193
- });
194
- });
195
- describe('AgentStateProjection', () => {
196
- let eventStore;
197
- let projection;
198
- beforeEach(async () => {
199
- eventStore = new EventStore({ databasePath: ':memory:' });
200
- await eventStore.initialize();
201
- projection = new AgentStateProjection(eventStore);
202
- });
203
- afterEach(async () => {
204
- await eventStore.shutdown();
205
- });
206
- it('should build agent state from events', async () => {
207
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', ['coding']));
208
- await eventStore.append(createAgentStartedEvent('agent-1'));
209
- await projection.initialize();
210
- const agent = projection.getAgent('agent-1');
211
- expect(agent).not.toBeNull();
212
- expect(agent?.role).toBe('coder');
213
- expect(agent?.status).toBe('active');
214
- });
215
- it('should track task assignments', async () => {
216
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
217
- await eventStore.append(createAgentTaskAssignedEvent('agent-1', 'task-1', Date.now()));
218
- await projection.initialize();
219
- const agent = projection.getAgent('agent-1');
220
- expect(agent?.currentTask).toBe('task-1');
221
- });
222
- it('should track completed tasks', async () => {
223
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
224
- await eventStore.append(createAgentTaskCompletedEvent('agent-1', 'task-1', { success: true }, Date.now(), 5000));
225
- await projection.initialize();
226
- const agent = projection.getAgent('agent-1');
227
- expect(agent?.completedTasks).toContain('task-1');
228
- expect(agent?.taskCount).toBe(1);
229
- });
230
- it('should filter agents by status', async () => {
231
- await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
232
- await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
233
- await eventStore.append(createAgentStartedEvent('agent-1'));
234
- await projection.initialize();
235
- const activeAgents = projection.getAgentsByStatus('active');
236
- const idleAgents = projection.getAgentsByStatus('idle');
237
- expect(activeAgents).toHaveLength(1);
238
- expect(idleAgents).toHaveLength(1);
239
- });
240
- });
241
- describe('TaskHistoryProjection', () => {
242
- let eventStore;
243
- let projection;
244
- beforeEach(async () => {
245
- eventStore = new EventStore({ databasePath: ':memory:' });
246
- await eventStore.initialize();
247
- projection = new TaskHistoryProjection(eventStore);
248
- });
249
- afterEach(async () => {
250
- await eventStore.shutdown();
251
- });
252
- it('should build task history from events', async () => {
253
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Build Feature', 'Description', 'high', []));
254
- await eventStore.append(createTaskStartedEvent('task-1', 'agent-1'));
255
- await eventStore.append(createTaskCompletedEvent('task-1', { success: true }, 5000));
256
- await projection.initialize();
257
- const task = projection.getTask('task-1');
258
- expect(task).not.toBeNull();
259
- expect(task?.title).toBe('Build Feature');
260
- expect(task?.status).toBe('completed');
261
- expect(task?.assignedAgent).toBe('agent-1');
262
- });
263
- it('should calculate average task duration', async () => {
264
- await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task 1', 'Desc', 'high', []));
265
- await eventStore.append(createTaskCompletedEvent('task-1', { success: true }, 5000));
266
- await eventStore.append(createTaskCreatedEvent('task-2', 'implementation', 'Task 2', 'Desc', 'high', []));
267
- await eventStore.append(createTaskCompletedEvent('task-2', { success: true }, 3000));
268
- await projection.initialize();
269
- const avgDuration = projection.getAverageTaskDuration();
270
- expect(avgDuration).toBe(4000);
271
- });
272
- });
273
- describe('MemoryIndexProjection', () => {
274
- let eventStore;
275
- let projection;
276
- beforeEach(async () => {
277
- eventStore = new EventStore({ databasePath: ':memory:' });
278
- await eventStore.initialize();
279
- projection = new MemoryIndexProjection(eventStore);
280
- });
281
- afterEach(async () => {
282
- await eventStore.shutdown();
283
- });
284
- it('should build memory index from events', async () => {
285
- await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
286
- await eventStore.append(createMemoryRetrievedEvent('mem-1', 'default', 'key1', 1));
287
- await projection.initialize();
288
- const memory = projection.getMemory('mem-1');
289
- expect(memory).not.toBeNull();
290
- expect(memory?.namespace).toBe('default');
291
- expect(memory?.accessCount).toBe(1);
292
- });
293
- it('should track memory deletions', async () => {
294
- await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
295
- await eventStore.append(createMemoryDeletedEvent('mem-1', 'default', 'key1'));
296
- await projection.initialize();
297
- const memory = projection.getMemory('mem-1');
298
- expect(memory?.isDeleted).toBe(true);
299
- const activeMemories = projection.getActiveMemories();
300
- expect(activeMemories).toHaveLength(0);
301
- });
302
- it('should calculate total size by namespace', async () => {
303
- await eventStore.append(createMemoryStoredEvent('mem-1', 'ns1', 'key1', 'semantic', 1024));
304
- await eventStore.append(createMemoryStoredEvent('mem-2', 'ns1', 'key2', 'semantic', 2048));
305
- await eventStore.append(createMemoryStoredEvent('mem-3', 'ns2', 'key3', 'semantic', 512));
306
- await projection.initialize();
307
- const ns1Size = projection.getTotalSizeByNamespace('ns1');
308
- const ns2Size = projection.getTotalSizeByNamespace('ns2');
309
- expect(ns1Size).toBe(3072);
310
- expect(ns2Size).toBe(512);
311
- });
312
- });
313
- //# sourceMappingURL=event-store.test.js.map
@@ -1,9 +0,0 @@
1
- /**
2
- * V3 Hooks System - Tests
3
- *
4
- * Comprehensive tests for hook registry and executor.
5
- *
6
- * @module v3/shared/hooks/hooks.test
7
- */
8
- export {};
9
- //# sourceMappingURL=hooks.test.d.ts.map