@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,316 @@
1
+ /**
2
+ * @file Unit tests for defineWebSocket and message validation
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { defineWebSocket, isWSHost } from '../define-websocket.js';
7
+ import type { PluginContextV3, WebSocketHostContext, WSInput, WSSender } from '@kb-labs/plugin-contracts';
8
+
9
+ describe('defineWebSocket', () => {
10
+ let mockContext: PluginContextV3<unknown>;
11
+ let mockSender: WSSender;
12
+ let mockLogger: any;
13
+
14
+ beforeEach(() => {
15
+ mockLogger = {
16
+ info: vi.fn(),
17
+ error: vi.fn(),
18
+ warn: vi.fn(),
19
+ debug: vi.fn(),
20
+ };
21
+
22
+ const wsHostContext: WebSocketHostContext = {
23
+ host: 'ws',
24
+ channelPath: '/test',
25
+ connectionId: 'conn-123',
26
+ clientIp: '127.0.0.1',
27
+ headers: {},
28
+ requestId: 'req-123',
29
+ traceId: 'trace-123',
30
+ };
31
+
32
+ mockContext = {
33
+ host: 'ws',
34
+ hostContext: wsHostContext,
35
+ ui: {} as any,
36
+ platform: {
37
+ logger: mockLogger,
38
+ } as any,
39
+ config: {},
40
+ } as any;
41
+
42
+ mockSender = {
43
+ send: vi.fn(),
44
+ broadcast: vi.fn(),
45
+ sendTo: vi.fn(),
46
+ close: vi.fn(),
47
+ getConnectionId: vi.fn(() => 'conn-123'),
48
+ };
49
+ });
50
+
51
+ describe('host validation', () => {
52
+ it('should throw error if host is not ws', async () => {
53
+ const handler = defineWebSocket({
54
+ path: '/test',
55
+ handler: {
56
+ async onConnect() {},
57
+ },
58
+ });
59
+
60
+ const invalidContext = {
61
+ ...mockContext,
62
+ host: 'cli' as const,
63
+ hostContext: { host: 'cli' as const } as any,
64
+ };
65
+
66
+ const input: WSInput = { event: 'connect', sender: mockSender };
67
+
68
+ await expect(
69
+ handler.execute(invalidContext, input)
70
+ ).rejects.toThrow('can only run in ws host');
71
+ });
72
+
73
+ it('should throw error if channel path mismatch', async () => {
74
+ const handler = defineWebSocket({
75
+ path: '/expected',
76
+ handler: {
77
+ async onConnect() {},
78
+ },
79
+ });
80
+
81
+ const wrongPathContext = {
82
+ ...mockContext,
83
+ hostContext: {
84
+ ...mockContext.hostContext,
85
+ channelPath: '/wrong',
86
+ } as WebSocketHostContext,
87
+ };
88
+
89
+ const input: WSInput = { event: 'connect', sender: mockSender };
90
+
91
+ await expect(
92
+ handler.execute(wrongPathContext, input)
93
+ ).rejects.toThrow('expects channel /expected but got /wrong');
94
+ });
95
+
96
+ it('should throw error if sender not provided for connect event', async () => {
97
+ const handler = defineWebSocket({
98
+ path: '/test',
99
+ handler: {
100
+ async onConnect() {},
101
+ },
102
+ });
103
+
104
+ const input: WSInput = { event: 'connect' }; // no sender
105
+
106
+ await expect(
107
+ handler.execute(mockContext, input)
108
+ ).rejects.toThrow('WebSocket sender not provided in input');
109
+ });
110
+
111
+ it('should accept valid ws host context with sender', async () => {
112
+ const onConnect = vi.fn();
113
+
114
+ const handler = defineWebSocket({
115
+ path: '/test',
116
+ handler: { onConnect },
117
+ });
118
+
119
+ const input: WSInput = { event: 'connect', sender: mockSender };
120
+
121
+ await handler.execute(mockContext, input);
122
+
123
+ expect(onConnect).toHaveBeenCalled();
124
+ });
125
+ });
126
+
127
+ describe('message validation', () => {
128
+ it('should validate message structure', async () => {
129
+ const onMessage = vi.fn();
130
+
131
+ const handler = defineWebSocket({
132
+ path: '/test',
133
+ handler: { onMessage },
134
+ });
135
+
136
+ const validMessage = {
137
+ type: 'test',
138
+ payload: { data: 'hello' },
139
+ timestamp: Date.now(),
140
+ };
141
+
142
+ const input: WSInput = {
143
+ event: 'message',
144
+ message: validMessage,
145
+ sender: mockSender,
146
+ };
147
+
148
+ await handler.execute(mockContext, input);
149
+
150
+ expect(onMessage).toHaveBeenCalled();
151
+ expect(mockLogger.error).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('should reject message without type field', async () => {
155
+ const onMessage = vi.fn();
156
+
157
+ const handler = defineWebSocket({
158
+ path: '/test',
159
+ handler: { onMessage },
160
+ });
161
+
162
+ const invalidMessage = {
163
+ payload: { data: 'hello' },
164
+ timestamp: Date.now(),
165
+ } as any;
166
+
167
+ const input: WSInput = {
168
+ event: 'message',
169
+ message: invalidMessage,
170
+ sender: mockSender,
171
+ };
172
+
173
+ await handler.execute(mockContext, input);
174
+
175
+ expect(onMessage).not.toHaveBeenCalled();
176
+ expect(mockContext.platform.logger.error).toHaveBeenCalled();
177
+ expect(mockSender.send).toHaveBeenCalledWith({
178
+ type: 'error',
179
+ payload: { error: 'Invalid message format' },
180
+ timestamp: expect.any(Number),
181
+ });
182
+ });
183
+
184
+ it('should reject message with empty type', async () => {
185
+ const onMessage = vi.fn();
186
+
187
+ const handler = defineWebSocket({
188
+ path: '/test',
189
+ handler: { onMessage },
190
+ });
191
+
192
+ const invalidMessage = {
193
+ type: '',
194
+ payload: {},
195
+ timestamp: Date.now(),
196
+ };
197
+
198
+ const input: WSInput = {
199
+ event: 'message',
200
+ message: invalidMessage,
201
+ sender: mockSender,
202
+ };
203
+
204
+ await handler.execute(mockContext, input);
205
+
206
+ expect(onMessage).not.toHaveBeenCalled();
207
+ expect(mockLogger.error).toHaveBeenCalled();
208
+ });
209
+ });
210
+
211
+ describe('lifecycle events', () => {
212
+ it('should call onConnect handler', async () => {
213
+ const onConnect = vi.fn();
214
+
215
+ const handler = defineWebSocket({
216
+ path: '/test',
217
+ handler: { onConnect },
218
+ });
219
+
220
+ const input: WSInput = { event: 'connect', sender: mockSender };
221
+
222
+ await handler.execute(mockContext, input);
223
+
224
+ expect(onConnect).toHaveBeenCalled();
225
+ });
226
+
227
+ it('should call onDisconnect handler without sender', async () => {
228
+ const onDisconnect = vi.fn();
229
+
230
+ const handler = defineWebSocket({
231
+ path: '/test',
232
+ handler: { onDisconnect },
233
+ });
234
+
235
+ // disconnect event doesn't need sender
236
+ const input: WSInput = {
237
+ event: 'disconnect',
238
+ disconnectCode: 1000,
239
+ disconnectReason: 'Normal closure',
240
+ };
241
+
242
+ await handler.execute(mockContext, input);
243
+
244
+ expect(onDisconnect).toHaveBeenCalledWith(mockContext, 1000, 'Normal closure');
245
+ });
246
+ });
247
+
248
+ describe('cleanup', () => {
249
+ it('should call cleanup after successful execution', async () => {
250
+ const cleanup = vi.fn();
251
+
252
+ const handler = defineWebSocket({
253
+ path: '/test',
254
+ handler: {
255
+ async onConnect() {},
256
+ cleanup,
257
+ },
258
+ });
259
+
260
+ const input: WSInput = { event: 'connect', sender: mockSender };
261
+
262
+ await handler.execute(mockContext, input);
263
+
264
+ expect(cleanup).toHaveBeenCalled();
265
+ });
266
+
267
+ it('should call cleanup even if handler throws', async () => {
268
+ const cleanup = vi.fn();
269
+
270
+ const handler = defineWebSocket({
271
+ path: '/test',
272
+ handler: {
273
+ async onMessage() {
274
+ throw new Error('Handler error');
275
+ },
276
+ cleanup,
277
+ },
278
+ });
279
+
280
+ const message = { type: 'test', payload: {}, timestamp: Date.now() };
281
+ const input: WSInput = { event: 'message', message, sender: mockSender };
282
+
283
+ await expect(handler.execute(mockContext, input)).rejects.toThrow(
284
+ 'Handler error'
285
+ );
286
+
287
+ expect(cleanup).toHaveBeenCalled();
288
+ });
289
+ });
290
+ });
291
+
292
+ describe('isWSHost', () => {
293
+ it('should return true for ws host context', () => {
294
+ const wsContext: WebSocketHostContext = {
295
+ host: 'ws',
296
+ channelPath: '/test',
297
+ connectionId: 'conn-123',
298
+ clientIp: '127.0.0.1',
299
+ headers: {},
300
+ requestId: 'req-123',
301
+ traceId: 'trace-123',
302
+ };
303
+
304
+ expect(isWSHost(wsContext)).toBe(true);
305
+ });
306
+
307
+ it('should return false for non-ws host context', () => {
308
+ const cliContext = {
309
+ host: 'cli' as const,
310
+ argv: [],
311
+ flags: {},
312
+ };
313
+
314
+ expect(isWSHost(cliContext)).toBe(false);
315
+ });
316
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatError } from '../errors/index';
3
+
4
+ describe('formatError', () => {
5
+ it('should format Error object', () => {
6
+ const error = new Error('Something went wrong');
7
+ const formatted = formatError(error);
8
+
9
+ expect(formatted.message).toBe('Something went wrong');
10
+ expect(formatted.json.ok).toBe(false);
11
+ expect(formatted.json.error).toBe('Something went wrong');
12
+ });
13
+
14
+ it('should format string error', () => {
15
+ const formatted = formatError('String error');
16
+
17
+ expect(formatted.message).toBe('String error');
18
+ expect(formatted.json.error).toBe('String error');
19
+ });
20
+
21
+ it('should include stack trace when showStack is true', () => {
22
+ const error = new Error('Test error');
23
+ const formatted = formatError(error, { showStack: true });
24
+
25
+ expect(formatted.message).toContain('Test error');
26
+ expect(formatted.message).toContain('Error: Test error');
27
+ expect(formatted.json.stack).toBeDefined();
28
+ });
29
+
30
+ it('should include timing when provided', () => {
31
+ const error = new Error('Test error');
32
+ const formatted = formatError(error, { timingMs: 1234 });
33
+
34
+ expect(formatted.json.timingMs).toBe(1234);
35
+ });
36
+
37
+ it('should not include stack when showStack is false', () => {
38
+ const error = new Error('Test error');
39
+ const formatted = formatError(error, { showStack: false });
40
+
41
+ expect(formatted.message).toBe('Test error');
42
+ expect(formatted.json.stack).toBeUndefined();
43
+ });
44
+ });
45
+
@@ -0,0 +1,353 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineFlags, validateFlags, validateFlagsSafe, FlagValidationError } from '../flags/index';
3
+
4
+ describe('defineFlags', () => {
5
+ it('should create flag schema with type inference', () => {
6
+ const schema = defineFlags({
7
+ scope: {
8
+ type: 'string',
9
+ required: true,
10
+ },
11
+ 'dry-run': {
12
+ type: 'boolean',
13
+ default: false,
14
+ },
15
+ });
16
+
17
+ expect(schema.schema).toBeDefined();
18
+ expect(schema.schema.scope).toBeDefined();
19
+ expect(schema.schema['dry-run']).toBeDefined();
20
+ });
21
+ });
22
+
23
+ describe('validateFlags', () => {
24
+ it('should validate boolean flags', async () => {
25
+ const schema = defineFlags({
26
+ verbose: { type: 'boolean', default: false },
27
+ quiet: { type: 'boolean' },
28
+ });
29
+
30
+ const flags = await validateFlags({ verbose: true, quiet: false }, schema);
31
+ expect(flags.verbose).toBe(true);
32
+ expect(flags.quiet).toBe(false);
33
+ });
34
+
35
+ it('should validate string flags', async () => {
36
+ const schema = defineFlags({
37
+ name: { type: 'string', required: true },
38
+ format: { type: 'string', default: 'json' },
39
+ });
40
+
41
+ const flags = await validateFlags({ name: 'test', format: 'yaml' }, schema);
42
+ expect(flags.name).toBe('test');
43
+ expect(flags.format).toBe('yaml');
44
+ });
45
+
46
+ it('should validate number flags', async () => {
47
+ const schema = defineFlags({
48
+ limit: { type: 'number', min: 1, max: 100, default: 10 },
49
+ port: { type: 'number' },
50
+ });
51
+
52
+ const flags = await validateFlags({ limit: 50, port: 8080 }, schema);
53
+ expect(flags.limit).toBe(50);
54
+ expect(flags.port).toBe(8080);
55
+ });
56
+
57
+ it('should validate array flags', async () => {
58
+ const schema = defineFlags({
59
+ tags: { type: 'array', items: 'string' },
60
+ numbers: { type: 'array', items: 'number' },
61
+ });
62
+
63
+ const flags = await validateFlags(
64
+ { tags: ['a', 'b'], numbers: [1, 2, 3] },
65
+ schema
66
+ );
67
+ expect(flags.tags).toEqual(['a', 'b']);
68
+ expect(flags.numbers).toEqual([1, 2, 3]);
69
+ });
70
+
71
+ it('should apply default values', async () => {
72
+ const schema = defineFlags({
73
+ verbose: { type: 'boolean', default: false },
74
+ limit: { type: 'number', default: 10 },
75
+ format: { type: 'string', default: 'json' },
76
+ });
77
+
78
+ const flags = await validateFlags({}, schema);
79
+ expect(flags.verbose).toBe(false);
80
+ expect(flags.limit).toBe(10);
81
+ expect(flags.format).toBe('json');
82
+ });
83
+
84
+ it('should throw error for required flags', async () => {
85
+ const schema = defineFlags({
86
+ name: { type: 'string', required: true },
87
+ });
88
+
89
+ await expect(validateFlags({}, schema)).rejects.toThrow(FlagValidationError);
90
+ });
91
+
92
+ it('should validate choices', async () => {
93
+ const schema = defineFlags({
94
+ format: {
95
+ type: 'string',
96
+ choices: ['json', 'yaml', 'toml'] as const,
97
+ },
98
+ });
99
+
100
+ const flags = await validateFlags({ format: 'json' }, schema);
101
+ expect(flags.format).toBe('json');
102
+
103
+ await expect(validateFlags({ format: 'xml' }, schema)).rejects.toThrow();
104
+ });
105
+
106
+ it('should validate pattern', async () => {
107
+ const schema = defineFlags({
108
+ scope: {
109
+ type: 'string',
110
+ pattern: /^[@a-z0-9-/]+$/i,
111
+ },
112
+ });
113
+
114
+ const flags = await validateFlags({ scope: '@my-org/package' }, schema);
115
+ expect(flags.scope).toBe('@my-org/package');
116
+
117
+ await expect(validateFlags({ scope: 'invalid scope!' }, schema)).rejects.toThrow();
118
+ });
119
+
120
+ it('should validate min/max for numbers', async () => {
121
+ const schema = defineFlags({
122
+ limit: { type: 'number', min: 1, max: 100 },
123
+ });
124
+
125
+ await expect(validateFlags({ limit: 0 }, schema)).rejects.toThrow();
126
+ await expect(validateFlags({ limit: 101 }, schema)).rejects.toThrow();
127
+
128
+ const flags = await validateFlags({ limit: 50 }, schema);
129
+ expect(flags.limit).toBe(50);
130
+ });
131
+
132
+ it('should coerce string to boolean', async () => {
133
+ const schema = defineFlags({
134
+ verbose: { type: 'boolean' },
135
+ });
136
+
137
+ const flags1 = await validateFlags({ verbose: 'true' }, schema);
138
+ expect(flags1.verbose).toBe(true);
139
+
140
+ const flags2 = await validateFlags({ verbose: 'false' }, schema);
141
+ expect(flags2.verbose).toBe(false);
142
+ });
143
+
144
+ it('should coerce string to number', async () => {
145
+ const schema = defineFlags({
146
+ limit: { type: 'number' },
147
+ });
148
+
149
+ const flags = await validateFlags({ limit: '42' }, schema);
150
+ expect(flags.limit).toBe(42);
151
+ });
152
+
153
+ it('should coerce comma-separated string to array', async () => {
154
+ const schema = defineFlags({
155
+ tags: { type: 'array', items: 'string' },
156
+ });
157
+
158
+ const flags = await validateFlags({ tags: 'a,b,c' }, schema);
159
+ expect(flags.tags).toEqual(['a', 'b', 'c']);
160
+ });
161
+ });
162
+
163
+ describe('validateFlagsSafe', () => {
164
+ it('should return success result for valid flags', async () => {
165
+ const schema = defineFlags({
166
+ name: { type: 'string', required: true },
167
+ });
168
+
169
+ const result = await validateFlagsSafe({ name: 'test' }, schema);
170
+ expect(result.success).toBe(true);
171
+ expect(result.data).toBeDefined();
172
+ expect(result.data?.name).toBe('test');
173
+ expect(result.errors).toEqual([]);
174
+ });
175
+
176
+ it('should return error result for invalid flags', async () => {
177
+ const schema = defineFlags({
178
+ name: { type: 'string', required: true },
179
+ });
180
+
181
+ const result = await validateFlagsSafe({}, schema);
182
+ expect(result.success).toBe(false);
183
+ expect(result.errors).toHaveLength(1);
184
+ expect(result.errors[0]?.flag).toBe('name');
185
+ });
186
+ });
187
+
188
+ describe('validateFlags - advanced features', () => {
189
+ it('should validate conflicts', async () => {
190
+ const schema = defineFlags({
191
+ verbose: { type: 'boolean', conflicts: ['quiet'] },
192
+ quiet: { type: 'boolean', conflicts: ['verbose'] },
193
+ });
194
+
195
+ await expect(
196
+ validateFlags({ verbose: true, quiet: true }, schema)
197
+ ).rejects.toThrow();
198
+ });
199
+
200
+ it('should validate dependsOn', async () => {
201
+ const schema = defineFlags({
202
+ output: { type: 'string', dependsOn: ['format'] },
203
+ format: { type: 'string' },
204
+ });
205
+
206
+ await expect(
207
+ validateFlags({ output: 'file.txt' }, schema)
208
+ ).rejects.toThrow();
209
+
210
+ const flags = await validateFlags(
211
+ { output: 'file.txt', format: 'json' },
212
+ schema
213
+ );
214
+ expect(flags.output).toBe('file.txt');
215
+ });
216
+
217
+ it('should validate implies', async () => {
218
+ const schema = defineFlags({
219
+ verbose: { type: 'boolean', implies: ['debug'] },
220
+ debug: { type: 'boolean' },
221
+ });
222
+
223
+ const flags = await validateFlags({ verbose: true }, schema);
224
+ expect(flags.debug).toBe(true);
225
+ });
226
+
227
+ it('should apply custom validator', async () => {
228
+ const schema = defineFlags({
229
+ port: {
230
+ type: 'number',
231
+ validate: (value: number) => {
232
+ if (value < 1024 || value > 65535) {
233
+ return 'Port must be between 1024 and 65535';
234
+ }
235
+ return true;
236
+ },
237
+ },
238
+ });
239
+
240
+ await expect(validateFlags({ port: 80 }, schema)).rejects.toThrow();
241
+ await expect(validateFlags({ port: 70000 }, schema)).rejects.toThrow();
242
+
243
+ const flags = await validateFlags({ port: 8080 }, schema);
244
+ expect(flags.port).toBe(8080);
245
+ });
246
+
247
+ it('should apply async custom validator', async () => {
248
+ const schema = defineFlags({
249
+ name: {
250
+ type: 'string',
251
+ validate: async (value: string) => {
252
+ // Simulate async check
253
+ await new Promise<void>((resolve) => { setTimeout(resolve, 10); });
254
+ if (value.length < 3) {
255
+ return 'Name must be at least 3 characters';
256
+ }
257
+ return true;
258
+ },
259
+ },
260
+ });
261
+
262
+ await expect(validateFlags({ name: 'ab' }, schema)).rejects.toThrow();
263
+
264
+ const flags = await validateFlags({ name: 'test' }, schema);
265
+ expect(flags.name).toBe('test');
266
+ });
267
+
268
+ it('should apply transform', async () => {
269
+ const schema = defineFlags({
270
+ name: {
271
+ type: 'string',
272
+ transform: (value: string) => value.toUpperCase(),
273
+ },
274
+ });
275
+
276
+ const flags = await validateFlags({ name: 'test' }, schema);
277
+ expect(flags.name).toBe('TEST');
278
+ });
279
+
280
+ it('should apply async transform', async () => {
281
+ const schema = defineFlags({
282
+ name: {
283
+ type: 'string',
284
+ transform: async (value: string) => {
285
+ await new Promise<void>((resolve) => { setTimeout(resolve, 10); });
286
+ return value.trim().toLowerCase();
287
+ },
288
+ },
289
+ });
290
+
291
+ const flags = await validateFlags({ name: ' TEST ' }, schema);
292
+ expect(flags.name).toBe('test');
293
+ });
294
+
295
+ it('should validate array minLength', async () => {
296
+ const schema = defineFlags({
297
+ tags: {
298
+ type: 'array',
299
+ items: 'string',
300
+ minLength: 2,
301
+ },
302
+ });
303
+
304
+ await expect(validateFlags({ tags: ['a'] }, schema)).rejects.toThrow();
305
+
306
+ const flags = await validateFlags({ tags: ['a', 'b'] }, schema);
307
+ expect(flags.tags).toEqual(['a', 'b']);
308
+ });
309
+
310
+ it('should validate array maxLength', async () => {
311
+ const schema = defineFlags({
312
+ tags: {
313
+ type: 'array',
314
+ items: 'string',
315
+ maxLength: 2,
316
+ },
317
+ });
318
+
319
+ await expect(validateFlags({ tags: ['a', 'b', 'c'] }, schema)).rejects.toThrow();
320
+
321
+ const flags = await validateFlags({ tags: ['a', 'b'] }, schema);
322
+ expect(flags.tags).toEqual(['a', 'b']);
323
+ });
324
+
325
+ it('should validate string minLength', async () => {
326
+ const schema = defineFlags({
327
+ name: {
328
+ type: 'string',
329
+ minLength: 3,
330
+ },
331
+ });
332
+
333
+ await expect(validateFlags({ name: 'ab' }, schema)).rejects.toThrow();
334
+
335
+ const flags = await validateFlags({ name: 'abc' }, schema);
336
+ expect(flags.name).toBe('abc');
337
+ });
338
+
339
+ it('should validate string maxLength', async () => {
340
+ const schema = defineFlags({
341
+ name: {
342
+ type: 'string',
343
+ maxLength: 5,
344
+ },
345
+ });
346
+
347
+ await expect(validateFlags({ name: 'abcdef' }, schema)).rejects.toThrow();
348
+
349
+ const flags = await validateFlags({ name: 'abc' }, schema);
350
+ expect(flags.name).toBe('abc');
351
+ });
352
+ });
353
+