@kb-labs/shared 1.1.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.
Files changed (232) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/.vscode/settings.json +23 -0
  21. package/CHANGELOG.md +33 -0
  22. package/CONTRIBUTING.md +117 -0
  23. package/LICENSE +21 -0
  24. package/README.md +306 -0
  25. package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
  26. package/docs/DOCUMENTATION.md +70 -0
  27. package/docs/adr/0000-template.md +52 -0
  28. package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
  29. package/docs/adr/0002-plugins-and-extensibility.md +44 -0
  30. package/docs/adr/0003-package-and-module-boundaries.md +35 -0
  31. package/docs/adr/0004-versioning-and-release-policy.md +36 -0
  32. package/docs/adr/0005-reactive-loader-pattern.md +179 -0
  33. package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
  34. package/eslint.config.js +27 -0
  35. package/kb-labs.config.json +5 -0
  36. package/package.json +88 -0
  37. package/package.json.bin +25 -0
  38. package/package.json.lib +30 -0
  39. package/packages/shared-cli-ui/CHANGELOG.md +20 -0
  40. package/packages/shared-cli-ui/README.md +342 -0
  41. package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
  42. package/packages/shared-cli-ui/eslint.config.js +27 -0
  43. package/packages/shared-cli-ui/package.json +72 -0
  44. package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
  45. package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
  46. package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
  47. package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
  48. package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
  49. package/packages/shared-cli-ui/src/colors.ts +142 -0
  50. package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
  51. package/packages/shared-cli-ui/src/command-output.ts +153 -0
  52. package/packages/shared-cli-ui/src/command-result.ts +267 -0
  53. package/packages/shared-cli-ui/src/command-runner.ts +310 -0
  54. package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
  55. package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
  56. package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
  57. package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
  58. package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
  59. package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
  60. package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
  61. package/packages/shared-cli-ui/src/debug/index.ts +56 -0
  62. package/packages/shared-cli-ui/src/debug/types.ts +57 -0
  63. package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
  64. package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
  65. package/packages/shared-cli-ui/src/format.ts +412 -0
  66. package/packages/shared-cli-ui/src/index.ts +34 -0
  67. package/packages/shared-cli-ui/src/loader.ts +196 -0
  68. package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
  69. package/packages/shared-cli-ui/src/modern-format.ts +271 -0
  70. package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
  71. package/packages/shared-cli-ui/src/table.ts +134 -0
  72. package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
  73. package/packages/shared-cli-ui/src/utils/context.ts +12 -0
  74. package/packages/shared-cli-ui/src/utils/env.ts +164 -0
  75. package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
  76. package/packages/shared-cli-ui/src/utils/path.ts +8 -0
  77. package/packages/shared-cli-ui/tsconfig.build.json +15 -0
  78. package/packages/shared-cli-ui/tsconfig.json +9 -0
  79. package/packages/shared-cli-ui/tsup.config.ts +11 -0
  80. package/packages/shared-cli-ui/vitest.config.ts +15 -0
  81. package/packages/shared-command-kit/CHANGELOG.md +20 -0
  82. package/packages/shared-command-kit/LICENSE +22 -0
  83. package/packages/shared-command-kit/README.md +1030 -0
  84. package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
  85. package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
  86. package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
  87. package/packages/shared-command-kit/eslint.config.js +27 -0
  88. package/packages/shared-command-kit/eslint.config.ts +14 -0
  89. package/packages/shared-command-kit/package.json +76 -0
  90. package/packages/shared-command-kit/prettierrc.json +5 -0
  91. package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
  92. package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
  93. package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
  94. package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
  95. package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
  96. package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
  97. package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
  98. package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
  99. package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
  100. package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
  101. package/packages/shared-command-kit/src/analytics/index.ts +6 -0
  102. package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
  103. package/packages/shared-command-kit/src/define-action.ts +100 -0
  104. package/packages/shared-command-kit/src/define-command.ts +113 -0
  105. package/packages/shared-command-kit/src/define-route.ts +113 -0
  106. package/packages/shared-command-kit/src/define-system-command.ts +362 -0
  107. package/packages/shared-command-kit/src/define-webhook.ts +115 -0
  108. package/packages/shared-command-kit/src/define-websocket.ts +308 -0
  109. package/packages/shared-command-kit/src/errors/factory.ts +282 -0
  110. package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
  111. package/packages/shared-command-kit/src/errors/format.ts +92 -0
  112. package/packages/shared-command-kit/src/errors/index.ts +9 -0
  113. package/packages/shared-command-kit/src/errors/types.ts +32 -0
  114. package/packages/shared-command-kit/src/flags/define.ts +92 -0
  115. package/packages/shared-command-kit/src/flags/index.ts +9 -0
  116. package/packages/shared-command-kit/src/flags/types.ts +153 -0
  117. package/packages/shared-command-kit/src/flags/validate.ts +358 -0
  118. package/packages/shared-command-kit/src/helpers/context.ts +8 -0
  119. package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
  120. package/packages/shared-command-kit/src/helpers/index.ts +42 -0
  121. package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
  122. package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
  123. package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
  124. package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
  125. package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
  126. package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
  127. package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
  128. package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
  129. package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
  130. package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
  131. package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
  132. package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
  133. package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
  134. package/packages/shared-command-kit/src/index.ts +410 -0
  135. package/packages/shared-command-kit/src/jobs.ts +132 -0
  136. package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
  137. package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
  138. package/packages/shared-command-kit/src/manifest.ts +127 -0
  139. package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
  140. package/packages/shared-command-kit/src/rest/index.ts +11 -0
  141. package/packages/shared-command-kit/src/studio/index.ts +12 -0
  142. package/packages/shared-command-kit/src/validation/index.ts +6 -0
  143. package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
  144. package/packages/shared-command-kit/src/ws-types.ts +106 -0
  145. package/packages/shared-command-kit/tsconfig.build.json +15 -0
  146. package/packages/shared-command-kit/tsconfig.json +9 -0
  147. package/packages/shared-command-kit/tsup.config.ts +30 -0
  148. package/packages/shared-command-kit/vitest.config.ts +4 -0
  149. package/packages/shared-http/package.json +67 -0
  150. package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
  151. package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
  152. package/packages/shared-http/src/http-observability-collector.ts +363 -0
  153. package/packages/shared-http/src/index.ts +36 -0
  154. package/packages/shared-http/src/log-correlation.ts +89 -0
  155. package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
  156. package/packages/shared-http/src/register-openapi.ts +108 -0
  157. package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
  158. package/packages/shared-http/src/schemas.ts +29 -0
  159. package/packages/shared-http/src/service-observability.ts +63 -0
  160. package/packages/shared-http/tsconfig.build.json +15 -0
  161. package/packages/shared-http/tsconfig.json +9 -0
  162. package/packages/shared-http/tsup.config.ts +23 -0
  163. package/packages/shared-http/vitest.config.ts +13 -0
  164. package/packages/shared-perm-presets/CHANGELOG.md +20 -0
  165. package/packages/shared-perm-presets/README.md +78 -0
  166. package/packages/shared-perm-presets/eslint.config.js +27 -0
  167. package/packages/shared-perm-presets/package.json +45 -0
  168. package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
  169. package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
  170. package/packages/shared-perm-presets/src/combine.ts +278 -0
  171. package/packages/shared-perm-presets/src/index.ts +18 -0
  172. package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
  173. package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
  174. package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
  175. package/packages/shared-perm-presets/src/presets/index.ts +8 -0
  176. package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
  177. package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
  178. package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
  179. package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
  180. package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
  181. package/packages/shared-perm-presets/src/types.ts +192 -0
  182. package/packages/shared-perm-presets/tsconfig.build.json +15 -0
  183. package/packages/shared-perm-presets/tsconfig.json +9 -0
  184. package/packages/shared-perm-presets/tsup.config.ts +8 -0
  185. package/packages/shared-perm-presets/vitest.config.ts +9 -0
  186. package/packages/shared-testing/CHANGELOG.md +20 -0
  187. package/packages/shared-testing/README.md +430 -0
  188. package/packages/shared-testing/package.json +51 -0
  189. package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
  190. package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
  191. package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
  192. package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
  193. package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
  194. package/packages/shared-testing/src/create-test-context.ts +550 -0
  195. package/packages/shared-testing/src/index.ts +77 -0
  196. package/packages/shared-testing/src/mock-cache.ts +179 -0
  197. package/packages/shared-testing/src/mock-llm.ts +319 -0
  198. package/packages/shared-testing/src/mock-logger.ts +97 -0
  199. package/packages/shared-testing/src/mock-storage.ts +108 -0
  200. package/packages/shared-testing/src/setup-platform.ts +101 -0
  201. package/packages/shared-testing/src/test-command.ts +288 -0
  202. package/packages/shared-testing/tsconfig.build.json +15 -0
  203. package/packages/shared-testing/tsconfig.json +9 -0
  204. package/packages/shared-testing/tsup.config.ts +20 -0
  205. package/packages/shared-testing/vitest.config.ts +3 -0
  206. package/packages/shared-tool-kit/CHANGELOG.md +20 -0
  207. package/packages/shared-tool-kit/package.json +47 -0
  208. package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
  209. package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
  210. package/packages/shared-tool-kit/src/factory.ts +126 -0
  211. package/packages/shared-tool-kit/src/index.ts +32 -0
  212. package/packages/shared-tool-kit/src/testing/index.ts +84 -0
  213. package/packages/shared-tool-kit/tsconfig.build.json +15 -0
  214. package/packages/shared-tool-kit/tsconfig.json +9 -0
  215. package/packages/shared-tool-kit/tsup.config.ts +21 -0
  216. package/pnpm-workspace.yaml +11070 -0
  217. package/prettierrc.json +1 -0
  218. package/scripts/devkit-sync.mjs +37 -0
  219. package/scripts/hooks/post-push +9 -0
  220. package/scripts/hooks/pre-commit +9 -0
  221. package/scripts/hooks/pre-push +9 -0
  222. package/tsconfig.base.json +9 -0
  223. package/tsconfig.build.json +15 -0
  224. package/tsconfig.json +9 -0
  225. package/tsconfig.paths.json +50 -0
  226. package/tsconfig.tools.json +18 -0
  227. package/tsup.config.bin.ts +34 -0
  228. package/tsup.config.cli.ts +41 -0
  229. package/tsup.config.dual.ts +46 -0
  230. package/tsup.config.ts +36 -0
  231. package/tsup.external.json +104 -0
  232. package/vitest.config.ts +48 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @module @kb-labs/shared-testing/mock-cache
