claude-flow 3.56.6 → 3.56.8
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/.claude/helpers/auto-memory-hook.mjs +109 -10
- package/.claude/helpers/intelligence.cjs +57 -4
- package/README.md +9 -3
- package/node_modules/@claude-flow/codex/README.md +20 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
- package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/package.json +6 -3
- package/v3/@claude-flow/cli/bin/cli.js +11 -1
- package/v3/@claude-flow/cli/bin/mcp-server.js +11 -1
- package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/commands/coverage-reader.js +247 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor-native-addon.d.ts +23 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor-native-addon.js +135 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +3 -0
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +17 -181
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +37 -5
- package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +19 -5
- package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +12 -0
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +98 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.d.ts +25 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.js +51 -2
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -14
- package/v3/@claude-flow/cli/dist/src/init/helper-commands.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-commands.js +2 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +4 -1
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +3 -3
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +26 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +2 -1
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -1
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +3 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +3 -2
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +12 -11
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +7 -3
- package/v3/@claude-flow/cli/dist/src/mods/install.js +32 -12
- package/v3/@claude-flow/cli/dist/src/mods/plugin-resolve.js +3 -1
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +92 -4
- package/v3/@claude-flow/cli/dist/src/services/command-recording.d.ts +17 -0
- package/v3/@claude-flow/cli/dist/src/services/command-recording.js +91 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +20 -2
- package/v3/@claude-flow/cli/package.json +3 -3
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.d.ts +9 -0
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.js +44 -3
- package/v3/@claude-flow/shared/dist/mcp/transport/http.js +11 -1
- package/v3/@claude-flow/shared/package.json +12 -9
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.d.ts +0 -2
- package/v3/@claude-flow/shared/dist/events/event-store-pagination.test.js +0 -32
- package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +0 -8
- package/v3/@claude-flow/shared/dist/events/event-store.test.js +0 -313
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +0 -9
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +0 -322
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +0 -9
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +0 -93
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.d.ts +0 -2
- package/v3/@claude-flow/shared/dist/resilience/bulkhead-sync-throw.test.js +0 -33
- package/v3/@claude-flow/shared/dist/resilience/retry-timeout-cleanup.test.d.ts +0 -2
- 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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/shared",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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,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,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
|