@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,294 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { defineCommand } from '../index';
3
+
4
+ describe('defineCommand', () => {
5
+ let mockCtx: any; // PluginContextV3
6
+ let mockUI: {
7
+ write: ReturnType<typeof vi.fn>;
8
+ json: ReturnType<typeof vi.fn>;
9
+ error: ReturnType<typeof vi.fn>;
10
+ info: ReturnType<typeof vi.fn>;
11
+ success: ReturnType<typeof vi.fn>;
12
+ warn: ReturnType<typeof vi.fn>;
13
+ };
14
+ let mockLogger: {
15
+ info: ReturnType<typeof vi.fn>;
16
+ error: ReturnType<typeof vi.fn>;
17
+ };
18
+
19
+ beforeEach(() => {
20
+ mockUI = {
21
+ write: vi.fn(),
22
+ json: vi.fn(),
23
+ error: vi.fn(),
24
+ info: vi.fn(),
25
+ success: vi.fn(),
26
+ warn: vi.fn(),
27
+ };
28
+
29
+ mockLogger = {
30
+ info: vi.fn(),
31
+ error: vi.fn(),
32
+ };
33
+
34
+ // PluginContextV3 structure
35
+ mockCtx = {
36
+ host: 'cli',
37
+ requestId: 'test-request-id',
38
+ pluginId: '@kb-labs/test',
39
+ cwd: '/test',
40
+ ui: mockUI,
41
+ platform: {
42
+ logger: mockLogger as unknown as any,
43
+ llm: {} as any,
44
+ embeddings: {} as any,
45
+ vectorStore: {} as any,
46
+ cache: {} as any,
47
+ storage: {} as any,
48
+ analytics: {} as any,
49
+ },
50
+ runtime: {
51
+ fs: {} as any,
52
+ fetch: vi.fn(),
53
+ env: vi.fn(),
54
+ } as any,
55
+ api: {} as any,
56
+ trace: {
57
+ traceId: 'test-trace-id',
58
+ spanId: 'test-span-id',
59
+ },
60
+ };
61
+ });
62
+
63
+ it('should call handler with correct input (V3 API)', async () => {
64
+ const handler = vi.fn().mockResolvedValue({ exitCode: 0, ok: true, result: 'success' });
65
+
66
+ const command = defineCommand({
67
+ id: 'test:command',
68
+ description: 'Test command',
69
+ handler: {
70
+ execute: handler,
71
+ },
72
+ });
73
+
74
+ const result = await command.execute(mockCtx, { flags: { name: 'test' }, argv: [] });
75
+
76
+ expect(result.exitCode).toBe(0);
77
+ expect(result.exitCode).toBe(0);
78
+ expect(handler).toHaveBeenCalledOnce();
79
+ expect(handler.mock.calls[0]?.[0]).toBe(mockCtx);
80
+ expect(handler.mock.calls[0]?.[1]).toEqual({ flags: { name: 'test' }, argv: [] });
81
+ });
82
+
83
+ it('should enforce CLI host restriction', async () => {
84
+ const handler = vi.fn();
85
+
86
+ const command = defineCommand({
87
+ id: 'test:command',
88
+ description: 'Test command',
89
+ handler: {
90
+ execute: handler,
91
+ },
92
+ });
93
+
94
+ // Change host to REST
95
+ mockCtx.host = 'rest';
96
+
97
+ try {
98
+ await command.execute(mockCtx, { flags: {}, argv: [] });
99
+ expect.fail('Should have thrown an error');
100
+ } catch (error: any) {
101
+ expect(error.message).toContain('can only run in CLI or workflow host');
102
+ }
103
+
104
+ expect(handler).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it('should allow workflow host', async () => {
108
+ const handler = vi.fn().mockResolvedValue({ exitCode: 0, ok: true });
109
+
110
+ const command = defineCommand({
111
+ id: 'test:command',
112
+ description: 'Test command',
113
+ handler: {
114
+ execute: handler,
115
+ },
116
+ });
117
+
118
+ // Change host to workflow
119
+ mockCtx.host = 'workflow';
120
+
121
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
122
+
123
+ expect(result.exitCode).toBe(0);
124
+ expect(handler).toHaveBeenCalledOnce();
125
+ });
126
+
127
+ it('should handle handler returning number', async () => {
128
+ const handler = vi.fn().mockResolvedValue({ exitCode: 0, ok: true });
129
+
130
+ const command = defineCommand({
131
+ id: 'test:command',
132
+ description: 'Test command',
133
+ handler: {
134
+ execute: handler,
135
+ },
136
+ });
137
+
138
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
139
+
140
+ expect(result.exitCode).toBe(0);
141
+ expect(result.exitCode).toBe(0);
142
+ });
143
+
144
+ it('should handle handler returning object with ok', async () => {
145
+ const handler = vi.fn().mockResolvedValue({ exitCode: 0, ok: true, data: 'test' });
146
+
147
+ const command = defineCommand({
148
+ id: 'test:command',
149
+ description: 'Test command',
150
+ handler: {
151
+ execute: handler,
152
+ },
153
+ });
154
+
155
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
156
+
157
+ expect(result.exitCode).toBe(0);
158
+ expect(result.exitCode).toBe(0);
159
+ });
160
+
161
+ it('should handle errors in handler', async () => {
162
+ const handler = vi.fn().mockRejectedValue(new Error('Handler error'));
163
+
164
+ const command = defineCommand({
165
+ id: 'test:command',
166
+ description: 'Test command',
167
+ handler: {
168
+ execute: handler,
169
+ },
170
+ });
171
+
172
+ await expect(
173
+ command.execute(mockCtx, { flags: {}, argv: [] })
174
+ ).rejects.toThrow('Handler error');
175
+ });
176
+
177
+ it('should call cleanup if provided', async () => {
178
+ const handler = vi.fn().mockResolvedValue({ exitCode: 0, ok: true });
179
+ const cleanup = vi.fn().mockResolvedValue(undefined);
180
+
181
+ const command = defineCommand({
182
+ id: 'test:command',
183
+ description: 'Test command',
184
+ handler: {
185
+ execute: handler,
186
+ cleanup,
187
+ },
188
+ });
189
+
190
+ await command.execute(mockCtx, { flags: {}, argv: [] });
191
+
192
+ // Cleanup should be available
193
+ expect(command.cleanup).toBe(cleanup);
194
+
195
+ // Call cleanup manually
196
+ await command.cleanup!();
197
+ expect(cleanup).toHaveBeenCalledOnce();
198
+ });
199
+
200
+ it('should handle handler returning exit code 2', async () => {
201
+ const handler = vi.fn().mockResolvedValue({ exitCode: 2, ok: false });
202
+
203
+ const command = defineCommand({
204
+ id: 'test:command',
205
+ description: 'Test command',
206
+ handler: {
207
+ execute: handler,
208
+ },
209
+ });
210
+
211
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
212
+
213
+ expect(result.exitCode).toBe(2);
214
+ expect(result.exitCode).not.toBe(0);
215
+ });
216
+
217
+ it('should handle handler returning object with ok: false', async () => {
218
+ const handler = vi.fn().mockResolvedValue({ exitCode: 1, ok: false });
219
+
220
+ const command = defineCommand({
221
+ id: 'test:command',
222
+ description: 'Test command',
223
+ handler: {
224
+ execute: handler,
225
+ },
226
+ });
227
+
228
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
229
+
230
+ expect(result.exitCode).toBe(1);
231
+ expect(result.exitCode).not.toBe(0);
232
+ });
233
+
234
+ it('should pass through custom result fields', async () => {
235
+ const handler = vi.fn().mockResolvedValue({
236
+ exitCode: 0,
237
+ ok: true,
238
+ customField: 'custom-value',
239
+ data: { nested: 'data' },
240
+ });
241
+
242
+ const command = defineCommand({
243
+ id: 'test:command',
244
+ description: 'Test command',
245
+ handler: {
246
+ execute: handler,
247
+ },
248
+ });
249
+
250
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] }) as any;
251
+
252
+ expect(result.exitCode).toBe(0);
253
+ expect(result.exitCode).toBe(0);
254
+ expect(result.customField).toBe('custom-value');
255
+ expect(result.data).toEqual({ nested: 'data' });
256
+ });
257
+
258
+ it('should work with async handlers', async () => {
259
+ const handler = vi.fn(async () => {
260
+ await new Promise<void>(resolve => { setTimeout(resolve, 10); });
261
+ return { exitCode: 0, ok: true };
262
+ });
263
+
264
+ const command = defineCommand({
265
+ id: 'test:command',
266
+ description: 'Test command',
267
+ handler: {
268
+ execute: handler,
269
+ },
270
+ });
271
+
272
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
273
+
274
+ expect(result.exitCode).toBe(0);
275
+ expect(result.exitCode).toBe(0);
276
+ });
277
+
278
+ it('should work with sync handlers', async () => {
279
+ const handler = vi.fn(() => ({ exitCode: 0, ok: true }));
280
+
281
+ const command = defineCommand({
282
+ id: 'test:command',
283
+ description: 'Test command',
284
+ handler: {
285
+ execute: handler,
286
+ },
287
+ });
288
+
289
+ const result = await command.execute(mockCtx, { flags: {}, argv: [] });
290
+
291
+ expect(result.exitCode).toBe(0);
292
+ expect(result.exitCode).toBe(0);
293
+ });
294
+ });
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @file Unit tests for defineRoute
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { defineRoute, isRESTHost } from '../define-route.js';
7
+ import type { PluginContextV3, RestHostContext } from '@kb-labs/plugin-contracts';
8
+
9
+ describe('defineRoute', () => {
10
+ let mockContext: PluginContextV3<unknown>;
11
+ let mockLogger: any;
12
+
13
+ beforeEach(() => {
14
+ mockLogger = {
15
+ info: vi.fn(),
16
+ error: vi.fn(),
17
+ warn: vi.fn(),
18
+ debug: vi.fn(),
19
+ };
20
+
21
+ const restHostContext: RestHostContext = {
22
+ host: 'rest',
23
+ path: '/api/test',
24
+ method: 'POST',
25
+ headers: {},
26
+ requestId: 'req-123',
27
+ traceId: 'trace-123',
28
+ };
29
+
30
+ mockContext = {
31
+ host: 'rest',
32
+ hostContext: restHostContext,
33
+ ui: {} as any,
34
+ platform: {} as any,
35
+ config: {},
36
+ } as any;
37
+ });
38
+
39
+ describe('host validation', () => {
40
+ it('should throw error if host is not rest', async () => {
41
+ const handler = defineRoute({
42
+ path: '/api/test',
43
+ method: 'POST',
44
+ handler: {
45
+ async execute() {
46
+ return { data: {}, exitCode: 0 };
47
+ },
48
+ },
49
+ });
50
+
51
+ const invalidContext = {
52
+ ...mockContext,
53
+ host: 'cli' as const,
54
+ hostContext: { host: 'cli' as const } as any,
55
+ };
56
+
57
+ await expect(
58
+ handler.execute(invalidContext, {})
59
+ ).rejects.toThrow('can only run in REST host');
60
+ });
61
+
62
+ it('should throw error if HTTP method mismatch', async () => {
63
+ const handler = defineRoute({
64
+ path: '/api/test',
65
+ method: 'POST',
66
+ handler: {
67
+ async execute() {
68
+ return { data: {}, exitCode: 0 };
69
+ },
70
+ },
71
+ });
72
+
73
+ const wrongMethodContext = {
74
+ ...mockContext,
75
+ hostContext: {
76
+ ...mockContext.hostContext,
77
+ method: 'GET',
78
+ } as RestHostContext,
79
+ };
80
+
81
+ await expect(
82
+ handler.execute(wrongMethodContext, {})
83
+ ).rejects.toThrow('expects POST but got GET');
84
+ });
85
+
86
+ it('should be case-insensitive for HTTP methods', async () => {
87
+ const handler = defineRoute({
88
+ path: '/api/test',
89
+ method: 'post',
90
+ handler: {
91
+ async execute() {
92
+ return { data: {}, exitCode: 0 };
93
+ },
94
+ },
95
+ });
96
+
97
+ const upperCaseContext = {
98
+ ...mockContext,
99
+ hostContext: {
100
+ ...mockContext.hostContext,
101
+ method: 'POST',
102
+ } as RestHostContext,
103
+ };
104
+
105
+ await expect(
106
+ handler.execute(upperCaseContext, {})
107
+ ).resolves.toBeDefined();
108
+ });
109
+
110
+ it('should accept valid REST host context', async () => {
111
+ const execute = vi.fn(async () => ({ data: {}, exitCode: 0 }));
112
+
113
+ const handler = defineRoute({
114
+ path: '/api/test',
115
+ method: 'POST',
116
+ handler: { execute },
117
+ });
118
+
119
+ await handler.execute(mockContext, {});
120
+
121
+ expect(execute).toHaveBeenCalled();
122
+ });
123
+ });
124
+
125
+ describe('handler execution', () => {
126
+ it('should call execute with context and input', async () => {
127
+ const execute = vi.fn(async (ctx, input) => ({ data: { received: input }, exitCode: 0 }));
128
+
129
+ const handler = defineRoute({
130
+ path: '/api/test',
131
+ method: 'POST',
132
+ handler: { execute },
133
+ });
134
+
135
+ const input = { name: 'test' };
136
+ await handler.execute(mockContext, input);
137
+
138
+ expect(execute).toHaveBeenCalledWith(mockContext, input);
139
+ });
140
+
141
+ it('should return handler result', async () => {
142
+ const expectedResult = { data: { message: 'success' }, exitCode: 0 };
143
+
144
+ const handler = defineRoute({
145
+ path: '/api/test',
146
+ method: 'POST',
147
+ handler: {
148
+ async execute() {
149
+ return expectedResult;
150
+ },
151
+ },
152
+ });
153
+
154
+ const result = await handler.execute(mockContext, {});
155
+
156
+ expect(result).toEqual(expectedResult);
157
+ });
158
+
159
+ it('should handle void return from handler', async () => {
160
+ const handler = defineRoute({
161
+ path: '/api/test',
162
+ method: 'POST',
163
+ handler: {
164
+ async execute() {
165
+ // void return
166
+ },
167
+ },
168
+ });
169
+
170
+ const result = await handler.execute(mockContext, {});
171
+
172
+ expect(result).toBeUndefined();
173
+ });
174
+ });
175
+
176
+ describe('cleanup', () => {
177
+ it('should call cleanup after successful execution', async () => {
178
+ const cleanup = vi.fn();
179
+
180
+ const handler = defineRoute({
181
+ path: '/api/test',
182
+ method: 'POST',
183
+ handler: {
184
+ async execute() {
185
+ return { data: {}, exitCode: 0 };
186
+ },
187
+ cleanup,
188
+ },
189
+ });
190
+
191
+ await handler.execute(mockContext, {});
192
+
193
+ expect(cleanup).toHaveBeenCalled();
194
+ });
195
+
196
+ it('should call cleanup even if handler throws', async () => {
197
+ const cleanup = vi.fn();
198
+
199
+ const handler = defineRoute({
200
+ path: '/api/test',
201
+ method: 'POST',
202
+ handler: {
203
+ async execute() {
204
+ throw new Error('Handler error');
205
+ },
206
+ cleanup,
207
+ },
208
+ });
209
+
210
+ await expect(handler.execute(mockContext, {})).rejects.toThrow('Handler error');
211
+
212
+ expect(cleanup).toHaveBeenCalled();
213
+ });
214
+
215
+ it('should not call cleanup if validation fails', async () => {
216
+ const cleanup = vi.fn();
217
+ const execute = vi.fn();
218
+
219
+ const handler = defineRoute({
220
+ path: '/api/test',
221
+ method: 'POST',
222
+ handler: {
223
+ execute,
224
+ cleanup,
225
+ },
226
+ });
227
+
228
+ const invalidContext = {
229
+ ...mockContext,
230
+ host: 'cli' as const,
231
+ hostContext: { host: 'cli' as const } as any,
232
+ };
233
+
234
+ await expect(handler.execute(invalidContext, {})).rejects.toThrow();
235
+
236
+ expect(cleanup).not.toHaveBeenCalled();
237
+ expect(execute).not.toHaveBeenCalled();
238
+ });
239
+
240
+ it('should propagate handler error after cleanup', async () => {
241
+ const cleanup = vi.fn();
242
+ const error = new Error('Handler error');
243
+
244
+ const handler = defineRoute({
245
+ path: '/api/test',
246
+ method: 'POST',
247
+ handler: {
248
+ async execute() {
249
+ throw error;
250
+ },
251
+ cleanup,
252
+ },
253
+ });
254
+
255
+ await expect(handler.execute(mockContext, {})).rejects.toThrow(error);
256
+
257
+ expect(cleanup).toHaveBeenCalled();
258
+ });
259
+ });
260
+ });
261
+
262
+ describe('isRESTHost', () => {
263
+ it('should return true for REST host context', () => {
264
+ const restContext: RestHostContext = {
265
+ host: 'rest',
266
+ path: '/api/test',
267
+ method: 'POST',
268
+ headers: {},
269
+ requestId: 'req-123',
270
+ traceId: 'trace-123',
271
+ };
272
+
273
+ expect(isRESTHost(restContext)).toBe(true);
274
+ });
275
+
276
+ it('should return false for non-REST host context', () => {
277
+ const cliContext = {
278
+ host: 'cli' as const,
279
+ argv: [],
280
+ flags: {},
281
+ };
282
+
283
+ expect(isRESTHost(cliContext)).toBe(false);
284
+ });
285
+ });