@sparkleideas/testing 3.0.0-alpha.7
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/README.md +547 -0
- package/__tests__/framework.test.ts +21 -0
- package/package.json +61 -0
- package/src/fixtures/agent-fixtures.ts +793 -0
- package/src/fixtures/agents.ts +212 -0
- package/src/fixtures/configurations.ts +491 -0
- package/src/fixtures/index.ts +21 -0
- package/src/fixtures/mcp-fixtures.ts +1030 -0
- package/src/fixtures/memory-entries.ts +328 -0
- package/src/fixtures/memory-fixtures.ts +750 -0
- package/src/fixtures/swarm-fixtures.ts +837 -0
- package/src/fixtures/tasks.ts +309 -0
- package/src/helpers/assertion-helpers.ts +616 -0
- package/src/helpers/assertions.ts +286 -0
- package/src/helpers/create-mock.ts +200 -0
- package/src/helpers/index.ts +182 -0
- package/src/helpers/mock-factory.ts +711 -0
- package/src/helpers/setup-teardown.ts +678 -0
- package/src/helpers/swarm-instance.ts +326 -0
- package/src/helpers/test-application.ts +310 -0
- package/src/helpers/test-utils.ts +670 -0
- package/src/index.ts +232 -0
- package/src/mocks/index.ts +29 -0
- package/src/mocks/mock-mcp-client.ts +723 -0
- package/src/mocks/mock-services.ts +793 -0
- package/src/regression/api-contract.ts +473 -0
- package/src/regression/index.ts +46 -0
- package/src/regression/integration-regression.ts +416 -0
- package/src/regression/performance-baseline.ts +356 -0
- package/src/regression/regression-runner.ts +339 -0
- package/src/regression/security-regression.ts +331 -0
- package/src/setup.ts +127 -0
- package/src/v2-compat/api-compat.test.ts +590 -0
- package/src/v2-compat/cli-compat.test.ts +484 -0
- package/src/v2-compat/compatibility-validator.ts +1072 -0
- package/src/v2-compat/hooks-compat.test.ts +602 -0
- package/src/v2-compat/index.ts +58 -0
- package/src/v2-compat/mcp-compat.test.ts +557 -0
- package/src/v2-compat/report-generator.ts +441 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/testing - Mock Services
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive mock implementations of V3 core services.
|
|
5
|
+
* Provides realistic behavior for testing with full state tracking.
|
|
6
|
+
*/
|
|
7
|
+
import { vi, type Mock } from 'vitest';
|
|
8
|
+
import type { V3AgentType } from '../fixtures/agent-fixtures.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mock AgentDB - Vector database mock with HNSW simulation
|
|
12
|
+
*/
|
|
13
|
+
export class MockAgentDB {
|
|
14
|
+
private vectors = new Map<string, { embedding: number[]; metadata: Record<string, unknown> }>();
|
|
15
|
+
private indexConfig = {
|
|
16
|
+
M: 16,
|
|
17
|
+
efConstruction: 200,
|
|
18
|
+
efSearch: 50,
|
|
19
|
+
dimensions: 384,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Mock methods for verification
|
|
23
|
+
insert = vi.fn(async (id: string, embedding: number[], metadata?: Record<string, unknown>) => {
|
|
24
|
+
if (embedding.length !== this.indexConfig.dimensions) {
|
|
25
|
+
throw new Error(`Invalid embedding dimensions: expected ${this.indexConfig.dimensions}, got ${embedding.length}`);
|
|
26
|
+
}
|
|
27
|
+
this.vectors.set(id, { embedding, metadata: metadata ?? {} });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
search = vi.fn(async (embedding: number[], k: number, threshold?: number) => {
|
|
31
|
+
const results: Array<{ id: string; score: number; metadata: Record<string, unknown> }> = [];
|
|
32
|
+
|
|
33
|
+
for (const [id, data] of this.vectors) {
|
|
34
|
+
const score = this.cosineSimilarity(embedding, data.embedding);
|
|
35
|
+
if (threshold === undefined || score >= threshold) {
|
|
36
|
+
results.push({ id, score, metadata: data.metadata });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results
|
|
41
|
+
.sort((a, b) => b.score - a.score)
|
|
42
|
+
.slice(0, k);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
delete = vi.fn(async (id: string) => {
|
|
46
|
+
this.vectors.delete(id);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
update = vi.fn(async (id: string, embedding: number[], metadata?: Record<string, unknown>) => {
|
|
50
|
+
const existing = this.vectors.get(id);
|
|
51
|
+
if (!existing) {
|
|
52
|
+
throw new Error(`Vector not found: ${id}`);
|
|
53
|
+
}
|
|
54
|
+
this.vectors.set(id, { embedding, metadata: metadata ?? existing.metadata });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
getVector = vi.fn(async (id: string) => {
|
|
58
|
+
return this.vectors.get(id) ?? null;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
getStats = vi.fn(() => ({
|
|
62
|
+
vectorCount: this.vectors.size,
|
|
63
|
+
indexSize: this.vectors.size * this.indexConfig.dimensions * 4, // 4 bytes per float
|
|
64
|
+
dimensions: this.indexConfig.dimensions,
|
|
65
|
+
M: this.indexConfig.M,
|
|
66
|
+
efConstruction: this.indexConfig.efConstruction,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
rebuildIndex = vi.fn(async () => {
|
|
70
|
+
// Simulate index rebuild
|
|
71
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
clear = vi.fn(() => {
|
|
75
|
+
this.vectors.clear();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
private cosineSimilarity(a: number[], b: number[]): number {
|
|
79
|
+
let dotProduct = 0;
|
|
80
|
+
let normA = 0;
|
|
81
|
+
let normB = 0;
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < a.length; i++) {
|
|
84
|
+
dotProduct += a[i] * b[i];
|
|
85
|
+
normA += a[i] * a[i];
|
|
86
|
+
normB += b[i] * b[i];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Configure the mock index
|
|
94
|
+
*/
|
|
95
|
+
configure(config: Partial<typeof this.indexConfig>): void {
|
|
96
|
+
Object.assign(this.indexConfig, config);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get all stored vectors (for testing)
|
|
101
|
+
*/
|
|
102
|
+
getAllVectors(): Map<string, { embedding: number[]; metadata: Record<string, unknown> }> {
|
|
103
|
+
return new Map(this.vectors);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mock Unified Swarm Coordinator
|
|
109
|
+
*/
|
|
110
|
+
export class MockSwarmCoordinator {
|
|
111
|
+
private agents = new Map<string, MockSwarmAgent>();
|
|
112
|
+
private state: SwarmState = {
|
|
113
|
+
id: `swarm-${Date.now()}`,
|
|
114
|
+
topology: 'hierarchical-mesh',
|
|
115
|
+
status: 'idle',
|
|
116
|
+
agentCount: 0,
|
|
117
|
+
activeAgentCount: 0,
|
|
118
|
+
leaderId: undefined,
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
};
|
|
121
|
+
private messageQueue: SwarmMessage[] = [];
|
|
122
|
+
private taskQueue: SwarmTask[] = [];
|
|
123
|
+
|
|
124
|
+
initialize = vi.fn(async (config: SwarmInitConfig) => {
|
|
125
|
+
this.state = {
|
|
126
|
+
...this.state,
|
|
127
|
+
topology: config.topology ?? 'hierarchical-mesh',
|
|
128
|
+
status: 'active',
|
|
129
|
+
};
|
|
130
|
+
return this.state;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
shutdown = vi.fn(async (graceful: boolean = true) => {
|
|
134
|
+
if (graceful) {
|
|
135
|
+
// Complete pending tasks
|
|
136
|
+
await Promise.all(
|
|
137
|
+
Array.from(this.agents.values()).map(agent => agent.terminate())
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
this.state.status = 'shutdown';
|
|
141
|
+
this.agents.clear();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
addAgent = vi.fn(async (config: AgentConfig) => {
|
|
145
|
+
const id = `agent-${config.type}-${Date.now()}`;
|
|
146
|
+
const agent = new MockSwarmAgent(id, config);
|
|
147
|
+
this.agents.set(id, agent);
|
|
148
|
+
this.state.agentCount++;
|
|
149
|
+
this.state.activeAgentCount++;
|
|
150
|
+
|
|
151
|
+
if (config.type === 'queen-coordinator' && !this.state.leaderId) {
|
|
152
|
+
this.state.leaderId = id;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return agent;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
removeAgent = vi.fn(async (agentId: string) => {
|
|
159
|
+
const agent = this.agents.get(agentId);
|
|
160
|
+
if (agent) {
|
|
161
|
+
await agent.terminate();
|
|
162
|
+
this.agents.delete(agentId);
|
|
163
|
+
this.state.agentCount--;
|
|
164
|
+
this.state.activeAgentCount--;
|
|
165
|
+
|
|
166
|
+
if (this.state.leaderId === agentId) {
|
|
167
|
+
this.electNewLeader();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
coordinate = vi.fn(async (task: SwarmTask) => {
|
|
173
|
+
this.taskQueue.push(task);
|
|
174
|
+
|
|
175
|
+
// Find suitable agents
|
|
176
|
+
const suitableAgents = Array.from(this.agents.values())
|
|
177
|
+
.filter(agent => agent.canHandle(task.type))
|
|
178
|
+
.sort((a, b) => b.priority - a.priority);
|
|
179
|
+
|
|
180
|
+
if (suitableAgents.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: 'No suitable agents available',
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
duration: 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
const results: TaskResult[] = [];
|
|
191
|
+
|
|
192
|
+
for (const agent of suitableAgents.slice(0, task.maxAgents ?? 1)) {
|
|
193
|
+
const result = await agent.execute(task);
|
|
194
|
+
results.push(result);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
success: results.every(r => r.success),
|
|
199
|
+
taskId: task.id,
|
|
200
|
+
duration: Date.now() - startTime,
|
|
201
|
+
results,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
broadcast = vi.fn(async (message: Omit<SwarmMessage, 'id' | 'timestamp'>) => {
|
|
206
|
+
const fullMessage: SwarmMessage = {
|
|
207
|
+
...message,
|
|
208
|
+
id: `msg-${Date.now()}`,
|
|
209
|
+
timestamp: new Date(),
|
|
210
|
+
to: 'broadcast',
|
|
211
|
+
};
|
|
212
|
+
this.messageQueue.push(fullMessage);
|
|
213
|
+
|
|
214
|
+
for (const agent of this.agents.values()) {
|
|
215
|
+
await agent.receive(fullMessage);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
sendMessage = vi.fn(async (message: SwarmMessage) => {
|
|
220
|
+
this.messageQueue.push(message);
|
|
221
|
+
|
|
222
|
+
if (message.to === 'broadcast') {
|
|
223
|
+
for (const agent of this.agents.values()) {
|
|
224
|
+
await agent.receive(message);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
const agent = this.agents.get(message.to);
|
|
228
|
+
if (agent) {
|
|
229
|
+
await agent.receive(message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
requestConsensus = vi.fn(async <T>(request: ConsensusRequest<T>): Promise<ConsensusResponse<T>> => {
|
|
235
|
+
const voters = request.voters ?? Array.from(this.agents.keys());
|
|
236
|
+
const votes = new Map<string, T>();
|
|
237
|
+
|
|
238
|
+
for (const voterId of voters) {
|
|
239
|
+
const agent = this.agents.get(voterId);
|
|
240
|
+
if (agent) {
|
|
241
|
+
// Simulate voting - random selection
|
|
242
|
+
const vote = request.options[Math.floor(Math.random() * request.options.length)];
|
|
243
|
+
votes.set(voterId, vote);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const voteCounts = new Map<string, number>();
|
|
248
|
+
for (const vote of votes.values()) {
|
|
249
|
+
const key = JSON.stringify(vote);
|
|
250
|
+
voteCounts.set(key, (voteCounts.get(key) ?? 0) + 1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const majority = Math.floor(voters.length / 2) + 1;
|
|
254
|
+
let decision: T | null = null;
|
|
255
|
+
let consensus = false;
|
|
256
|
+
|
|
257
|
+
for (const [key, count] of voteCounts) {
|
|
258
|
+
if (count >= majority) {
|
|
259
|
+
decision = JSON.parse(key);
|
|
260
|
+
consensus = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
topic: request.topic,
|
|
267
|
+
decision,
|
|
268
|
+
votes,
|
|
269
|
+
consensus,
|
|
270
|
+
votingDuration: 100,
|
|
271
|
+
participatingAgents: Array.from(votes.keys()),
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
getState = vi.fn(() => ({ ...this.state }));
|
|
276
|
+
|
|
277
|
+
getAgent = vi.fn((id: string) => this.agents.get(id));
|
|
278
|
+
|
|
279
|
+
getAgents = vi.fn(() => Array.from(this.agents.values()));
|
|
280
|
+
|
|
281
|
+
getMessageQueue = vi.fn(() => [...this.messageQueue]);
|
|
282
|
+
|
|
283
|
+
getTaskQueue = vi.fn(() => [...this.taskQueue]);
|
|
284
|
+
|
|
285
|
+
private electNewLeader(): void {
|
|
286
|
+
const candidates = Array.from(this.agents.values())
|
|
287
|
+
.filter(a => a.config.type === 'queen-coordinator')
|
|
288
|
+
.sort((a, b) => b.priority - a.priority);
|
|
289
|
+
|
|
290
|
+
this.state.leaderId = candidates[0]?.id;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
reset(): void {
|
|
294
|
+
this.agents.clear();
|
|
295
|
+
this.messageQueue = [];
|
|
296
|
+
this.taskQueue = [];
|
|
297
|
+
this.state = {
|
|
298
|
+
id: `swarm-${Date.now()}`,
|
|
299
|
+
topology: 'hierarchical-mesh',
|
|
300
|
+
status: 'idle',
|
|
301
|
+
agentCount: 0,
|
|
302
|
+
activeAgentCount: 0,
|
|
303
|
+
leaderId: undefined,
|
|
304
|
+
createdAt: new Date(),
|
|
305
|
+
};
|
|
306
|
+
vi.clearAllMocks();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Mock Swarm Agent
|
|
312
|
+
*/
|
|
313
|
+
export class MockSwarmAgent {
|
|
314
|
+
readonly id: string;
|
|
315
|
+
readonly config: AgentConfig;
|
|
316
|
+
status: 'idle' | 'busy' | 'terminated' = 'idle';
|
|
317
|
+
priority: number;
|
|
318
|
+
private messages: SwarmMessage[] = [];
|
|
319
|
+
private taskResults: TaskResult[] = [];
|
|
320
|
+
|
|
321
|
+
execute = vi.fn();
|
|
322
|
+
receive = vi.fn();
|
|
323
|
+
send = vi.fn();
|
|
324
|
+
terminate = vi.fn();
|
|
325
|
+
|
|
326
|
+
constructor(id: string, config: AgentConfig) {
|
|
327
|
+
this.id = id;
|
|
328
|
+
this.config = config;
|
|
329
|
+
this.priority = config.priority ?? 50;
|
|
330
|
+
|
|
331
|
+
this.execute.mockImplementation(async (task: SwarmTask) => {
|
|
332
|
+
this.status = 'busy';
|
|
333
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
334
|
+
this.status = 'idle';
|
|
335
|
+
|
|
336
|
+
const result: TaskResult = {
|
|
337
|
+
taskId: task.id,
|
|
338
|
+
agentId: this.id,
|
|
339
|
+
success: true,
|
|
340
|
+
duration: Math.random() * 100 + 10,
|
|
341
|
+
};
|
|
342
|
+
this.taskResults.push(result);
|
|
343
|
+
return result;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
this.receive.mockImplementation(async (message: SwarmMessage) => {
|
|
347
|
+
this.messages.push(message);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.send.mockImplementation(async () => {});
|
|
351
|
+
|
|
352
|
+
this.terminate.mockImplementation(async () => {
|
|
353
|
+
this.status = 'terminated';
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
canHandle(taskType: string): boolean {
|
|
358
|
+
const capabilities = agentCapabilities[this.config.type] ?? [];
|
|
359
|
+
return capabilities.some(cap =>
|
|
360
|
+
cap.includes(taskType) || taskType.includes(cap)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getMessages(): SwarmMessage[] {
|
|
365
|
+
return [...this.messages];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getTaskResults(): TaskResult[] {
|
|
369
|
+
return [...this.taskResults];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Mock Memory Service with caching
|
|
375
|
+
*/
|
|
376
|
+
export class MockMemoryService {
|
|
377
|
+
private store = new Map<string, { value: unknown; metadata: MemoryMetadata; expiresAt?: Date }>();
|
|
378
|
+
private cache = new Map<string, { value: unknown; accessCount: number }>();
|
|
379
|
+
private cacheHits = 0;
|
|
380
|
+
private cacheMisses = 0;
|
|
381
|
+
|
|
382
|
+
set = vi.fn(async (key: string, value: unknown, metadata?: MemoryMetadata) => {
|
|
383
|
+
const expiresAt = metadata?.ttl ? new Date(Date.now() + metadata.ttl) : undefined;
|
|
384
|
+
this.store.set(key, { value, metadata: metadata ?? { type: 'short-term', tags: [] }, expiresAt });
|
|
385
|
+
this.cache.delete(key); // Invalidate cache
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
get = vi.fn(async (key: string) => {
|
|
389
|
+
// Check cache first
|
|
390
|
+
const cached = this.cache.get(key);
|
|
391
|
+
if (cached) {
|
|
392
|
+
this.cacheHits++;
|
|
393
|
+
cached.accessCount++;
|
|
394
|
+
return cached.value;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.cacheMisses++;
|
|
398
|
+
const entry = this.store.get(key);
|
|
399
|
+
|
|
400
|
+
if (!entry) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check expiration
|
|
405
|
+
if (entry.expiresAt && entry.expiresAt < new Date()) {
|
|
406
|
+
this.store.delete(key);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Add to cache
|
|
411
|
+
this.cache.set(key, { value: entry.value, accessCount: 1 });
|
|
412
|
+
|
|
413
|
+
return entry.value;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
delete = vi.fn(async (key: string) => {
|
|
417
|
+
this.store.delete(key);
|
|
418
|
+
this.cache.delete(key);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
search = vi.fn(async (query: VectorSearchQuery) => {
|
|
422
|
+
// Simulate vector search with filtering
|
|
423
|
+
const results: SearchResult[] = [];
|
|
424
|
+
|
|
425
|
+
for (const [key, entry] of this.store) {
|
|
426
|
+
if (query.filters) {
|
|
427
|
+
const matches = Object.entries(query.filters).every(([k, v]) =>
|
|
428
|
+
entry.metadata[k as keyof MemoryMetadata] === v
|
|
429
|
+
);
|
|
430
|
+
if (!matches) continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
results.push({
|
|
434
|
+
key,
|
|
435
|
+
value: entry.value,
|
|
436
|
+
score: Math.random() * 0.3 + 0.7, // Random score 0.7-1.0
|
|
437
|
+
metadata: entry.metadata,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return results
|
|
442
|
+
.filter(r => !query.threshold || r.score >= query.threshold)
|
|
443
|
+
.sort((a, b) => b.score - a.score)
|
|
444
|
+
.slice(0, query.topK);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
clear = vi.fn(async () => {
|
|
448
|
+
this.store.clear();
|
|
449
|
+
this.cache.clear();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
getStats = vi.fn(() => ({
|
|
453
|
+
totalEntries: this.store.size,
|
|
454
|
+
cacheSize: this.cache.size,
|
|
455
|
+
cacheHitRate: this.cacheHits / (this.cacheHits + this.cacheMisses) || 0,
|
|
456
|
+
cacheHits: this.cacheHits,
|
|
457
|
+
cacheMisses: this.cacheMisses,
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
prune = vi.fn(async () => {
|
|
461
|
+
const now = new Date();
|
|
462
|
+
let pruned = 0;
|
|
463
|
+
|
|
464
|
+
for (const [key, entry] of this.store) {
|
|
465
|
+
if (entry.expiresAt && entry.expiresAt < now) {
|
|
466
|
+
this.store.delete(key);
|
|
467
|
+
this.cache.delete(key);
|
|
468
|
+
pruned++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return pruned;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
reset(): void {
|
|
476
|
+
this.store.clear();
|
|
477
|
+
this.cache.clear();
|
|
478
|
+
this.cacheHits = 0;
|
|
479
|
+
this.cacheMisses = 0;
|
|
480
|
+
vi.clearAllMocks();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Mock Event Bus with history tracking
|
|
486
|
+
*/
|
|
487
|
+
export class MockEventBus {
|
|
488
|
+
private subscribers = new Map<string, Set<EventHandler>>();
|
|
489
|
+
private history: DomainEvent[] = [];
|
|
490
|
+
private maxHistorySize = 1000;
|
|
491
|
+
|
|
492
|
+
publish = vi.fn(async (event: DomainEvent) => {
|
|
493
|
+
this.history.push(event);
|
|
494
|
+
if (this.history.length > this.maxHistorySize) {
|
|
495
|
+
this.history.shift();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const handlers = this.subscribers.get(event.type) ?? new Set();
|
|
499
|
+
const wildcardHandlers = this.subscribers.get('*') ?? new Set();
|
|
500
|
+
|
|
501
|
+
const allHandlers = [...handlers, ...wildcardHandlers];
|
|
502
|
+
|
|
503
|
+
await Promise.all(allHandlers.map(handler => handler(event)));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
subscribe = vi.fn((eventType: string, handler: EventHandler) => {
|
|
507
|
+
if (!this.subscribers.has(eventType)) {
|
|
508
|
+
this.subscribers.set(eventType, new Set());
|
|
509
|
+
}
|
|
510
|
+
this.subscribers.get(eventType)!.add(handler);
|
|
511
|
+
|
|
512
|
+
return () => this.unsubscribe(eventType, handler);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
unsubscribe = vi.fn((eventType: string, handler: EventHandler) => {
|
|
516
|
+
this.subscribers.get(eventType)?.delete(handler);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
getHistory(eventType?: string): DomainEvent[] {
|
|
520
|
+
if (eventType) {
|
|
521
|
+
return this.history.filter(e => e.type === eventType);
|
|
522
|
+
}
|
|
523
|
+
return [...this.history];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
getSubscriberCount(eventType: string): number {
|
|
527
|
+
return this.subscribers.get(eventType)?.size ?? 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
clear(): void {
|
|
531
|
+
this.history = [];
|
|
532
|
+
vi.clearAllMocks();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
reset(): void {
|
|
536
|
+
this.subscribers.clear();
|
|
537
|
+
this.history = [];
|
|
538
|
+
vi.clearAllMocks();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Mock Security Service
|
|
544
|
+
*/
|
|
545
|
+
export class MockSecurityService {
|
|
546
|
+
private blockedPaths = ['../', '~/', '/etc/', '/tmp/', '/var/', '/root/'];
|
|
547
|
+
private allowedCommands = ['npm', 'npx', 'node', 'git'];
|
|
548
|
+
private tokens = new Map<string, { payload: Record<string, unknown>; expiresAt: Date }>();
|
|
549
|
+
|
|
550
|
+
validatePath = vi.fn((path: string) => {
|
|
551
|
+
return !this.blockedPaths.some(blocked => path.includes(blocked));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
validateInput = vi.fn((input: string, options?: InputValidationOptions) => {
|
|
555
|
+
const errors: string[] = [];
|
|
556
|
+
|
|
557
|
+
if (options?.maxLength && input.length > options.maxLength) {
|
|
558
|
+
errors.push(`Input exceeds maximum length of ${options.maxLength}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (options?.allowedChars && !options.allowedChars.test(input)) {
|
|
562
|
+
errors.push('Input contains disallowed characters');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
valid: errors.length === 0,
|
|
567
|
+
sanitized: options?.sanitize ? this.sanitize(input) : input,
|
|
568
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
hashPassword = vi.fn(async (password: string) => {
|
|
573
|
+
// Simulate argon2 hash format
|
|
574
|
+
return `$argon2id$v=19$m=65536,t=3,p=4$${Buffer.from(password).toString('base64')}`;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
verifyPassword = vi.fn(async (password: string, hash: string) => {
|
|
578
|
+
const parts = hash.split('$');
|
|
579
|
+
if (parts.length < 5) return false;
|
|
580
|
+
return Buffer.from(parts[4], 'base64').toString() === password;
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
generateToken = vi.fn(async (payload: Record<string, unknown>, expiresIn: number = 3600000) => {
|
|
584
|
+
const token = `token_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
585
|
+
this.tokens.set(token, {
|
|
586
|
+
payload,
|
|
587
|
+
expiresAt: new Date(Date.now() + expiresIn),
|
|
588
|
+
});
|
|
589
|
+
return token;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
verifyToken = vi.fn(async (token: string) => {
|
|
593
|
+
const entry = this.tokens.get(token);
|
|
594
|
+
if (!entry) {
|
|
595
|
+
throw new Error('Invalid token');
|
|
596
|
+
}
|
|
597
|
+
if (entry.expiresAt < new Date()) {
|
|
598
|
+
this.tokens.delete(token);
|
|
599
|
+
throw new Error('Token expired');
|
|
600
|
+
}
|
|
601
|
+
return entry.payload;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
executeSecurely = vi.fn(async (command: string, options?: ExecuteOptions) => {
|
|
605
|
+
const [cmd] = command.split(' ');
|
|
606
|
+
|
|
607
|
+
if (!this.allowedCommands.includes(cmd)) {
|
|
608
|
+
throw new Error(`Command not allowed: ${cmd}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
stdout: '',
|
|
613
|
+
stderr: '',
|
|
614
|
+
exitCode: 0,
|
|
615
|
+
duration: Math.random() * 100,
|
|
616
|
+
};
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
private sanitize(input: string): string {
|
|
620
|
+
return input
|
|
621
|
+
.replace(/</g, '<')
|
|
622
|
+
.replace(/>/g, '>')
|
|
623
|
+
.replace(/"/g, '"')
|
|
624
|
+
.replace(/'/g, ''');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
reset(): void {
|
|
628
|
+
this.tokens.clear();
|
|
629
|
+
vi.clearAllMocks();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Supporting types
|
|
634
|
+
interface SwarmState {
|
|
635
|
+
id: string;
|
|
636
|
+
topology: string;
|
|
637
|
+
status: string;
|
|
638
|
+
agentCount: number;
|
|
639
|
+
activeAgentCount: number;
|
|
640
|
+
leaderId?: string;
|
|
641
|
+
createdAt: Date;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
interface SwarmInitConfig {
|
|
645
|
+
topology?: string;
|
|
646
|
+
maxAgents?: number;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
interface AgentConfig {
|
|
650
|
+
type: V3AgentType;
|
|
651
|
+
name?: string;
|
|
652
|
+
capabilities?: string[];
|
|
653
|
+
priority?: number;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
interface SwarmTask {
|
|
657
|
+
id: string;
|
|
658
|
+
type: string;
|
|
659
|
+
payload: unknown;
|
|
660
|
+
priority?: number;
|
|
661
|
+
maxAgents?: number;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
interface SwarmMessage {
|
|
665
|
+
id: string;
|
|
666
|
+
from: string;
|
|
667
|
+
to: string;
|
|
668
|
+
type: string;
|
|
669
|
+
payload: unknown;
|
|
670
|
+
timestamp: Date;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
interface TaskResult {
|
|
674
|
+
taskId: string;
|
|
675
|
+
agentId: string;
|
|
676
|
+
success: boolean;
|
|
677
|
+
output?: unknown;
|
|
678
|
+
error?: Error;
|
|
679
|
+
duration: number;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
interface ConsensusRequest<T> {
|
|
683
|
+
topic: string;
|
|
684
|
+
options: T[];
|
|
685
|
+
voters?: string[];
|
|
686
|
+
timeout?: number;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
interface ConsensusResponse<T> {
|
|
690
|
+
topic: string;
|
|
691
|
+
decision: T | null;
|
|
692
|
+
votes: Map<string, T>;
|
|
693
|
+
consensus: boolean;
|
|
694
|
+
votingDuration: number;
|
|
695
|
+
participatingAgents: string[];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
interface MemoryMetadata {
|
|
699
|
+
type: 'short-term' | 'long-term' | 'semantic' | 'episodic';
|
|
700
|
+
tags: string[];
|
|
701
|
+
ttl?: number;
|
|
702
|
+
[key: string]: unknown;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
interface VectorSearchQuery {
|
|
706
|
+
embedding?: number[];
|
|
707
|
+
topK: number;
|
|
708
|
+
threshold?: number;
|
|
709
|
+
filters?: Record<string, unknown>;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
interface SearchResult {
|
|
713
|
+
key: string;
|
|
714
|
+
value: unknown;
|
|
715
|
+
score: number;
|
|
716
|
+
metadata: MemoryMetadata;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
interface DomainEvent {
|
|
720
|
+
id: string;
|
|
721
|
+
type: string;
|
|
722
|
+
payload: unknown;
|
|
723
|
+
timestamp: Date;
|
|
724
|
+
correlationId?: string;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
type EventHandler = (event: DomainEvent) => Promise<void>;
|
|
728
|
+
|
|
729
|
+
interface InputValidationOptions {
|
|
730
|
+
maxLength?: number;
|
|
731
|
+
allowedChars?: RegExp;
|
|
732
|
+
sanitize?: boolean;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
interface ExecuteOptions {
|
|
736
|
+
timeout?: number;
|
|
737
|
+
cwd?: string;
|
|
738
|
+
shell?: boolean;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Agent capabilities mapping
|
|
742
|
+
const agentCapabilities: Record<V3AgentType, string[]> = {
|
|
743
|
+
'queen-coordinator': ['orchestration', 'coordination', 'task-distribution'],
|
|
744
|
+
'security-architect': ['security', 'design', 'threat-modeling'],
|
|
745
|
+
'security-auditor': ['security', 'audit', 'vulnerability'],
|
|
746
|
+
'memory-specialist': ['memory', 'optimization', 'caching'],
|
|
747
|
+
'swarm-specialist': ['coordination', 'consensus', 'communication'],
|
|
748
|
+
'integration-architect': ['integration', 'api', 'compatibility'],
|
|
749
|
+
'performance-engineer': ['performance', 'optimization', 'benchmarking'],
|
|
750
|
+
'core-architect': ['architecture', 'design', 'domain'],
|
|
751
|
+
'test-architect': ['testing', 'tdd', 'quality'],
|
|
752
|
+
'project-coordinator': ['project', 'planning', 'scheduling'],
|
|
753
|
+
'coder': ['coding', 'implementation', 'debugging'],
|
|
754
|
+
'reviewer': ['review', 'quality', 'suggestions'],
|
|
755
|
+
'tester': ['testing', 'execution', 'coverage'],
|
|
756
|
+
'planner': ['planning', 'estimation', 'roadmap'],
|
|
757
|
+
'researcher': ['research', 'analysis', 'documentation'],
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Create all mock services as a bundle
|
|
762
|
+
*/
|
|
763
|
+
export function createMockServices(): MockServiceBundle {
|
|
764
|
+
return {
|
|
765
|
+
agentDB: new MockAgentDB(),
|
|
766
|
+
swarmCoordinator: new MockSwarmCoordinator(),
|
|
767
|
+
memoryService: new MockMemoryService(),
|
|
768
|
+
eventBus: new MockEventBus(),
|
|
769
|
+
securityService: new MockSecurityService(),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Mock service bundle interface
|
|
775
|
+
*/
|
|
776
|
+
export interface MockServiceBundle {
|
|
777
|
+
agentDB: MockAgentDB;
|
|
778
|
+
swarmCoordinator: MockSwarmCoordinator;
|
|
779
|
+
memoryService: MockMemoryService;
|
|
780
|
+
eventBus: MockEventBus;
|
|
781
|
+
securityService: MockSecurityService;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Reset all mock services
|
|
786
|
+
*/
|
|
787
|
+
export function resetMockServices(services: MockServiceBundle): void {
|
|
788
|
+
services.agentDB.clear();
|
|
789
|
+
services.swarmCoordinator.reset();
|
|
790
|
+
services.memoryService.reset();
|
|
791
|
+
services.eventBus.reset();
|
|
792
|
+
services.securityService.reset();
|
|
793
|
+
}
|