3
+ *
4
+ * In-memory cache mock that actually stores data (unlike noop mocks).
5
+ * All methods are vi.fn() spies for assertion.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const cache = mockCache();
10
+ * await cache.set('key', { data: 42 }, 5000);
11
+ * expect(await cache.get('key')).toEqual({ data: 42 });
12
+ * expect(cache.set).toHaveBeenCalledWith('key', { data: 42 }, 5000);
13
+ * ```
14
+ */
15
+
16
+ import { vi } from 'vitest';
17
+ import type { ICache } from '@kb-labs/core-platform';
18
+
19
+ interface CacheEntry {
20
+ value: unknown;
21
+ expiresAt: number | null; // null = no TTL
22
+ }
23
+
24
+ interface SortedSetMember {
25
+ score: number;
26
+ member: string;
27
+ }
28
+
29
+ /**
30
+ * Mock cache instance with working in-memory storage.
31
+ * All methods are vi.fn() spies.
32
+ */
33
+ export interface MockCacheInstance extends ICache {
34
+ /** Direct access to the internal store (for assertions) */
35
+ readonly store: Map<string, CacheEntry>;
36
+ /** Direct access to sorted sets (for assertions) */
37
+ readonly sortedSets: Map<string, SortedSetMember[]>;
38
+ /** Reset all data and spy call history */
39
+ reset: () => void;
40
+ }
41
+
42
+ /**
43
+ * Create a mock cache with working in-memory storage.
44
+ *
45
+ * Unlike noop mocks, this cache actually stores and retrieves data,
46
+ * respects TTL, and supports sorted set operations.
47
+ *
48
+ * @param initial - Optional initial data to populate the cache
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * // Empty cache
53
+ * const cache = mockCache();
54
+ *
55
+ * // Pre-populated cache
56
+ * const cache = mockCache({ 'user:1': { name: 'Alice' } });
57
+ *
58
+ * // TTL works
59
+ * await cache.set('temp', 'value', 100); // expires in 100ms
60
+ * await new Promise(r => setTimeout(r, 150));
61
+ * expect(await cache.get('temp')).toBeNull(); // expired
62
+ * ```
63
+ */
64
+ export function mockCache(initial?: Record<string, unknown>): MockCacheInstance {
65
+ const store = new Map<string, CacheEntry>();
66
+ const sortedSets = new Map<string, SortedSetMember[]>();
67
+
68
+ // Populate initial data
69
+ if (initial) {
70
+ for (const [key, value] of Object.entries(initial)) {
71
+ store.set(key, { value, expiresAt: null });
72
+ }
73
+ }
74
+
75
+ function isExpired(entry: CacheEntry): boolean {
76
+ return entry.expiresAt !== null && Date.now() > entry.expiresAt;
77
+ }
78
+
79
+ // Keep raw vi.fn() references for mockClear() in reset.
80
+ // The ICache-typed versions are cast below for the public interface.
81
+ const spies: ReturnType<typeof vi.fn>[] = [];
82
+
83
+ function spy<T extends (...args: any[]) => any>(fn: T): T {
84
+ const s = vi.fn(fn) as unknown as T;
85
+ spies.push(s as unknown as ReturnType<typeof vi.fn>);
86
+ return s;
87
+ }
88
+
89
+ const getFn = spy(async (key: string): Promise<unknown> => {
90
+ const entry = store.get(key);
91
+ if (!entry || isExpired(entry)) {
92
+ if (entry) {store.delete(key);} // cleanup expired
93
+ return null;
94
+ }
95
+ return entry.value;
96
+ });
97
+
98
+ const setFn = spy(async (key: string, value: unknown, ttl?: number): Promise<void> => {
99
+ store.set(key, {
100
+ value,
101
+ expiresAt: ttl ? Date.now() + ttl : null,
102
+ });
103
+ });
104
+
105
+ const deleteFn = spy(async (key: string): Promise<void> => {
106
+ store.delete(key);
107
+ });
108
+
109
+ const clearFn = spy(async (pattern?: string): Promise<void> => {
110
+ if (!pattern) {
111
+ store.clear();
112
+ return;
113
+ }
114
+ // Simple glob matching: only support trailing *
115
+ const prefix = pattern.replace(/\*$/, '');
116
+ for (const key of store.keys()) {
117
+ if (key.startsWith(prefix)) {
118
+ store.delete(key);
119
+ }
120
+ }
121
+ });
122
+
123
+ const zaddFn = spy(async (key: string, score: number, member: string): Promise<void> => {
124
+ if (!sortedSets.has(key)) {sortedSets.set(key, []);}
125
+ const set = sortedSets.get(key)!;
126
+ // Remove existing member
127
+ const idx = set.findIndex(m => m.member === member);
128
+ if (idx >= 0) {set.splice(idx, 1);}
129
+ // Add with new score
130
+ set.push({ score, member });
131
+ // Keep sorted by score
132
+ set.sort((a, b) => a.score - b.score);
133
+ });
134
+
135
+ const zrangebyscoreFn = spy(async (key: string, min: number, max: number): Promise<string[]> => {
136
+ const set = sortedSets.get(key);
137
+ if (!set) {return [];}
138
+ return set
139
+ .filter(m => m.score >= min && m.score <= max)
140
+ .map(m => m.member);
141
+ });
142
+
143
+ const zremFn = spy(async (key: string, member: string): Promise<void> => {
144
+ const set = sortedSets.get(key);
145
+ if (!set) {return;}
146
+ const idx = set.findIndex(m => m.member === member);
147
+ if (idx >= 0) {set.splice(idx, 1);}
148
+ });
149
+
150
+ const setIfNotExistsFn = spy(async (key: string, value: unknown, ttl?: number): Promise<boolean> => {
151
+ const existing = store.get(key);
152
+ if (existing && !isExpired(existing)) {return false;}
153
+ store.set(key, {
154
+ value,
155
+ expiresAt: ttl ? Date.now() + ttl : null,
156
+ });
157
+ return true;
158
+ });
159
+
160
+ const instance: MockCacheInstance = {
161
+ get: getFn as unknown as ICache['get'],
162
+ set: setFn as unknown as ICache['set'],
163
+ delete: deleteFn,
164
+ clear: clearFn,
165
+ zadd: zaddFn,
166
+ zrangebyscore: zrangebyscoreFn,
167
+ zrem: zremFn,
168
+ setIfNotExists: setIfNotExistsFn as unknown as ICache['setIfNotExists'],
169
+ get store() { return store; },
170
+ get sortedSets() { return sortedSets; },
171
+ reset: () => {
172
+ store.clear();
173
+ sortedSets.clear();
174
+ for (const s of spies) {s.mockClear();}
175
+ },
176
+ };
177
+
178
+ return instance;
179
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * @module @kb-labs/shared-testing/mock-llm
3
+ *
4
+ * LLM mock builder with fluent API and call tracking.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const llm = mockLLM()
9
+ * .onComplete('Generate commit').respondWith('feat: add login')
10
+ * .onComplete(/explain/i).respondWith('This function does X')
11
+ * .onAnyComplete().respondWith('default answer');
12
+ *
13
+ * const res = await llm.complete('Generate commit message');
14
+ * expect(res.content).toBe('feat: add login');
15
+ * expect(llm.complete).toHaveBeenCalledOnce();
16
+ * ```
17
+ */
18
+
19
+ import { vi } from 'vitest';
20
+ import type {
21
+ ILLM,
22
+ LLMOptions,
23
+ LLMResponse,
24
+ LLMMessage,
25
+ LLMToolCallOptions,
26
+ LLMToolCallResponse,
27
+ LLMToolCall,
28
+ } from '@kb-labs/core-platform';
29
+
30
+ // ────────────────────────────────────────────────────────────────────
31
+ // Types
32
+ // ────────────────────────────────────────────────────────────────────
33
+
34
+ /** Recorded call to complete() */
35
+ export interface LLMCall {
36
+ prompt: string;
37
+ options?: LLMOptions;
38
+ response: LLMResponse;
39
+ }
40
+
41
+ /** Recorded call to chatWithTools() */
42
+ export interface LLMToolCallRecord {
43
+ messages: LLMMessage[];
44
+ options: LLMToolCallOptions;
45
+ response: LLMToolCallResponse;
46
+ }
47
+
48
+ type PromptMatcher = string | RegExp | ((prompt: string) => boolean);
49
+
50
+ interface CompletionRule {
51
+ matcher: PromptMatcher;
52
+ response: string | LLMResponse | ((prompt: string) => string | LLMResponse);
53
+ }
54
+
55
+ /**
56
+ * Mock LLM instance with call tracking and fluent API.
57
+ * All methods are vi.fn() spies.
58
+ */
59
+ export interface MockLLMInstance extends ILLM {
60
+ /** All recorded complete() calls */
61
+ calls: LLMCall[];
62
+ /** Last complete() call (or undefined) */
63
+ lastCall: LLMCall | undefined;
64
+ /** All recorded chatWithTools() calls */
65
+ toolCalls: LLMToolCallRecord[];
66
+ /** Reset all recorded calls and spies */
67
+ resetCalls: () => void;
68
+ }
69
+
70
+ /** Public type returned by mockLLM() — builder fluent API + ILLM spy instance */
71
+ export interface MockLLM extends MockLLMInstance {
72
+ onComplete(matcher: PromptMatcher): { respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLM };
73
+ onAnyComplete(): { respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLM };
74
+ streaming(chunks: string[]): MockLLM;
75
+ failing(error: Error): MockLLM;
76
+ withToolCalls(calls: LLMToolCall[], content?: string): MockLLM;
77
+ }
78
+
79
+ // ────────────────────────────────────────────────────────────────────
80
+ // Builder
81
+ // ────────────────────────────────────────────────────────────────────
82
+
83
+ class MockLLMBuilder {
84
+ private rules: CompletionRule[] = [];
85
+ private defaultResponse: string | LLMResponse | ((prompt: string) => string | LLMResponse) = 'mock response';
86
+ private streamChunks: string[] = ['mock'];
87
+ private errorToThrow: Error | null = null;
88
+ private toolCallsToReturn: LLMToolCall[] = [];
89
+ private toolCallResponseContent = '';
90
+
91
+ /**
92
+ * Match a specific prompt (exact string or regex).
93
+ * Returns a handler to set the response.
94
+ */
95
+ onComplete(matcher: PromptMatcher): { respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLMBuilder } {
96
+ return {
97
+ respondWith: (response) => {
98
+ this.rules.push({ matcher, response });
99
+ return this;
100
+ },
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Set the default response for any prompt that doesn't match a rule.
106
+ */
107
+ onAnyComplete(): { respondWith: (response: string | LLMResponse | ((prompt: string) => string | LLMResponse)) => MockLLMBuilder } {
108
+ return {
109
+ respondWith: (response) => {
110
+ this.defaultResponse = response;
111
+ return this;
112
+ },
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Configure streaming to yield specific chunks.
118
+ */
119
+ streaming(chunks: string[]): MockLLMBuilder {
120
+ this.streamChunks = chunks;
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Make the LLM throw an error on every call.
126
+ */
127
+ failing(error: Error): MockLLMBuilder {
128
+ this.errorToThrow = error;
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Configure chatWithTools() to return specific tool calls.
134
+ */
135
+ withToolCalls(calls: LLMToolCall[], content = ''): MockLLMBuilder {
136
+ this.toolCallsToReturn = calls;
137
+ this.toolCallResponseContent = content;
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Build the mock ILLM instance. Called automatically by mockLLM().
143
+ */
144
+ build(): MockLLMInstance {
145
+ const calls: LLMCall[] = [];
146
+ const toolCallRecords: LLMToolCallRecord[] = [];
147
+ const rules = this.rules;
148
+ const defaultResponse = this.defaultResponse;
149
+ const streamChunks = this.streamChunks;
150
+ const errorToThrow = this.errorToThrow;
151
+ const toolCallsToReturn = this.toolCallsToReturn;
152
+ const toolCallResponseContent = this.toolCallResponseContent;
153
+
154
+ function resolveResponse(prompt: string): LLMResponse {
155
+ // Check rules in order
156
+ for (const rule of rules) {
157
+ if (matchesPrompt(rule.matcher, prompt)) {
158
+ return toResponse(rule.response, prompt);
159
+ }
160
+ }
161
+ // Fallback
162
+ return toResponse(defaultResponse, prompt);
163
+ }
164
+
165
+ const completeFn = vi.fn(async (prompt: string, options?: LLMOptions): Promise<LLMResponse> => {
166
+ if (errorToThrow) {throw errorToThrow;}
167
+ const response = resolveResponse(prompt);
168
+ calls.push({ prompt, options, response });
169
+ return response;
170
+ });
171
+
172
+ const streamFn = vi.fn(async function* (_prompt: string, _options?: LLMOptions): AsyncIterable<string> {
173
+ if (errorToThrow) {throw errorToThrow;}
174
+ for (const chunk of streamChunks) {
175
+ yield chunk;
176
+ }
177
+ });
178
+
179
+ const chatWithToolsFn = vi.fn(async (messages: LLMMessage[], options: LLMToolCallOptions): Promise<LLMToolCallResponse> => {
180
+ if (errorToThrow) {throw errorToThrow;}
181
+
182
+ const lastUserMsg = messages.filter(m => m.role === 'user').pop();
183
+ const prompt = lastUserMsg?.content ?? '';
184
+ const baseResponse = resolveResponse(prompt);
185
+
186
+ const response: LLMToolCallResponse = {
187
+ ...baseResponse,
188
+ content: toolCallsToReturn.length > 0 ? toolCallResponseContent : baseResponse.content,
189
+ toolCalls: toolCallsToReturn.length > 0 ? toolCallsToReturn : undefined,
190
+ };
191
+
192
+ toolCallRecords.push({ messages, options, response });
193
+ return response;
194
+ });
195
+
196
+ const instance: MockLLMInstance = {
197
+ complete: completeFn,
198
+ stream: streamFn,
199
+ chatWithTools: chatWithToolsFn,
200
+ calls,
201
+ toolCalls: toolCallRecords,
202
+ get lastCall() {
203
+ return calls[calls.length - 1];
204
+ },
205
+ resetCalls: () => {
206
+ calls.length = 0;
207
+ toolCallRecords.length = 0;
208
+ completeFn.mockClear();
209
+ streamFn.mockClear();
210
+ chatWithToolsFn.mockClear();
211
+ },
212
+ };
213
+
214
+ return instance;
215
+ }
216
+ }
217
+
218
+ // ────────────────────────────────────────────────────────────────────
219
+ // Helpers
220
+ // ────────────────────────────────────────────────────────────────────
221
+
222
+ function matchesPrompt(matcher: PromptMatcher, prompt: string): boolean {
223
+ if (typeof matcher === 'string') {return prompt.includes(matcher);}
224
+ if (matcher instanceof RegExp) {return matcher.test(prompt);}
225
+ return matcher(prompt);
226
+ }
227
+
228
+ function toResponse(
229
+ value: string | LLMResponse | ((prompt: string) => string | LLMResponse),
230
+ prompt: string,
231
+ ): LLMResponse {
232
+ const resolved = typeof value === 'function' ? value(prompt) : value;
233
+ if (typeof resolved === 'string') {
234
+ return {
235
+ content: resolved,
236
+ usage: { promptTokens: 0, completionTokens: 0 },
237
+ model: 'mock',
238
+ };
239
+ }
240
+ return resolved;
241
+ }
242
+
243
+ // ────────────────────────────────────────────────────────────────────
244
+ // Public API
245
+ // ────────────────────────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Create a mock LLM with fluent builder API.
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * // Simple: always returns same response
253
+ * const llm = mockLLM();
254
+ *
255
+ * // With specific responses
256
+ * const llm = mockLLM()
257
+ * .onComplete('generate commit').respondWith('feat: add feature')
258
+ * .onComplete(/explain/i).respondWith('This code does...')
259
+ * .onAnyComplete().respondWith('default');
260
+ *
261
+ * // Streaming
262
+ * const llm = mockLLM().streaming(['chunk1', 'chunk2', 'chunk3']);
263
+ *
264
+ * // Error simulation
265
+ * const llm = mockLLM().failing(new Error('rate limit exceeded'));
266
+ *
267
+ * // Tool calling
268
+ * const llm = mockLLM().withToolCalls([
269
+ * { id: 'call-1', name: 'search', input: { query: 'test' } },
270
+ * ]);
271
+ * ```
272
+ */
273
+ export function mockLLM(): MockLLM {
274
+ const builder = new MockLLMBuilder();
275
+
276
+ // Create a Proxy that acts as both builder and built instance.
277
+ // Builder methods are forwarded to the builder.
278
+ // ILLM methods (complete, stream, etc.) trigger auto-build on first access.
279
+ let built: MockLLMInstance | null = null;
280
+
281
+ function ensureBuilt(): MockLLMInstance {
282
+ if (!built) {built = builder.build();}
283
+ return built;
284
+ }
285
+
286
+ const proxy = new Proxy(builder, {
287
+ get(target, prop, _receiver) {
288
+ // Builder methods — return from builder
289
+ if (prop in target && typeof (target as any)[prop] === 'function') {
290
+ const method = (target as any)[prop].bind(target);
291
+ // After calling a builder method, invalidate the built instance
292
+ return (...args: unknown[]) => {
293
+ built = null;
294
+ const result = method(...args);
295
+ // If the result is the builder itself (chaining), return the proxy
296
+ if (result === target || (result && typeof result === 'object' && 'respondWith' in result)) {
297
+ if (result === target) {return proxy;}
298
+ // Wrap respondWith to return proxy
299
+ return {
300
+ respondWith: (...rArgs: unknown[]) => {
301
+ built = null;
302
+ (result as any).respondWith(...rArgs);
303
+ return proxy;
304
+ },
305
+ };
306
+ }
307
+ return result;
308
+ };
309
+ }
310
+
311
+ // ILLM instance properties — auto-build
312
+ const instance = ensureBuilt();
313
+ // Return functions directly (not bound) to preserve vi.fn() spy identity
314
+ return (instance as any)[prop];
315
+ },
316
+ });
317
+
318
+ return proxy as unknown as MockLLM;
319
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @module @kb-labs/shared-testing/mock-logger
3
+ *
4
+ * Logger mock with message recording and vi.fn() spies.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const logger = mockLogger();
9
+ * logger.info('hello', { extra: 'data' });
10
+ * logger.error('oops', new Error('fail'));
11
+ *
12
+ * expect(logger.messages).toEqual([
13
+ * { level: 'info', msg: 'hello', meta: { extra: 'data' } },
14
+ * { level: 'error', msg: 'oops', error: expect.any(Error), meta: undefined },
15
+ * ]);
16
+ * expect(logger.info).toHaveBeenCalledWith('hello', { extra: 'data' });
17
+ * ```
18
+ */
19
+
20
+ import { vi } from 'vitest';
21
+ import type { ILogger } from '@kb-labs/core-platform';
22
+
23
+ /** Recorded log entry */
24
+ export interface LogEntry {
25
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
26
+ msg: string;
27
+ error?: Error;
28
+ meta?: Record<string, unknown>;
29
+ }
30
+
31
+ /**
32
+ * Mock logger instance with message recording.
33
+ */
34
+ export interface MockLoggerInstance extends ILogger {
35
+ /** All recorded log messages */
36
+ readonly messages: LogEntry[];
37
+ /** Reset messages and spy call history */
38
+ reset: () => void;
39
+ }
40
+
41
+ /**
42
+ * Create a mock logger with message recording.
43
+ *
44
+ * All log methods are vi.fn() spies. Messages are collected
45
+ * into a `.messages` array for easy assertion.
46
+ *
47
+ * Child loggers share the same messages array.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const logger = mockLogger();
52
+ * const child = logger.child({ module: 'auth' });
53
+ *
54
+ * child.info('user logged in');
55
+ * expect(logger.messages).toHaveLength(1);
56
+ * expect(logger.messages[0].msg).toBe('user logged in');
57
+ * ```
58
+ */
59
+ export function mockLogger(sharedMessages?: LogEntry[]): MockLoggerInstance {
60
+ const messages: LogEntry[] = sharedMessages ?? [];
61
+
62
+ // trace, debug, info, warn: (message, meta?)
63
+ function createSimpleLogMethod(level: LogEntry['level']) {
64
+ return vi.fn((msg: string, meta?: Record<string, unknown>) => {
65
+ messages.push({ level, msg, meta });
66
+ });
67
+ }
68
+
69
+ // error, fatal: (message, error?, meta?)
70
+ function createErrorLogMethod(level: 'error' | 'fatal') {
71
+ return vi.fn((msg: string, error?: Error, meta?: Record<string, unknown>) => {
72
+ messages.push({ level, msg, error, meta });
73
+ });
74
+ }
75
+
76
+ const instance: MockLoggerInstance = {
77
+ trace: createSimpleLogMethod('trace'),
78
+ debug: createSimpleLogMethod('debug'),
79
+ info: createSimpleLogMethod('info'),
80
+ warn: createSimpleLogMethod('warn'),
81
+ error: createErrorLogMethod('error'),
82
+ fatal: createErrorLogMethod('fatal'),
83
+ child: (_bindings?: Record<string, unknown>) => mockLogger(messages),
84
+ get messages() { return messages; },
85
+ reset: () => {
86
+ messages.length = 0;
87
+ (instance.trace as ReturnType<typeof vi.fn>).mockClear();
88
+ (instance.debug as ReturnType<typeof vi.fn>).mockClear();
89
+ (instance.info as ReturnType<typeof vi.fn>).mockClear();
90
+ (instance.warn as ReturnType<typeof vi.fn>).mockClear();
91
+ (instance.error as ReturnType<typeof vi.fn>).mockClear();
92
+ (instance.fatal as ReturnType<typeof vi.fn>).mockClear();
93
+ },
94
+ };
95
+
96
+ return instance;
97
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @module @kb-labs/shared-testing/mock-storage
3
+ *
4
+ * Virtual filesystem mock for IStorage interface.
5
+ * All methods are vi.fn() spies.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const storage = mockStorage({
10
+ * 'config.json': '{"key": "value"}',
11
+ * 'data/file.txt': 'hello world',
12
+ * });
13
+ *
14
+ * const content = await storage.read('config.json');
15
+ * expect(content?.toString()).toBe('{"key": "value"}');
16
+ * expect(storage.read).toHaveBeenCalledWith('config.json');
17
+ * ```
18
+ */
19
+
20
+ import { vi } from 'vitest';
21
+ import type { IStorage } from '@kb-labs/core-platform';
22
+
23
+ /**
24
+ * Mock storage instance with in-memory virtual filesystem.
25
+ */
26
+ export interface MockStorageInstance extends IStorage {
27
+ /** Direct access to the virtual filesystem (for assertions) */
28
+ readonly files: Map<string, Buffer>;
29
+ /** Reset all files and spy call history */
30
+ reset: () => void;
31
+ }
32
+
33
+ /**
34
+ * Create a mock storage with in-memory virtual filesystem.
35
+ *
36
+ * @param initial - Optional initial files. Keys are paths, values are content (string or Buffer).
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const storage = mockStorage({
41
+ * 'data.json': JSON.stringify({ items: [] }),
42
+ * 'binary.dat': Buffer.from([0x00, 0x01]),
43
+ * });
44
+ *
45
+ * // Write and read
46
+ * await storage.write('new.txt', Buffer.from('hello'));
47
+ * expect(await storage.exists('new.txt')).toBe(true);
48
+ *
49
+ * // List files
50
+ * const files = await storage.list('');
51
+ * expect(files).toContain('new.txt');
52
+ * ```
53
+ */
54
+ export function mockStorage(initial?: Record<string, string | Buffer>): MockStorageInstance {
55
+ const files = new Map<string, Buffer>();
56
+
57
+ // Populate initial files
58
+ if (initial) {
59
+ for (const [path, content] of Object.entries(initial)) {
60
+ files.set(path, typeof content === 'string' ? Buffer.from(content) : content);
61
+ }
62
+ }
63
+
64
+ const readFn = vi.fn(async (path: string): Promise<Buffer | null> => {
65
+ return files.get(path) ?? null;
66
+ });
67
+
68
+ const writeFn = vi.fn(async (path: string, data: Buffer): Promise<void> => {
69
+ files.set(path, data);
70
+ });
71
+
72
+ const deleteFn = vi.fn(async (path: string): Promise<void> => {
73
+ files.delete(path);
74
+ });
75
+
76
+ const listFn = vi.fn(async (prefix: string): Promise<string[]> => {
77
+ const result: string[] = [];
78
+ for (const path of files.keys()) {
79
+ if (path.startsWith(prefix)) {
80
+ result.push(path);
81
+ }
82
+ }
83
+ return result.sort();
84
+ });
85
+
86
+ const existsFn = vi.fn(async (path: string): Promise<boolean> => {
87
+ return files.has(path);
88
+ });
89
+
90
+ const instance: MockStorageInstance = {
91
+ read: readFn,
92
+ write: writeFn,
93
+ delete: deleteFn,
94
+ list: listFn,
95
+ exists: existsFn,
96
+ get files() { return files; },
97
+ reset: () => {
98
+ files.clear();
99
+ readFn.mockClear();
100
+ writeFn.mockClear();
101
+ deleteFn.mockClear();
102
+ listFn.mockClear();
103
+ existsFn.mockClear();
104
+ },
105
+ };
106
+
107
+ return instance;
108
+ }