@vfarcic/dot-ai 0.4.9 → 0.5.1

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 (145) hide show
  1. package/.claude/commands/context-load.md +11 -0
  2. package/.claude/commands/context-save.md +16 -0
  3. package/.claude/commands/prd-done.md +115 -0
  4. package/.claude/commands/prd-get.md +25 -0
  5. package/.claude/commands/prd-start.md +87 -0
  6. package/.claude/commands/task-done.md +77 -0
  7. package/.claude/commands/tests-reminder.md +32 -0
  8. package/.claude/settings.local.json +20 -0
  9. package/.eslintrc.json +25 -0
  10. package/.github/workflows/ci.yml +170 -0
  11. package/.prettierrc.json +10 -0
  12. package/.teller.yml +8 -0
  13. package/CLAUDE.md +162 -0
  14. package/assets/images/logo.png +0 -0
  15. package/bin/dot-ai.ts +47 -0
  16. package/destroy.sh +45 -0
  17. package/devbox.json +13 -0
  18. package/devbox.lock +225 -0
  19. package/docs/API.md +449 -0
  20. package/docs/CONTEXT.md +49 -0
  21. package/docs/DEVELOPMENT.md +203 -0
  22. package/docs/NEXT_STEPS.md +97 -0
  23. package/docs/STAGE_BASED_API.md +97 -0
  24. package/docs/cli-guide.md +798 -0
  25. package/docs/design.md +750 -0
  26. package/docs/discovery-engine.md +515 -0
  27. package/docs/error-handling.md +429 -0
  28. package/docs/function-registration.md +157 -0
  29. package/docs/mcp-guide.md +416 -0
  30. package/package.json +2 -123
  31. package/renovate.json +51 -0
  32. package/setup.sh +111 -0
  33. package/{dist/cli.js → src/cli.ts} +26 -19
  34. package/src/core/claude.ts +280 -0
  35. package/src/core/deploy-operation.ts +127 -0
  36. package/src/core/discovery.ts +900 -0
  37. package/src/core/error-handling.ts +562 -0
  38. package/src/core/index.ts +143 -0
  39. package/src/core/kubernetes-utils.ts +218 -0
  40. package/src/core/memory.ts +148 -0
  41. package/src/core/schema.ts +830 -0
  42. package/src/core/session-utils.ts +97 -0
  43. package/src/core/workflow.ts +234 -0
  44. package/src/index.ts +18 -0
  45. package/src/interfaces/cli.ts +872 -0
  46. package/src/interfaces/mcp.ts +183 -0
  47. package/src/mcp/server.ts +131 -0
  48. package/src/tools/answer-question.ts +807 -0
  49. package/src/tools/choose-solution.ts +169 -0
  50. package/src/tools/deploy-manifests.ts +94 -0
  51. package/src/tools/generate-manifests.ts +502 -0
  52. package/src/tools/index.ts +41 -0
  53. package/src/tools/recommend.ts +370 -0
  54. package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
  55. package/tests/build-system.test.ts +345 -0
  56. package/tests/configuration.test.ts +226 -0
  57. package/tests/core/deploy-operation.test.ts +38 -0
  58. package/tests/core/discovery.test.ts +1648 -0
  59. package/tests/core/error-handling.test.ts +632 -0
  60. package/tests/core/schema.test.ts +1658 -0
  61. package/tests/core/session-utils.test.ts +245 -0
  62. package/tests/core.test.ts +439 -0
  63. package/tests/fixtures/configmap-no-labels.yaml +8 -0
  64. package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
  65. package/tests/fixtures/crossplane-providers.yaml +45 -0
  66. package/tests/fixtures/crossplane-rbac.yaml +48 -0
  67. package/tests/fixtures/invalid-configmap.yaml +8 -0
  68. package/tests/fixtures/invalid-deployment.yaml +17 -0
  69. package/tests/fixtures/test-deployment.yaml +28 -0
  70. package/tests/fixtures/valid-configmap.yaml +15 -0
  71. package/tests/infrastructure.test.ts +426 -0
  72. package/tests/interfaces/cli.test.ts +1036 -0
  73. package/tests/interfaces/mcp.test.ts +139 -0
  74. package/tests/kubernetes-utils.test.ts +200 -0
  75. package/tests/mcp/server.test.ts +126 -0
  76. package/tests/setup.ts +31 -0
  77. package/tests/tools/answer-question.test.ts +367 -0
  78. package/tests/tools/choose-solution.test.ts +481 -0
  79. package/tests/tools/deploy-manifests.test.ts +185 -0
  80. package/tests/tools/generate-manifests.test.ts +441 -0
  81. package/tests/tools/index.test.ts +111 -0
  82. package/tests/tools/recommend.test.ts +180 -0
  83. package/tsconfig.json +34 -0
  84. package/dist/cli.d.ts +0 -3
  85. package/dist/cli.d.ts.map +0 -1
  86. package/dist/core/claude.d.ts +0 -42
  87. package/dist/core/claude.d.ts.map +0 -1
  88. package/dist/core/claude.js +0 -229
  89. package/dist/core/deploy-operation.d.ts +0 -38
  90. package/dist/core/deploy-operation.d.ts.map +0 -1
  91. package/dist/core/deploy-operation.js +0 -101
  92. package/dist/core/discovery.d.ts +0 -162
  93. package/dist/core/discovery.d.ts.map +0 -1
  94. package/dist/core/discovery.js +0 -758
  95. package/dist/core/error-handling.d.ts +0 -167
  96. package/dist/core/error-handling.d.ts.map +0 -1
  97. package/dist/core/error-handling.js +0 -399
  98. package/dist/core/index.d.ts +0 -42
  99. package/dist/core/index.d.ts.map +0 -1
  100. package/dist/core/index.js +0 -123
  101. package/dist/core/kubernetes-utils.d.ts +0 -38
  102. package/dist/core/kubernetes-utils.d.ts.map +0 -1
  103. package/dist/core/kubernetes-utils.js +0 -177
  104. package/dist/core/memory.d.ts +0 -45
  105. package/dist/core/memory.d.ts.map +0 -1
  106. package/dist/core/memory.js +0 -113
  107. package/dist/core/schema.d.ts +0 -187
  108. package/dist/core/schema.d.ts.map +0 -1
  109. package/dist/core/schema.js +0 -655
  110. package/dist/core/session-utils.d.ts +0 -29
  111. package/dist/core/session-utils.d.ts.map +0 -1
  112. package/dist/core/session-utils.js +0 -121
  113. package/dist/core/workflow.d.ts +0 -70
  114. package/dist/core/workflow.d.ts.map +0 -1
  115. package/dist/core/workflow.js +0 -161
  116. package/dist/index.d.ts +0 -15
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -32
  119. package/dist/interfaces/cli.d.ts +0 -74
  120. package/dist/interfaces/cli.d.ts.map +0 -1
  121. package/dist/interfaces/cli.js +0 -769
  122. package/dist/interfaces/mcp.d.ts +0 -30
  123. package/dist/interfaces/mcp.d.ts.map +0 -1
  124. package/dist/interfaces/mcp.js +0 -105
  125. package/dist/mcp/server.d.ts +0 -9
  126. package/dist/mcp/server.d.ts.map +0 -1
  127. package/dist/mcp/server.js +0 -151
  128. package/dist/tools/answer-question.d.ts +0 -27
  129. package/dist/tools/answer-question.d.ts.map +0 -1
  130. package/dist/tools/answer-question.js +0 -696
  131. package/dist/tools/choose-solution.d.ts +0 -23
  132. package/dist/tools/choose-solution.d.ts.map +0 -1
  133. package/dist/tools/choose-solution.js +0 -171
  134. package/dist/tools/deploy-manifests.d.ts +0 -25
  135. package/dist/tools/deploy-manifests.d.ts.map +0 -1
  136. package/dist/tools/deploy-manifests.js +0 -74
  137. package/dist/tools/generate-manifests.d.ts +0 -23
  138. package/dist/tools/generate-manifests.d.ts.map +0 -1
  139. package/dist/tools/generate-manifests.js +0 -424
  140. package/dist/tools/index.d.ts +0 -11
  141. package/dist/tools/index.d.ts.map +0 -1
  142. package/dist/tools/index.js +0 -34
  143. package/dist/tools/recommend.d.ts +0 -23
  144. package/dist/tools/recommend.d.ts.map +0 -1
  145. package/dist/tools/recommend.js +0 -332
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Tests for Choose Solution Tool
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import {
9
+ CHOOSESOLUTION_TOOL_NAME,
10
+ CHOOSESOLUTION_TOOL_DESCRIPTION,
11
+ CHOOSESOLUTION_TOOL_INPUT_SCHEMA,
12
+ handleChooseSolutionTool
13
+ } from '../../src/tools/choose-solution';
14
+
15
+ describe('Choose Solution Tool', () => {
16
+ let tempDir: string;
17
+ let sessionDir: string;
18
+ let mockContext: any;
19
+
20
+ beforeEach(() => {
21
+ // Create temporary directory for test files
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'choose-solution-test-'));
23
+ sessionDir = path.join(tempDir, 'solutions');
24
+ fs.mkdirSync(sessionDir, { recursive: true });
25
+
26
+ // Mock tool context
27
+ mockContext = {
28
+ requestId: 'test-request-123',
29
+ logger: {
30
+ debug: jest.fn(),
31
+ info: jest.fn(),
32
+ warn: jest.fn(),
33
+ error: jest.fn(),
34
+ fatal: jest.fn()
35
+ },
36
+ dotAI: {} as any // Mock DotAI object
37
+ };
38
+ });
39
+
40
+ afterEach(() => {
41
+ // Clean up temporary directory
42
+ if (fs.existsSync(tempDir)) {
43
+ fs.rmSync(tempDir, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ describe('Tool Metadata', () => {
48
+ test('should have correct tool metadata structure', () => {
49
+ expect(CHOOSESOLUTION_TOOL_NAME).toBe('chooseSolution');
50
+ expect(CHOOSESOLUTION_TOOL_DESCRIPTION).toContain('Select a solution');
51
+ expect(CHOOSESOLUTION_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
52
+ });
53
+ });
54
+
55
+ describe('Input Validation', () => {
56
+ describe('CLI Mode (with sessionDir parameter)', () => {
57
+ test('should reject missing solutionId', async () => {
58
+ // Test the error we get when no environment is set and no solutionId provided
59
+ delete process.env.DOT_AI_SESSION_DIR;
60
+
61
+ const args = { solutionId: 'sol_2025-07-01T154349_1e1e242592ff' };
62
+
63
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
64
+ message: 'Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable'
65
+ });
66
+ });
67
+
68
+ test('should reject missing sessionDir when environment variable not set', async () => {
69
+ // Ensure environment variable is not set
70
+ const originalEnv = process.env.DOT_AI_SESSION_DIR;
71
+ delete process.env.DOT_AI_SESSION_DIR;
72
+
73
+ const args = {
74
+ solutionId: 'sol_2025-07-01T154349_1e1e242592ff'
75
+ };
76
+
77
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
78
+ message: 'Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable'
79
+ });
80
+
81
+ // Restore environment variable
82
+ if (originalEnv !== undefined) {
83
+ process.env.DOT_AI_SESSION_DIR = originalEnv;
84
+ }
85
+ });
86
+
87
+ test('should reject invalid solutionId format', async () => {
88
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
89
+
90
+ const args = {
91
+ solutionId: 'invalid-format'
92
+ };
93
+
94
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
95
+ message: expect.stringContaining('Solution file not found')
96
+ });
97
+ });
98
+ });
99
+
100
+ describe('MCP Mode (with environment variable)', () => {
101
+ let originalEnv: string | undefined;
102
+
103
+ beforeEach(() => {
104
+ originalEnv = process.env.DOT_AI_SESSION_DIR;
105
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
106
+ });
107
+
108
+ afterEach(() => {
109
+ if (originalEnv !== undefined) {
110
+ process.env.DOT_AI_SESSION_DIR = originalEnv;
111
+ } else {
112
+ delete process.env.DOT_AI_SESSION_DIR;
113
+ }
114
+ });
115
+
116
+ test('should work with only solutionId when environment variable is set', async () => {
117
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592ff';
118
+ const solutionData = {
119
+ solutionId: solutionId,
120
+ questions: {
121
+ required: [{
122
+ id: 'name',
123
+ question: 'What name would you like to give to your application?',
124
+ type: 'text'
125
+ }]
126
+ }
127
+ };
128
+
129
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
130
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData));
131
+
132
+ const args = {
133
+ solutionId: solutionId
134
+ // No sessionDir - should come from environment
135
+ };
136
+
137
+ const result = await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
138
+ expect(result.content[0].type).toBe('text');
139
+ const response = JSON.parse(result.content[0].text);
140
+ expect(response.status).toBe('stage_questions');
141
+ expect(response.currentStage).toBe('required');
142
+ expect(response.nextStage).toBe('basic');
143
+ expect(response.nextAction).toBe('answerQuestion');
144
+ expect(response.solutionId).toBe(solutionId);
145
+ });
146
+
147
+ test('should reject missing solutionId in MCP mode', async () => {
148
+ // This test is no longer valid since the handler expects solutionId parameter
149
+ // The MCP SDK would catch this type error before it reaches our handler
150
+ // So we'll test environment variable missing instead
151
+ delete process.env.DOT_AI_SESSION_DIR;
152
+
153
+ const args = { solutionId: 'sol_2025-07-01T154349_1e1e242592ff' };
154
+
155
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
156
+ message: 'Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable'
157
+ });
158
+ });
159
+
160
+ test('should fail when environment variable is not set', async () => {
161
+ delete process.env.DOT_AI_SESSION_DIR;
162
+
163
+ const args = {
164
+ solutionId: 'sol_2025-07-01T154349_1e1e242592ff'
165
+ };
166
+
167
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
168
+ message: 'Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable'
169
+ });
170
+ });
171
+ });
172
+
173
+ test('should accept valid solutionId format', async () => {
174
+ const validSolutionId = 'sol_2025-07-01T154349_1e1e242592ff';
175
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
176
+
177
+ // Create a valid solution file
178
+ const solutionData = {
179
+ solutionId: validSolutionId,
180
+ questions: {
181
+ required: [],
182
+ basic: [],
183
+ advanced: [],
184
+ open: {}
185
+ }
186
+ };
187
+
188
+ const solutionPath = path.join(sessionDir, `${validSolutionId}.json`);
189
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData, null, 2));
190
+
191
+ const args = {
192
+ solutionId: validSolutionId
193
+ };
194
+
195
+ const result = await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
196
+ const response = JSON.parse(result.content[0].text);
197
+
198
+ expect(response.error).toBeFalsy();
199
+ expect(response.status).toBe('stage_questions');
200
+ expect(response.currentStage).toBe('required');
201
+ expect(response.nextStage).toBe('basic');
202
+ expect(response.nextAction).toBe('answerQuestion');
203
+ });
204
+ });
205
+
206
+ describe('Session Directory Validation', () => {
207
+ test('should reject non-existent session directory', async () => {
208
+ process.env.DOT_AI_SESSION_DIR = '/non/existent/path';
209
+
210
+ const args = {
211
+ solutionId: 'sol_2025-07-01T154349_1e1e242592ff'
212
+ };
213
+
214
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
215
+ message: expect.stringContaining('Session directory does not exist')
216
+ });
217
+ });
218
+
219
+ test('should reject session directory that is not a directory', async () => {
220
+ // Create a file instead of directory
221
+ const filePath = path.join(tempDir, 'not-a-directory');
222
+ fs.writeFileSync(filePath, 'test');
223
+ process.env.DOT_AI_SESSION_DIR = filePath;
224
+
225
+ const args = {
226
+ solutionId: 'sol_2025-07-01T154349_1e1e242592ff'
227
+ };
228
+
229
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
230
+ message: expect.stringContaining('not a directory')
231
+ });
232
+ });
233
+
234
+ test('should accept valid readable session directory', async () => {
235
+ const validSolutionId = 'sol_2025-07-01T154349_1e1e242592ff';
236
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
237
+
238
+ // Create a valid solution file
239
+ const solutionData = {
240
+ solutionId: validSolutionId,
241
+ questions: {
242
+ required: [],
243
+ basic: [],
244
+ advanced: [],
245
+ open: {}
246
+ }
247
+ };
248
+
249
+ const solutionPath = path.join(sessionDir, `${validSolutionId}.json`);
250
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData, null, 2));
251
+
252
+ const args = {
253
+ solutionId: validSolutionId
254
+ };
255
+
256
+ const result = await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
257
+ const response = JSON.parse(result.content[0].text);
258
+
259
+ expect(response.error).toBeFalsy();
260
+ expect(response.status).toBe('stage_questions');
261
+ expect(response.currentStage).toBe('required');
262
+ expect(response.nextStage).toBe('basic');
263
+ expect(response.nextAction).toBe('answerQuestion');
264
+ });
265
+ });
266
+
267
+ describe('Solution File Loading', () => {
268
+ test('should reject non-existent solution file', async () => {
269
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
270
+
271
+ const args = {
272
+ solutionId: 'sol_2025-07-01T154349_1e1e242592fa' // Valid format but non-existent file
273
+ };
274
+
275
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
276
+ message: expect.stringContaining('Solution file not found')
277
+ });
278
+ });
279
+
280
+ test('should reject invalid JSON in solution file', async () => {
281
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592ff';
282
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
283
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
284
+
285
+ // Write invalid JSON
286
+ fs.writeFileSync(solutionPath, '{ invalid json }');
287
+
288
+ const args = {
289
+ solutionId: solutionId
290
+ };
291
+
292
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
293
+ message: expect.stringContaining('Invalid JSON')
294
+ });
295
+ });
296
+
297
+ test('should reject solution file with missing required fields', async () => {
298
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592ff';
299
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
300
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
301
+
302
+ // Write JSON without required fields
303
+ const invalidSolution = {
304
+ someField: 'value'
305
+ };
306
+ fs.writeFileSync(solutionPath, JSON.stringify(invalidSolution));
307
+
308
+ const args = {
309
+ solutionId: solutionId
310
+ };
311
+
312
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
313
+ message: expect.stringContaining('Invalid solution file structure')
314
+ });
315
+ });
316
+ });
317
+
318
+ describe('Successful Execution', () => {
319
+ test('should return complete question structure for valid solution', async () => {
320
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592ff';
321
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
322
+
323
+ const solutionData = {
324
+ solutionId: solutionId,
325
+ intent: 'deploy a stateless application',
326
+ type: 'single',
327
+ score: 85,
328
+ description: 'Test solution',
329
+ questions: {
330
+ required: [
331
+ {
332
+ id: 'name',
333
+ question: 'What name would you like to give to your application?',
334
+ type: 'text',
335
+ validation: { required: true }
336
+ }
337
+ ],
338
+ basic: [
339
+ {
340
+ id: 'port',
341
+ question: 'What port does your application listen on?',
342
+ type: 'number',
343
+ default: 8080
344
+ }
345
+ ],
346
+ advanced: [
347
+ {
348
+ id: 'scaling-enabled',
349
+ question: 'Would you like to enable auto-scaling?',
350
+ type: 'boolean',
351
+ default: false
352
+ }
353
+ ],
354
+ open: {
355
+ question: 'Is there anything else about your requirements?',
356
+ placeholder: 'e.g., specific security requirements...'
357
+ }
358
+ }
359
+ };
360
+
361
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
362
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData, null, 2));
363
+
364
+ const args = {
365
+ solutionId: solutionId
366
+ };
367
+
368
+ const result = await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
369
+ const response = JSON.parse(result.content[0].text);
370
+
371
+ expect(response.status).toBe('stage_questions');
372
+ expect(response.currentStage).toBe('required');
373
+ expect(response.nextStage).toBe('basic');
374
+ expect(response.nextAction).toBe('answerQuestion');
375
+ expect(response.solutionId).toBe(solutionId);
376
+ expect(response.questions).toEqual(solutionData.questions.required);
377
+ expect(response.nextAction).toContain('answerQuestion');
378
+ expect(response.guidance).toContain('Answer questions in this stage');
379
+ expect(response.timestamp).toBeDefined();
380
+ });
381
+
382
+ test('should handle solution with minimal question structure', async () => {
383
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592fb'; // Valid hex format
384
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
385
+
386
+ const solutionData = {
387
+ solutionId: solutionId,
388
+ questions: {
389
+ required: [],
390
+ basic: [],
391
+ advanced: [],
392
+ open: {}
393
+ }
394
+ };
395
+
396
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
397
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData, null, 2));
398
+
399
+ const args = {
400
+ solutionId: solutionId
401
+ };
402
+
403
+ const result = await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
404
+ const response = JSON.parse(result.content[0].text);
405
+
406
+ expect(response.status).toBe('stage_questions');
407
+ expect(response.currentStage).toBe('required');
408
+ expect(response.nextStage).toBe('basic');
409
+ expect(response.nextAction).toBe('answerQuestion');
410
+ expect(response.solutionId).toBe(solutionId);
411
+ expect(response.questions).toEqual([]);
412
+ });
413
+
414
+ test('should log appropriate debug and info messages', async () => {
415
+ const solutionId = 'sol_2025-07-01T154349_1e1e242592fc'; // Valid hex format
416
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
417
+
418
+ const solutionData = {
419
+ solutionId: solutionId,
420
+ questions: {
421
+ required: [{ id: 'test' }],
422
+ basic: [{ id: 'test2' }],
423
+ advanced: [],
424
+ open: { question: 'test' }
425
+ }
426
+ };
427
+
428
+ const solutionPath = path.join(sessionDir, `${solutionId}.json`);
429
+ fs.writeFileSync(solutionPath, JSON.stringify(solutionData, null, 2));
430
+
431
+ const args = {
432
+ solutionId: solutionId
433
+ };
434
+
435
+ await handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
436
+
437
+ expect(mockContext.logger.debug).toHaveBeenCalledWith(
438
+ 'Session directory resolved and validated',
439
+ { sessionDir: sessionDir }
440
+ );
441
+ expect(mockContext.logger.debug).toHaveBeenCalledWith(
442
+ 'Solution file loaded successfully',
443
+ expect.objectContaining({
444
+ solutionId: solutionId,
445
+ hasQuestions: true,
446
+ questionCategories: {
447
+ required: 1,
448
+ basic: 1,
449
+ advanced: 0,
450
+ hasOpen: true
451
+ }
452
+ })
453
+ );
454
+ expect(mockContext.logger.info).toHaveBeenCalledWith(
455
+ 'Choose solution completed successfully',
456
+ expect.objectContaining({
457
+ solutionId: solutionId,
458
+ totalQuestions: 3
459
+ })
460
+ );
461
+ });
462
+ });
463
+
464
+ describe('Error Context and Suggestions', () => {
465
+ test('should provide helpful error context for missing files', async () => {
466
+ process.env.DOT_AI_SESSION_DIR = sessionDir;
467
+
468
+ const args = {
469
+ solutionId: 'sol_2025-07-01T154349_1e1e242592fd' // Valid format but missing file
470
+ };
471
+
472
+ await expect(handleChooseSolutionTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId)).rejects.toMatchObject({
473
+ message: expect.stringContaining('Solution file not found'),
474
+ context: expect.objectContaining({
475
+ operation: 'solution_file_load',
476
+ component: 'ChooseSolutionTool'
477
+ })
478
+ });
479
+ });
480
+ });
481
+ });
@@ -0,0 +1,185 @@
1
+ import {
2
+ DEPLOYMANIFESTS_TOOL_NAME,
3
+ DEPLOYMANIFESTS_TOOL_DESCRIPTION,
4
+ DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA,
5
+ handleDeployManifestsTool
6
+ } from '../../src/tools/deploy-manifests';
7
+ import { DeployOperation } from '../../src/core/deploy-operation';
8
+ import { ErrorHandler, ErrorCategory, ErrorSeverity } from '../../src/core/error-handling';
9
+
10
+ // Mock dependencies
11
+ jest.mock('../../src/core/deploy-operation');
12
+ jest.mock('../../src/core/error-handling', () => {
13
+ const original = jest.requireActual('../../src/core/error-handling');
14
+ return {
15
+ ...original,
16
+ ErrorHandler: {
17
+ ...original.ErrorHandler,
18
+ withErrorHandling: jest.fn((fn) => fn()),
19
+ createError: jest.fn()
20
+ }
21
+ };
22
+ });
23
+
24
+ const mockDeployOperation = DeployOperation as jest.MockedClass<typeof DeployOperation>;
25
+
26
+ describe('Deploy Manifests Tool', () => {
27
+ let mockContext: any;
28
+
29
+ beforeEach(() => {
30
+ mockContext = {
31
+ requestId: 'test-request',
32
+ logger: {
33
+ debug: jest.fn(),
34
+ info: jest.fn(),
35
+ warn: jest.fn(),
36
+ error: jest.fn(),
37
+ fatal: jest.fn()
38
+ },
39
+ dotAI: null
40
+ };
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ // Get the mocked function reference
45
+ const getMockWithErrorHandling = () => ErrorHandler.withErrorHandling as jest.MockedFunction<typeof ErrorHandler.withErrorHandling>;
46
+ const getMockCreateError = () => ErrorHandler.createError as jest.MockedFunction<typeof ErrorHandler.createError>;
47
+
48
+ describe('Tool Metadata', () => {
49
+ it('should have correct tool metadata properties', () => {
50
+ expect(DEPLOYMANIFESTS_TOOL_NAME).toBe('deployManifests');
51
+ expect(DEPLOYMANIFESTS_TOOL_DESCRIPTION).toContain('Deploy Kubernetes manifests');
52
+ expect(DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
53
+ expect(DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA.timeout).toBeDefined();
54
+ });
55
+ });
56
+
57
+ describe('Tool Handler', () => {
58
+ const validArgs = {
59
+ solutionId: 'sol_2025-01-01T120000_abc123',
60
+ sessionDir: '/test/sessions',
61
+ timeout: 60
62
+ };
63
+
64
+ it('should validate input arguments and handle CLI mode', async () => {
65
+ const mockDeploy = jest.fn().mockResolvedValue({
66
+ success: true,
67
+ kubectlOutput: 'deployment created',
68
+ manifestPath: '/test/path/manifest.yaml',
69
+ solutionId: validArgs.solutionId,
70
+ readinessTimeout: false,
71
+ message: 'Success'
72
+ });
73
+
74
+ mockDeployOperation.prototype.deploy = mockDeploy;
75
+
76
+ await handleDeployManifestsTool(validArgs, mockContext.dotAI, mockContext.logger, mockContext.requestId);
77
+
78
+ expect(getMockWithErrorHandling()).toHaveBeenCalled();
79
+ expect(mockDeploy).toHaveBeenCalledWith({
80
+ solutionId: validArgs.solutionId,
81
+ timeout: validArgs.timeout
82
+ });
83
+ });
84
+
85
+ it('should successfully deploy manifests and return formatted response', async () => {
86
+ const deployResult = {
87
+ success: true,
88
+ kubectlOutput: 'deployment.apps/test-app created\nservice/test-service created',
89
+ manifestPath: '/test/sessions/sol_test/manifest.yaml',
90
+ solutionId: validArgs.solutionId,
91
+ readinessTimeout: false,
92
+ message: 'Deployment completed successfully'
93
+ };
94
+
95
+ const mockDeploy = jest.fn().mockResolvedValue(deployResult);
96
+ mockDeployOperation.prototype.deploy = mockDeploy;
97
+
98
+ const result = await handleDeployManifestsTool(validArgs, mockContext.dotAI, mockContext.logger, mockContext.requestId);
99
+
100
+ expect(mockDeploy).toHaveBeenCalledWith({
101
+ solutionId: validArgs.solutionId,
102
+ timeout: validArgs.timeout
103
+ });
104
+
105
+ expect(result.content).toHaveLength(1);
106
+ expect(result.content[0].type).toBe('text');
107
+
108
+ const responseData = JSON.parse(result.content[0].text);
109
+ expect(responseData.success).toBe(true);
110
+ expect(responseData.deploymentComplete).toBe(true);
111
+ expect(responseData.requiresStatusCheck).toBe(false);
112
+ expect(responseData.kubectlOutput).toBe(deployResult.kubectlOutput);
113
+ });
114
+
115
+ it('should handle deployment timeout correctly', async () => {
116
+ const deployResult = {
117
+ success: true,
118
+ kubectlOutput: 'deployment applied but timed out',
119
+ manifestPath: '/test/sessions/sol_test/manifest.yaml',
120
+ solutionId: validArgs.solutionId,
121
+ readinessTimeout: true,
122
+ message: 'Deployment applied but resources did not become ready within timeout'
123
+ };
124
+
125
+ const mockDeploy = jest.fn().mockResolvedValue(deployResult);
126
+ mockDeployOperation.prototype.deploy = mockDeploy;
127
+
128
+ const result = await handleDeployManifestsTool(validArgs, mockContext.dotAI, mockContext.logger, mockContext.requestId);
129
+
130
+ const responseData = JSON.parse(result.content[0].text);
131
+ expect(responseData.success).toBe(true);
132
+ expect(responseData.readinessTimeout).toBe(true);
133
+ expect(responseData.deploymentComplete).toBe(false);
134
+ expect(responseData.requiresStatusCheck).toBe(true);
135
+ });
136
+
137
+ it('should handle deployment failures', async () => {
138
+ const deployResult = {
139
+ success: false,
140
+ kubectlOutput: 'Error: manifest validation failed',
141
+ manifestPath: '/test/sessions/sol_test/manifest.yaml',
142
+ solutionId: validArgs.solutionId,
143
+ readinessTimeout: false,
144
+ message: 'Deployment failed'
145
+ };
146
+
147
+ const mockDeploy = jest.fn().mockResolvedValue(deployResult);
148
+ mockDeployOperation.prototype.deploy = mockDeploy;
149
+
150
+ const result = await handleDeployManifestsTool(validArgs, mockContext.dotAI, mockContext.logger, mockContext.requestId);
151
+
152
+ const responseData = JSON.parse(result.content[0].text);
153
+ expect(responseData.success).toBe(false);
154
+ expect(responseData.deploymentComplete).toBe(false);
155
+ expect(responseData.requiresStatusCheck).toBe(false);
156
+ expect(responseData.message).toBe('Deployment failed');
157
+ });
158
+
159
+ it('should use default timeout when not provided', async () => {
160
+ const argsWithoutTimeout = {
161
+ solutionId: validArgs.solutionId,
162
+ sessionDir: validArgs.sessionDir
163
+ };
164
+
165
+ const mockDeploy = jest.fn().mockResolvedValue({
166
+ success: true,
167
+ kubectlOutput: 'success',
168
+ manifestPath: '/test/path',
169
+ solutionId: validArgs.solutionId,
170
+ readinessTimeout: false,
171
+ message: 'Success'
172
+ });
173
+
174
+ mockDeployOperation.prototype.deploy = mockDeploy;
175
+
176
+ await handleDeployManifestsTool(argsWithoutTimeout, mockContext.dotAI, mockContext.logger, mockContext.requestId);
177
+
178
+ expect(mockDeploy).toHaveBeenCalledWith({
179
+ solutionId: validArgs.solutionId,
180
+ timeout: 30 // Default timeout
181
+ });
182
+ });
183
+
184
+ });
185
+ });