@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.
- package/README.md +430 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +916 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|