@kb-labs/shared-testing 1.0.0

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.
@@ -0,0 +1,545 @@
1
+ import { platform } from '@kb-labs/core-runtime';
2
+ import { ILLM, ICache, IEmbeddings, IVectorStore, IStorage, IAnalytics, ILogger, IEventBus, LLMOptions, LLMResponse, LLMMessage, LLMToolCallOptions, LLMToolCallResponse, LLMToolCall } from '@kb-labs/core-platform';
3
+ import { HostContext, PlatformServices, UIFacade, PluginContextV3, PluginAPI, EnvironmentAPI, WorkspaceAPI, SnapshotAPI, RuntimeAPI, TraceContext } from '@kb-labs/plugin-contracts';
4
+
5
+ /**
6
+ * @module @kb-labs/shared-testing/setup-platform
7
+ *
8
+ * Solves the "singleton gap" problem: composables (useLLM, useCache, etc.)
9
+ * read from the global platform singleton, but createTestContext() only
10
+ * populates ctx.platform. This module bridges the gap by setting test mocks
11
+ * directly into the global singleton.
12
+ */
13
+
14
+ /**
15
+ * Options for setting up the test platform.
16
+ * Only adapters that are provided will be registered.
17
+ * Omitted adapters will use the PlatformContainer's built-in fallbacks.
18
+ */
19
+ interface TestPlatformOptions {
20
+ llm?: ILLM;
21
+ cache?: ICache;
22
+ embeddings?: IEmbeddings;
23
+ vectorStore?: IVectorStore;
24
+ storage?: IStorage;
25
+ analytics?: IAnalytics;
26
+ logger?: ILogger;
27
+ eventBus?: IEventBus;
28
+ }
29
+ interface TestPlatformResult {
30
+ /** The global platform singleton (with test mocks applied) */
31
+ platform: typeof platform;
32
+ /** Call in afterEach() to reset the singleton to a clean state */
33
+ cleanup: () => void;
34
+ }
35
+ /**
36
+ * Setup the global platform singleton with test mocks.
37
+ *
38
+ * This ensures that useLLM(), useCache() and other composables
39
+ * return the test mocks instead of uninitialized/stale adapters.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import { setupTestPlatform, mockLLM } from '@kb-labs/shared-testing';
44
+ *
45
+ * describe('my handler', () => {
46
+ * let cleanup: () => void;
47
+ *
48
+ * beforeEach(() => {
49
+ * const result = setupTestPlatform({
50
+ * llm: mockLLM().onAnyComplete().respondWith('hello'),
51
+ * });
52
+ * cleanup = result.cleanup;
53
+ * });
54
+ *
55
+ * afterEach(() => cleanup());
56
+ *
57
+ * it('uses LLM', async () => {
58
+ * const llm = useLLM(); // Returns the test mock!
59
+ * const res = await llm!.complete('test');
60
+ * expect(res.content).toBe('hello');
61
+ * });
62
+ * });
63
+ * ```
64
+ */
65
+ declare function setupTestPlatform(options?: TestPlatformOptions): TestPlatformResult;
66
+
67
+ /**
68
+ * @module @kb-labs/shared-testing/mock-llm
69
+ *
70
+ * LLM mock builder with fluent API and call tracking.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const llm = mockLLM()
75
+ * .onComplete('Generate commit').respondWith('feat: add login')
76
+ * .onComplete(/explain/i).respondWith('This function does X')
77
+ * .onAnyComplete().respondWith('default answer');
78
+ *
79
+ * const res = await llm.complete('Generate commit message');
80
+ * expect(res.content).toBe('feat: add login');
81
+ * expect(llm.complete).toHaveBeenCalledOnce();
82
+ * ```
83
+ */
84
+
85
+ /** Recorded call to complete() */
86
+ interface LLMCall {
87
+ prompt: string;
88
+ options?: LLMOptions;
89
+ response: LLMResponse;
90
+ }
91
+ /** Recorded call to chatWithTools() */
92
+ interface LLMToolCallRecord {
93
+ messages: LLMMessage[];
94
+ options: LLMToolCallOptions;
95
+ response: LLMToolCallResponse;
96
+ }
97
+ type PromptMatcher = string | RegExp | ((prompt: string) => boolean);
98
+ /**
99
+ * Mock LLM instance with call tracking and fluent API.
100
+ * All methods are vi.fn() spies.
101
+ */
102
+ interface MockLLMInstance extends ILLM {
103
+ /** All recorded complete() calls */
104
+ calls: LLMCall[];
105
+ /** Last complete() call (or undefined) */
106
+ lastCall: LLMCall | undefined;
107
+ /** All recorded chatWithTools() calls */
108
+ toolCalls: LLMToolCallRecord[];
109
+ /** Reset all recorded calls and spies */
110
+ resetCalls: () => void;
111
+ }
112
+ /** Public type returned by mockLLM() — builder fluent API + ILLM spy instance */
113
+ interface MockLLM extends MockLLMInstance {
114
+ onComplete(matcher: PromptMatcher): {
115
+ respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLM;
116
+ };
117
+ onAnyComplete(): {
118
+ respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLM;
119
+ };
120
+ streaming(chunks: string[]): MockLLM;
121
+ failing(error: Error): MockLLM;
122
+ withToolCalls(calls: LLMToolCall[], content?: string): MockLLM;
123
+ }
124
+ /**
125
+ * Create a mock LLM with fluent builder API.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * // Simple: always returns same response
130
+ * const llm = mockLLM();
131
+ *
132
+ * // With specific responses
133
+ * const llm = mockLLM()
134
+ * .onComplete('generate commit').respondWith('feat: add feature')
135
+ * .onComplete(/explain/i).respondWith('This code does...')
136
+ * .onAnyComplete().respondWith('default');
137
+ *
138
+ * // Streaming
139
+ * const llm = mockLLM().streaming(['chunk1', 'chunk2', 'chunk3']);
140
+ *
141
+ * // Error simulation
142
+ * const llm = mockLLM().failing(new Error('rate limit exceeded'));
143
+ *
144
+ * // Tool calling
145
+ * const llm = mockLLM().withToolCalls([
146
+ * { id: 'call-1', name: 'search', input: { query: 'test' } },
147
+ * ]);
148
+ * ```
149
+ */
150
+ declare function mockLLM(): MockLLM;
151
+
152
+ /**
153
+ * @module @kb-labs/shared-testing/mock-cache
154
+ *
155
+ * In-memory cache mock that actually stores data (unlike noop mocks).
156
+ * All methods are vi.fn() spies for assertion.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const cache = mockCache();
161
+ * await cache.set('key', { data: 42 }, 5000);
162
+ * expect(await cache.get('key')).toEqual({ data: 42 });
163
+ * expect(cache.set).toHaveBeenCalledWith('key', { data: 42 }, 5000);
164
+ * ```
165
+ */
166
+
167
+ interface CacheEntry {
168
+ value: unknown;
169
+ expiresAt: number | null;
170
+ }
171
+ interface SortedSetMember {
172
+ score: number;
173
+ member: string;
174
+ }
175
+ /**
176
+ * Mock cache instance with working in-memory storage.
177
+ * All methods are vi.fn() spies.
178
+ */
179
+ interface MockCacheInstance extends ICache {
180
+ /** Direct access to the internal store (for assertions) */
181
+ readonly store: Map<string, CacheEntry>;
182
+ /** Direct access to sorted sets (for assertions) */
183
+ readonly sortedSets: Map<string, SortedSetMember[]>;
184
+ /** Reset all data and spy call history */
185
+ reset: () => void;
186
+ }
187
+ /**
188
+ * Create a mock cache with working in-memory storage.
189
+ *
190
+ * Unlike noop mocks, this cache actually stores and retrieves data,
191
+ * respects TTL, and supports sorted set operations.
192
+ *
193
+ * @param initial - Optional initial data to populate the cache
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * // Empty cache
198
+ * const cache = mockCache();
199
+ *
200
+ * // Pre-populated cache
201
+ * const cache = mockCache({ 'user:1': { name: 'Alice' } });
202
+ *
203
+ * // TTL works
204
+ * await cache.set('temp', 'value', 100); // expires in 100ms
205
+ * await new Promise(r => setTimeout(r, 150));
206
+ * expect(await cache.get('temp')).toBeNull(); // expired
207
+ * ```
208
+ */
209
+ declare function mockCache(initial?: Record<string, unknown>): MockCacheInstance;
210
+
211
+ /**
212
+ * @module @kb-labs/shared-testing/mock-storage
213
+ *
214
+ * Virtual filesystem mock for IStorage interface.
215
+ * All methods are vi.fn() spies.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const storage = mockStorage({
220
+ * 'config.json': '{"key": "value"}',
221
+ * 'data/file.txt': 'hello world',
222
+ * });
223
+ *
224
+ * const content = await storage.read('config.json');
225
+ * expect(content?.toString()).toBe('{"key": "value"}');
226
+ * expect(storage.read).toHaveBeenCalledWith('config.json');
227
+ * ```
228
+ */
229
+
230
+ /**
231
+ * Mock storage instance with in-memory virtual filesystem.
232
+ */
233
+ interface MockStorageInstance extends IStorage {
234
+ /** Direct access to the virtual filesystem (for assertions) */
235
+ readonly files: Map<string, Buffer>;
236
+ /** Reset all files and spy call history */
237
+ reset: () => void;
238
+ }
239
+ /**
240
+ * Create a mock storage with in-memory virtual filesystem.
241
+ *
242
+ * @param initial - Optional initial files. Keys are paths, values are content (string or Buffer).
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * const storage = mockStorage({
247
+ * 'data.json': JSON.stringify({ items: [] }),
248
+ * 'binary.dat': Buffer.from([0x00, 0x01]),
249
+ * });
250
+ *
251
+ * // Write and read
252
+ * await storage.write('new.txt', Buffer.from('hello'));
253
+ * expect(await storage.exists('new.txt')).toBe(true);
254
+ *
255
+ * // List files
256
+ * const files = await storage.list('');
257
+ * expect(files).toContain('new.txt');
258
+ * ```
259
+ */
260
+ declare function mockStorage(initial?: Record<string, string | Buffer>): MockStorageInstance;
261
+
262
+ /**
263
+ * @module @kb-labs/shared-testing/mock-logger
264
+ *
265
+ * Logger mock with message recording and vi.fn() spies.
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const logger = mockLogger();
270
+ * logger.info('hello', { extra: 'data' });
271
+ * logger.error('oops', new Error('fail'));
272
+ *
273
+ * expect(logger.messages).toEqual([
274
+ * { level: 'info', msg: 'hello', meta: { extra: 'data' } },
275
+ * { level: 'error', msg: 'oops', error: expect.any(Error), meta: undefined },
276
+ * ]);
277
+ * expect(logger.info).toHaveBeenCalledWith('hello', { extra: 'data' });
278
+ * ```
279
+ */
280
+
281
+ /** Recorded log entry */
282
+ interface LogEntry {
283
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
284
+ msg: string;
285
+ error?: Error;
286
+ meta?: Record<string, unknown>;
287
+ }
288
+ /**
289
+ * Mock logger instance with message recording.
290
+ */
291
+ interface MockLoggerInstance extends ILogger {
292
+ /** All recorded log messages */
293
+ readonly messages: LogEntry[];
294
+ /** Reset messages and spy call history */
295
+ reset: () => void;
296
+ }
297
+ /**
298
+ * Create a mock logger with message recording.
299
+ *
300
+ * All log methods are vi.fn() spies. Messages are collected
301
+ * into a `.messages` array for easy assertion.
302
+ *
303
+ * Child loggers share the same messages array.
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const logger = mockLogger();
308
+ * const child = logger.child({ module: 'auth' });
309
+ *
310
+ * child.info('user logged in');
311
+ * expect(logger.messages).toHaveLength(1);
312
+ * expect(logger.messages[0].msg).toBe('user logged in');
313
+ * ```
314
+ */
315
+ declare function mockLogger(sharedMessages?: LogEntry[]): MockLoggerInstance;
316
+
317
+ /**
318
+ * @module @kb-labs/shared-testing/create-test-context
319
+ *
320
+ * Enhanced test context factory that bridges ctx.platform and the global singleton.
321
+ *
322
+ * Unlike the original createTestContext() from SDK, this version:
323
+ * - Uses mockLLM/mockCache/mockLogger with vi.fn() spies (not noop functions)
324
+ * - Syncs ctx.platform adapters with the global singleton via setupTestPlatform()
325
+ * - Provides a cleanup function to reset the singleton in afterEach()
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * import { createTestContext, mockLLM } from '@kb-labs/shared-testing';
330
+ *
331
+ * const llm = mockLLM().onAnyComplete().respondWith('hello');
332
+ * const { ctx, cleanup } = createTestContext({ platform: { llm } });
333
+ *
334
+ * // Both work — ctx.platform and useLLM() return the same mock
335
+ * await handler.execute(ctx, args);
336
+ * expect(llm.complete).toHaveBeenCalled();
337
+ *
338
+ * cleanup(); // Reset singleton in afterEach
339
+ * ```
340
+ */
341
+
342
+ interface CreateTestContextOptions {
343
+ pluginId?: string;
344
+ pluginVersion?: string;
345
+ host?: 'cli' | 'rest' | 'workflow' | 'webhook';
346
+ hostContext?: HostContext;
347
+ config?: unknown;
348
+ cwd?: string;
349
+ outdir?: string;
350
+ tenantId?: string;
351
+ signal?: AbortSignal;
352
+ /** Override platform services. Also synced to global singleton. */
353
+ platform?: Partial<PlatformServices>;
354
+ /** Override UI facade */
355
+ ui?: Partial<UIFacade>;
356
+ /**
357
+ * If true, sync platform adapters to the global singleton
358
+ * so that useLLM(), useCache(), etc. return the test mocks.
359
+ * @default true
360
+ */
361
+ syncSingleton?: boolean;
362
+ }
363
+ interface TestContextResult<TConfig = unknown> {
364
+ /** The plugin context with test mocks */
365
+ ctx: PluginContextV3<TConfig>;
366
+ /** Call in afterEach() to reset the global singleton */
367
+ cleanup: () => void;
368
+ }
369
+ declare function createMockTrace(): TraceContext;
370
+ declare function createMockUI(): UIFacade;
371
+ declare function createMockRuntime(): RuntimeAPI;
372
+ declare function createMockEnvironmentAPI(): EnvironmentAPI;
373
+ declare function createMockWorkspaceAPI(): WorkspaceAPI;
374
+ declare function createMockSnapshotAPI(): SnapshotAPI;
375
+ declare function createInfraApiMocks(): Pick<PluginAPI, 'environment' | 'workspace' | 'snapshot'>;
376
+ declare function createMockPluginAPI(): PluginAPI;
377
+ declare const createMockPlatformApi: typeof createMockPluginAPI;
378
+ declare function createMockPluginContextV3<TConfig = unknown>(options?: CreateTestContextOptions): TestContextResult<TConfig>;
379
+ /**
380
+ * Create a test context for plugin development.
381
+ *
382
+ * Unlike the original SDK version, this:
383
+ * - Uses mock builders with vi.fn() spies (not noop functions)
384
+ * - Syncs platform adapters to the global singleton by default
385
+ * - Returns a cleanup function for afterEach()
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * import { createTestContext, mockLLM } from '@kb-labs/shared-testing';
390
+ *
391
+ * describe('my handler', () => {
392
+ * let cleanup: () => void;
393
+ *
394
+ * beforeEach(() => {
395
+ * const llm = mockLLM().onAnyComplete().respondWith('ok');
396
+ * const result = createTestContext({ platform: { llm } });
397
+ * cleanup = result.cleanup;
398
+ * // Use result.ctx in your tests
399
+ * });
400
+ *
401
+ * afterEach(() => cleanup());
402
+ * });
403
+ * ```
404
+ */
405
+ declare function createTestContext<TConfig = unknown>(options?: CreateTestContextOptions): TestContextResult<TConfig>;
406
+
407
+ /**
408
+ * @module @kb-labs/shared-testing/test-command
409
+ *
410
+ * Single-function test runner for plugin command handlers.
411
+ *
412
+ * Eliminates boilerplate: no manual context creation, no descriptor setup,
413
+ * no mock wiring. Import your handler, call testCommand(), assert on result.
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * import { testCommand, mockLLM } from '@kb-labs/shared-testing';
418
+ * import handler from '../src/commands/greet.js';
419
+ *
420
+ * it('greets the user', async () => {
421
+ * const result = await testCommand(handler, {
422
+ * flags: { name: 'Alice' },
423
+ * });
424
+ *
425
+ * expect(result.exitCode).toBe(0);
426
+ * expect(result.result).toEqual({ message: 'Hello, Alice!' });
427
+ * expect(result.ui.success).toHaveBeenCalledWith('Hello, Alice!');
428
+ * });
429
+ * ```
430
+ */
431
+
432
+ /**
433
+ * Any handler that has an execute(ctx, input) method.
434
+ * Covers CommandHandlerV3, RouteHandler, Handler, and raw objects.
435
+ */
436
+ interface TestableHandler<TConfig = unknown, TInput = unknown, TResult = unknown> {
437
+ execute(context: PluginContextV3<TConfig>, input: TInput): Promise<TResult> | TResult;
438
+ cleanup?(): Promise<void> | void;
439
+ }
440
+ /**
441
+ * Options for testCommand().
442
+ *
443
+ * Provide only what you need — everything else gets sensible defaults.
444
+ */
445
+ interface TestCommandOptions<TConfig = unknown> {
446
+ /** CLI flags (sugar for input: { flags, argv }) */
447
+ flags?: Record<string, unknown>;
448
+ /** CLI positional arguments (default: []) */
449
+ argv?: string[];
450
+ /** REST query params (sugar for input: { query }) */
451
+ query?: Record<string, unknown>;
452
+ /** REST request body (sugar for input: { body }) */
453
+ body?: unknown;
454
+ /** REST route params (sugar for input: { params }) */
455
+ params?: Record<string, unknown>;
456
+ /**
457
+ * Raw input object. If provided, flags/argv/query/body/params are ignored.
458
+ * Use this when your handler expects a custom input shape.
459
+ */
460
+ input?: unknown;
461
+ /** Host type (default: 'cli') */
462
+ host?: 'cli' | 'rest' | 'workflow' | 'webhook';
463
+ /** Plugin config passed as ctx.config */
464
+ config?: TConfig;
465
+ /** Working directory (default: process.cwd()) */
466
+ cwd?: string;
467
+ /** Tenant ID */
468
+ tenantId?: string;
469
+ /** Abort signal */
470
+ signal?: AbortSignal;
471
+ /** Override platform services (e.g., { llm: mockLLM().onAnyComplete().respondWith('ok') }) */
472
+ platform?: Partial<PlatformServices>;
473
+ /** Override UI facade (individual methods are merged with default spy UI) */
474
+ ui?: Partial<UIFacade>;
475
+ /**
476
+ * Sync platform mocks to global singleton for composables (useLLM, useCache, etc.)
477
+ * @default true
478
+ */
479
+ syncSingleton?: boolean;
480
+ }
481
+ /**
482
+ * Result of testCommand() — everything you need for assertions.
483
+ */
484
+ interface TestCommandResult<TResult = unknown> {
485
+ /** Exit code from CommandResult (0 = success). Defaults to 0 if handler returns raw data. */
486
+ exitCode: number;
487
+ /** The result data from the handler */
488
+ result: TResult | undefined;
489
+ /** Custom metadata from CommandResult.meta */
490
+ meta: Record<string, unknown> | undefined;
491
+ /** The raw return value from handler.execute() — useful for non-CommandResult handlers */
492
+ raw: unknown;
493
+ /** UI facade with vi.fn() spies — assert on ui.success, ui.error, etc. */
494
+ ui: UIFacade;
495
+ /** The full context that was passed to the handler */
496
+ ctx: PluginContextV3;
497
+ /** Call in afterEach() to reset the global platform singleton */
498
+ cleanup: () => void;
499
+ }
500
+ /**
501
+ * Test a plugin command handler with minimal boilerplate.
502
+ *
503
+ * Creates a test context, builds the input object, calls handler.execute(),
504
+ * and returns a result with UI spies for assertions.
505
+ *
506
+ * @example CLI command
507
+ * ```typescript
508
+ * import handler from '../src/commands/greet.js';
509
+ *
510
+ * const result = await testCommand(handler, {
511
+ * flags: { name: 'Alice' },
512
+ * });
513
+ * expect(result.exitCode).toBe(0);
514
+ * expect(result.ui.success).toHaveBeenCalledWith('Hello, Alice!');
515
+ * ```
516
+ *
517
+ * @example REST handler
518
+ * ```typescript
519
+ * import handler from '../src/routes/create-plan.js';
520
+ *
521
+ * const result = await testCommand(handler, {
522
+ * host: 'rest',
523
+ * body: { name: 'v2.0' },
524
+ * query: { workspace: 'root' },
525
+ * });
526
+ * expect(result.result).toMatchObject({ name: 'v2.0' });
527
+ * ```
528
+ *
529
+ * @example With LLM mock
530
+ * ```typescript
531
+ * import { testCommand, mockLLM } from '@kb-labs/shared-testing';
532
+ * import handler from '../src/commands/analyze.js';
533
+ *
534
+ * const llm = mockLLM().onAnyComplete().respondWith('looks good');
535
+ * const result = await testCommand(handler, {
536
+ * flags: { file: 'index.ts' },
537
+ * platform: { llm },
538
+ * });
539
+ * expect(llm.complete).toHaveBeenCalled();
540
+ * expect(result.exitCode).toBe(0);
541
+ * ```
542
+ */
543
+ declare function testCommand<TResult = unknown, TConfig = unknown>(handler: TestableHandler<TConfig, any, any>, options?: TestCommandOptions<TConfig>): Promise<TestCommandResult<TResult>>;
544
+
545
+ export { type CreateTestContextOptions, type LLMCall, type LLMToolCallRecord, type LogEntry, type MockCacheInstance, type MockLLM, type MockLLMInstance, type MockLoggerInstance, type MockStorageInstance, type TestCommandOptions, type TestCommandResult, type TestContextResult, type TestPlatformOptions, type TestPlatformResult, type TestableHandler, createInfraApiMocks, createMockEnvironmentAPI, createMockPlatformApi, createMockPluginAPI, createMockPluginContextV3, createMockRuntime, createMockSnapshotAPI, createMockTrace, createMockUI, createMockWorkspaceAPI, createTestContext, mockCache, mockLLM, mockLogger, mockStorage, setupTestPlatform, testCommand };