@sparkleideas/testing 3.0.0-alpha.10
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,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/testing - Setup & Teardown Helpers
|
|
3
|
+
*
|
|
4
|
+
* Global setup and teardown utilities for V3 module testing.
|
|
5
|
+
* Provides test isolation, resource cleanup, and environment management.
|
|
6
|
+
*/
|
|
7
|
+
import { vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Setup context for managing test resources
|
|
11
|
+
*/
|
|
12
|
+
export interface SetupContext {
|
|
13
|
+
/**
|
|
14
|
+
* Register a cleanup function to be called during teardown
|
|
15
|
+
*/
|
|
16
|
+
addCleanup(cleanup: CleanupFunction): void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a resource that needs to be closed/disposed
|
|
20
|
+
*/
|
|
21
|
+
registerResource<T extends Disposable>(resource: T): T;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get a registered resource by name
|
|
25
|
+
*/
|
|
26
|
+
getResource<T>(name: string): T | undefined;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set a named resource
|
|
30
|
+
*/
|
|
31
|
+
setResource<T>(name: string, resource: T): void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run all cleanup functions
|
|
35
|
+
*/
|
|
36
|
+
runCleanup(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cleanup function type
|
|
41
|
+
*/
|
|
42
|
+
export type CleanupFunction = () => void | Promise<void>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Disposable interface
|
|
46
|
+
*/
|
|
47
|
+
export interface Disposable {
|
|
48
|
+
dispose?(): void | Promise<void>;
|
|
49
|
+
close?(): void | Promise<void>;
|
|
50
|
+
destroy?(): void | Promise<void>;
|
|
51
|
+
shutdown?(): void | Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a setup context for managing test resources
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const ctx = createSetupContext();
|
|
59
|
+
* ctx.addCleanup(() => server.close());
|
|
60
|
+
* ctx.registerResource(database);
|
|
61
|
+
* // ... run tests
|
|
62
|
+
* await ctx.runCleanup();
|
|
63
|
+
*/
|
|
64
|
+
export function createSetupContext(): SetupContext {
|
|
65
|
+
const cleanups: CleanupFunction[] = [];
|
|
66
|
+
const resources = new Map<string, unknown>();
|
|
67
|
+
const disposables: Disposable[] = [];
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
addCleanup(cleanup: CleanupFunction): void {
|
|
71
|
+
cleanups.push(cleanup);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
registerResource<T extends Disposable>(resource: T): T {
|
|
75
|
+
disposables.push(resource);
|
|
76
|
+
return resource;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
getResource<T>(name: string): T | undefined {
|
|
80
|
+
return resources.get(name) as T | undefined;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
setResource<T>(name: string, resource: T): void {
|
|
84
|
+
resources.set(name, resource);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async runCleanup(): Promise<void> {
|
|
88
|
+
// Run cleanups in reverse order
|
|
89
|
+
for (const cleanup of cleanups.reverse()) {
|
|
90
|
+
try {
|
|
91
|
+
await cleanup();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Cleanup error:', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Dispose resources
|
|
98
|
+
for (const resource of disposables) {
|
|
99
|
+
try {
|
|
100
|
+
if (resource.dispose) {
|
|
101
|
+
await resource.dispose();
|
|
102
|
+
} else if (resource.close) {
|
|
103
|
+
await resource.close();
|
|
104
|
+
} else if (resource.destroy) {
|
|
105
|
+
await resource.destroy();
|
|
106
|
+
} else if (resource.shutdown) {
|
|
107
|
+
await resource.shutdown();
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Resource disposal error:', error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cleanups.length = 0;
|
|
115
|
+
resources.clear();
|
|
116
|
+
disposables.length = 0;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Global test context that persists across test files
|
|
123
|
+
*/
|
|
124
|
+
let globalContext: SetupContext | null = null;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get or create the global test context
|
|
128
|
+
*/
|
|
129
|
+
export function getGlobalContext(): SetupContext {
|
|
130
|
+
if (!globalContext) {
|
|
131
|
+
globalContext = createSetupContext();
|
|
132
|
+
}
|
|
133
|
+
return globalContext;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reset the global test context
|
|
138
|
+
*/
|
|
139
|
+
export async function resetGlobalContext(): Promise<void> {
|
|
140
|
+
if (globalContext) {
|
|
141
|
+
await globalContext.runCleanup();
|
|
142
|
+
globalContext = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Test environment configuration
|
|
148
|
+
*/
|
|
149
|
+
export interface TestEnvironmentConfig {
|
|
150
|
+
/**
|
|
151
|
+
* Reset all mocks before each test
|
|
152
|
+
*/
|
|
153
|
+
resetMocks?: boolean;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Use fake timers
|
|
157
|
+
*/
|
|
158
|
+
fakeTimers?: boolean;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Initial fake time
|
|
162
|
+
*/
|
|
163
|
+
initialTime?: Date | number;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Environment variables to set
|
|
167
|
+
*/
|
|
168
|
+
env?: Record<string, string>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Suppress console output during tests
|
|
172
|
+
*/
|
|
173
|
+
suppressConsole?: boolean | ('log' | 'warn' | 'error' | 'info')[];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Timeout for async operations
|
|
177
|
+
*/
|
|
178
|
+
timeout?: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Configure test environment with standard settings
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* configureTestEnvironment({
|
|
186
|
+
* resetMocks: true,
|
|
187
|
+
* fakeTimers: true,
|
|
188
|
+
* suppressConsole: ['log', 'warn'],
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
export function configureTestEnvironment(config: TestEnvironmentConfig = {}): void {
|
|
192
|
+
const {
|
|
193
|
+
resetMocks = true,
|
|
194
|
+
fakeTimers = false,
|
|
195
|
+
initialTime,
|
|
196
|
+
env = {},
|
|
197
|
+
suppressConsole = false,
|
|
198
|
+
} = config;
|
|
199
|
+
|
|
200
|
+
const originalEnv: Record<string, string | undefined> = {};
|
|
201
|
+
const originalConsole: Partial<Console> = {};
|
|
202
|
+
|
|
203
|
+
beforeAll(() => {
|
|
204
|
+
// Set environment variables
|
|
205
|
+
for (const [key, value] of Object.entries(env)) {
|
|
206
|
+
originalEnv[key] = process.env[key];
|
|
207
|
+
process.env[key] = value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Suppress console
|
|
211
|
+
if (suppressConsole) {
|
|
212
|
+
const methods = suppressConsole === true
|
|
213
|
+
? ['log', 'warn', 'error', 'info'] as const
|
|
214
|
+
: suppressConsole;
|
|
215
|
+
|
|
216
|
+
for (const method of methods) {
|
|
217
|
+
originalConsole[method] = console[method];
|
|
218
|
+
console[method] = vi.fn();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterAll(() => {
|
|
224
|
+
// Restore environment variables
|
|
225
|
+
for (const [key, value] of Object.entries(originalEnv)) {
|
|
226
|
+
if (value === undefined) {
|
|
227
|
+
delete process.env[key];
|
|
228
|
+
} else {
|
|
229
|
+
process.env[key] = value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Restore console
|
|
234
|
+
for (const [method, original] of Object.entries(originalConsole)) {
|
|
235
|
+
if (original) {
|
|
236
|
+
(console as unknown as Record<string, unknown>)[method] = original;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
if (resetMocks) {
|
|
243
|
+
vi.clearAllMocks();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (fakeTimers) {
|
|
247
|
+
vi.useFakeTimers();
|
|
248
|
+
if (initialTime) {
|
|
249
|
+
vi.setSystemTime(initialTime);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
afterEach(() => {
|
|
255
|
+
if (fakeTimers) {
|
|
256
|
+
vi.useRealTimers();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
vi.restoreAllMocks();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create a test suite with automatic setup/teardown
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const { beforeEachTest, afterEachTest, getContext } = createTestSuite({
|
|
268
|
+
* resetMocks: true,
|
|
269
|
+
* });
|
|
270
|
+
*/
|
|
271
|
+
export function createTestSuite(config: TestEnvironmentConfig = {}): TestSuiteHelpers {
|
|
272
|
+
const context = createSetupContext();
|
|
273
|
+
|
|
274
|
+
configureTestEnvironment(config);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
beforeEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => {
|
|
278
|
+
beforeEach(async () => {
|
|
279
|
+
await fn(context);
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
afterEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => {
|
|
284
|
+
afterEach(async () => {
|
|
285
|
+
await fn(context);
|
|
286
|
+
await context.runCleanup();
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
getContext: () => context,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Test suite helpers interface
|
|
296
|
+
*/
|
|
297
|
+
export interface TestSuiteHelpers {
|
|
298
|
+
beforeEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => void;
|
|
299
|
+
afterEachTest: (fn: (ctx: SetupContext) => void | Promise<void>) => void;
|
|
300
|
+
getContext: () => SetupContext;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create isolated test scope
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* const scope = createTestScope();
|
|
308
|
+
* scope.addMock(mockService);
|
|
309
|
+
* await scope.run(async () => {
|
|
310
|
+
* // test code
|
|
311
|
+
* });
|
|
312
|
+
*/
|
|
313
|
+
export function createTestScope(): TestScope {
|
|
314
|
+
const mocks: ReturnType<typeof vi.fn>[] = [];
|
|
315
|
+
const cleanups: CleanupFunction[] = [];
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
addMock<T extends ReturnType<typeof vi.fn>>(mock: T): T {
|
|
319
|
+
mocks.push(mock);
|
|
320
|
+
return mock;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
addCleanup(cleanup: CleanupFunction): void {
|
|
324
|
+
cleanups.push(cleanup);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
328
|
+
try {
|
|
329
|
+
return await fn();
|
|
330
|
+
} finally {
|
|
331
|
+
// Clear all mocks
|
|
332
|
+
for (const mock of mocks) {
|
|
333
|
+
mock.mockClear();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Run cleanups
|
|
337
|
+
for (const cleanup of cleanups.reverse()) {
|
|
338
|
+
await cleanup();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
clear(): void {
|
|
344
|
+
for (const mock of mocks) {
|
|
345
|
+
mock.mockClear();
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
reset(): void {
|
|
350
|
+
for (const mock of mocks) {
|
|
351
|
+
mock.mockReset();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Test scope interface
|
|
359
|
+
*/
|
|
360
|
+
export interface TestScope {
|
|
361
|
+
addMock<T extends ReturnType<typeof vi.fn>>(mock: T): T;
|
|
362
|
+
addCleanup(cleanup: CleanupFunction): void;
|
|
363
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
364
|
+
clear(): void;
|
|
365
|
+
reset(): void;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Database test helper for memory/agentdb testing
|
|
370
|
+
*/
|
|
371
|
+
export interface DatabaseTestHelper {
|
|
372
|
+
setup(): Promise<void>;
|
|
373
|
+
teardown(): Promise<void>;
|
|
374
|
+
clear(): Promise<void>;
|
|
375
|
+
seed(data: Record<string, unknown[]>): Promise<void>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create in-memory database helper for testing
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* const db = createInMemoryDatabaseHelper();
|
|
383
|
+
* await db.setup();
|
|
384
|
+
* await db.seed({ users: [{ id: 1, name: 'Test' }] });
|
|
385
|
+
* // ... run tests
|
|
386
|
+
* await db.teardown();
|
|
387
|
+
*/
|
|
388
|
+
export function createInMemoryDatabaseHelper(): DatabaseTestHelper {
|
|
389
|
+
const data = new Map<string, unknown[]>();
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
async setup(): Promise<void> {
|
|
393
|
+
data.clear();
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
async teardown(): Promise<void> {
|
|
397
|
+
data.clear();
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
async clear(): Promise<void> {
|
|
401
|
+
data.clear();
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
async seed(seedData: Record<string, unknown[]>): Promise<void> {
|
|
405
|
+
for (const [table, records] of Object.entries(seedData)) {
|
|
406
|
+
data.set(table, [...records]);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Network test helper for mocking HTTP/WebSocket
|
|
414
|
+
*/
|
|
415
|
+
export interface NetworkTestHelper {
|
|
416
|
+
mockFetch(responses: MockFetchResponse[]): void;
|
|
417
|
+
mockWebSocket(handler: (message: unknown) => unknown): void;
|
|
418
|
+
clearMocks(): void;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Mock fetch response
|
|
423
|
+
*/
|
|
424
|
+
export interface MockFetchResponse {
|
|
425
|
+
url: string | RegExp;
|
|
426
|
+
method?: string;
|
|
427
|
+
status?: number;
|
|
428
|
+
body?: unknown;
|
|
429
|
+
headers?: Record<string, string>;
|
|
430
|
+
delay?: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Create network test helper
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* const network = createNetworkTestHelper();
|
|
438
|
+
* network.mockFetch([
|
|
439
|
+
* { url: '/api/users', body: [{ id: 1 }] },
|
|
440
|
+
* ]);
|
|
441
|
+
*/
|
|
442
|
+
export function createNetworkTestHelper(): NetworkTestHelper {
|
|
443
|
+
const fetchResponses: MockFetchResponse[] = [];
|
|
444
|
+
let originalFetch: typeof global.fetch;
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
mockFetch(responses: MockFetchResponse[]): void {
|
|
448
|
+
fetchResponses.push(...responses);
|
|
449
|
+
|
|
450
|
+
if (!originalFetch) {
|
|
451
|
+
originalFetch = global.fetch;
|
|
452
|
+
|
|
453
|
+
global.fetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
454
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
455
|
+
const method = init?.method ?? 'GET';
|
|
456
|
+
|
|
457
|
+
const match = fetchResponses.find(r => {
|
|
458
|
+
const urlMatch = typeof r.url === 'string'
|
|
459
|
+
? url.includes(r.url)
|
|
460
|
+
: r.url.test(url);
|
|
461
|
+
const methodMatch = !r.method || r.method === method;
|
|
462
|
+
return urlMatch && methodMatch;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (!match) {
|
|
466
|
+
throw new Error(`No mock found for ${method} ${url}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (match.delay) {
|
|
470
|
+
await new Promise(resolve => setTimeout(resolve, match.delay));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return new Response(JSON.stringify(match.body), {
|
|
474
|
+
status: match.status ?? 200,
|
|
475
|
+
headers: match.headers ?? { 'Content-Type': 'application/json' },
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
mockWebSocket(handler: (message: unknown) => unknown): void {
|
|
482
|
+
// WebSocket mocking would require more setup
|
|
483
|
+
// This is a placeholder for the interface
|
|
484
|
+
console.warn('WebSocket mocking not yet implemented');
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
clearMocks(): void {
|
|
488
|
+
fetchResponses.length = 0;
|
|
489
|
+
if (originalFetch) {
|
|
490
|
+
global.fetch = originalFetch;
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* File system test helper
|
|
498
|
+
*/
|
|
499
|
+
export interface FileSystemTestHelper {
|
|
500
|
+
createTempDir(): Promise<string>;
|
|
501
|
+
createFile(path: string, content: string): Promise<void>;
|
|
502
|
+
readFile(path: string): Promise<string>;
|
|
503
|
+
cleanup(): Promise<void>;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Create in-memory file system helper
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* const fs = createInMemoryFileSystemHelper();
|
|
511
|
+
* await fs.createFile('/test.txt', 'content');
|
|
512
|
+
* const content = await fs.readFile('/test.txt');
|
|
513
|
+
*/
|
|
514
|
+
export function createInMemoryFileSystemHelper(): FileSystemTestHelper {
|
|
515
|
+
const files = new Map<string, string>();
|
|
516
|
+
const tempDirs: string[] = [];
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
async createTempDir(): Promise<string> {
|
|
520
|
+
const dir = `/tmp/test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
521
|
+
tempDirs.push(dir);
|
|
522
|
+
return dir;
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
async createFile(path: string, content: string): Promise<void> {
|
|
526
|
+
files.set(path, content);
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
async readFile(path: string): Promise<string> {
|
|
530
|
+
const content = files.get(path);
|
|
531
|
+
if (content === undefined) {
|
|
532
|
+
throw new Error(`File not found: ${path}`);
|
|
533
|
+
}
|
|
534
|
+
return content;
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
async cleanup(): Promise<void> {
|
|
538
|
+
files.clear();
|
|
539
|
+
tempDirs.length = 0;
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Performance test helper
|
|
546
|
+
*/
|
|
547
|
+
export interface PerformanceTestHelper {
|
|
548
|
+
startMeasurement(name: string): void;
|
|
549
|
+
endMeasurement(name: string): number;
|
|
550
|
+
getMeasurements(): Record<string, number[]>;
|
|
551
|
+
getStats(name: string): { min: number; max: number; avg: number; p95: number };
|
|
552
|
+
clear(): void;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Create performance test helper
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* const perf = createPerformanceTestHelper();
|
|
560
|
+
* perf.startMeasurement('search');
|
|
561
|
+
* await search();
|
|
562
|
+
* const duration = perf.endMeasurement('search');
|
|
563
|
+
*/
|
|
564
|
+
export function createPerformanceTestHelper(): PerformanceTestHelper {
|
|
565
|
+
const measurements = new Map<string, number[]>();
|
|
566
|
+
const starts = new Map<string, number>();
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
startMeasurement(name: string): void {
|
|
570
|
+
starts.set(name, performance.now());
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
endMeasurement(name: string): number {
|
|
574
|
+
const start = starts.get(name);
|
|
575
|
+
if (start === undefined) {
|
|
576
|
+
throw new Error(`No measurement started for: ${name}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const duration = performance.now() - start;
|
|
580
|
+
starts.delete(name);
|
|
581
|
+
|
|
582
|
+
if (!measurements.has(name)) {
|
|
583
|
+
measurements.set(name, []);
|
|
584
|
+
}
|
|
585
|
+
measurements.get(name)!.push(duration);
|
|
586
|
+
|
|
587
|
+
return duration;
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
getMeasurements(): Record<string, number[]> {
|
|
591
|
+
return Object.fromEntries(measurements);
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
getStats(name: string): { min: number; max: number; avg: number; p95: number } {
|
|
595
|
+
const values = measurements.get(name);
|
|
596
|
+
if (!values || values.length === 0) {
|
|
597
|
+
return { min: 0, max: 0, avg: 0, p95: 0 };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
601
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
min: sorted[0],
|
|
605
|
+
max: sorted[sorted.length - 1],
|
|
606
|
+
avg: sum / sorted.length,
|
|
607
|
+
p95: sorted[Math.floor(sorted.length * 0.95)],
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
clear(): void {
|
|
612
|
+
measurements.clear();
|
|
613
|
+
starts.clear();
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Standard V3 test setup
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* // In your test file:
|
|
623
|
+
* setupV3Tests();
|
|
624
|
+
*
|
|
625
|
+
* describe('MyModule', () => {
|
|
626
|
+
* // tests...
|
|
627
|
+
* });
|
|
628
|
+
*/
|
|
629
|
+
export function setupV3Tests(config: V3TestConfig = {}): void {
|
|
630
|
+
configureTestEnvironment({
|
|
631
|
+
resetMocks: true,
|
|
632
|
+
suppressConsole: config.suppressConsole ?? false,
|
|
633
|
+
env: {
|
|
634
|
+
NODE_ENV: 'test',
|
|
635
|
+
CLAUDE_FLOW_MODE: 'test',
|
|
636
|
+
...config.env,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* V3 test configuration
|
|
643
|
+
*/
|
|
644
|
+
export interface V3TestConfig {
|
|
645
|
+
suppressConsole?: boolean | ('log' | 'warn' | 'error' | 'info')[];
|
|
646
|
+
env?: Record<string, string>;
|
|
647
|
+
timeout?: number;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Wait for all pending promises to resolve
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* await flushPromises();
|
|
655
|
+
*/
|
|
656
|
+
export function flushPromises(): Promise<void> {
|
|
657
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Run with timeout
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* await withTimeout(async () => {
|
|
665
|
+
* await longRunningOperation();
|
|
666
|
+
* }, 5000);
|
|
667
|
+
*/
|
|
668
|
+
export async function withTestTimeout<T>(
|
|
669
|
+
fn: () => Promise<T>,
|
|
670
|
+
timeoutMs: number = 5000
|
|
671
|
+
): Promise<T> {
|
|
672
|
+
return Promise.race([
|
|
673
|
+
fn(),
|
|
674
|
+
new Promise<never>((_, reject) =>
|
|
675
|
+
setTimeout(() => reject(new Error(`Test timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
676
|
+
),
|
|
677
|
+
]);
|
|
678
|
+
}
|