@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.
- package/.claude/commands/context-load.md +11 -0
- package/.claude/commands/context-save.md +16 -0
- package/.claude/commands/prd-done.md +115 -0
- package/.claude/commands/prd-get.md +25 -0
- package/.claude/commands/prd-start.md +87 -0
- package/.claude/commands/task-done.md +77 -0
- package/.claude/commands/tests-reminder.md +32 -0
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/ci.yml +170 -0
- package/.prettierrc.json +10 -0
- package/.teller.yml +8 -0
- package/CLAUDE.md +162 -0
- package/assets/images/logo.png +0 -0
- package/bin/dot-ai.ts +47 -0
- package/destroy.sh +45 -0
- package/devbox.json +13 -0
- package/devbox.lock +225 -0
- package/docs/API.md +449 -0
- package/docs/CONTEXT.md +49 -0
- package/docs/DEVELOPMENT.md +203 -0
- package/docs/NEXT_STEPS.md +97 -0
- package/docs/STAGE_BASED_API.md +97 -0
- package/docs/cli-guide.md +798 -0
- package/docs/design.md +750 -0
- package/docs/discovery-engine.md +515 -0
- package/docs/error-handling.md +429 -0
- package/docs/function-registration.md +157 -0
- package/docs/mcp-guide.md +416 -0
- package/package.json +2 -123
- package/renovate.json +51 -0
- package/setup.sh +111 -0
- package/{dist/cli.js → src/cli.ts} +26 -19
- package/src/core/claude.ts +280 -0
- package/src/core/deploy-operation.ts +127 -0
- package/src/core/discovery.ts +900 -0
- package/src/core/error-handling.ts +562 -0
- package/src/core/index.ts +143 -0
- package/src/core/kubernetes-utils.ts +218 -0
- package/src/core/memory.ts +148 -0
- package/src/core/schema.ts +830 -0
- package/src/core/session-utils.ts +97 -0
- package/src/core/workflow.ts +234 -0
- package/src/index.ts +18 -0
- package/src/interfaces/cli.ts +872 -0
- package/src/interfaces/mcp.ts +183 -0
- package/src/mcp/server.ts +131 -0
- package/src/tools/answer-question.ts +807 -0
- package/src/tools/choose-solution.ts +169 -0
- package/src/tools/deploy-manifests.ts +94 -0
- package/src/tools/generate-manifests.ts +502 -0
- package/src/tools/index.ts +41 -0
- package/src/tools/recommend.ts +370 -0
- package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
- package/tests/build-system.test.ts +345 -0
- package/tests/configuration.test.ts +226 -0
- package/tests/core/deploy-operation.test.ts +38 -0
- package/tests/core/discovery.test.ts +1648 -0
- package/tests/core/error-handling.test.ts +632 -0
- package/tests/core/schema.test.ts +1658 -0
- package/tests/core/session-utils.test.ts +245 -0
- package/tests/core.test.ts +439 -0
- package/tests/fixtures/configmap-no-labels.yaml +8 -0
- package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
- package/tests/fixtures/crossplane-providers.yaml +45 -0
- package/tests/fixtures/crossplane-rbac.yaml +48 -0
- package/tests/fixtures/invalid-configmap.yaml +8 -0
- package/tests/fixtures/invalid-deployment.yaml +17 -0
- package/tests/fixtures/test-deployment.yaml +28 -0
- package/tests/fixtures/valid-configmap.yaml +15 -0
- package/tests/infrastructure.test.ts +426 -0
- package/tests/interfaces/cli.test.ts +1036 -0
- package/tests/interfaces/mcp.test.ts +139 -0
- package/tests/kubernetes-utils.test.ts +200 -0
- package/tests/mcp/server.test.ts +126 -0
- package/tests/setup.ts +31 -0
- package/tests/tools/answer-question.test.ts +367 -0
- package/tests/tools/choose-solution.test.ts +481 -0
- package/tests/tools/deploy-manifests.test.ts +185 -0
- package/tests/tools/generate-manifests.test.ts +441 -0
- package/tests/tools/index.test.ts +111 -0
- package/tests/tools/recommend.test.ts +180 -0
- package/tsconfig.json +34 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/core/claude.d.ts +0 -42
- package/dist/core/claude.d.ts.map +0 -1
- package/dist/core/claude.js +0 -229
- package/dist/core/deploy-operation.d.ts +0 -38
- package/dist/core/deploy-operation.d.ts.map +0 -1
- package/dist/core/deploy-operation.js +0 -101
- package/dist/core/discovery.d.ts +0 -162
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -758
- package/dist/core/error-handling.d.ts +0 -167
- package/dist/core/error-handling.d.ts.map +0 -1
- package/dist/core/error-handling.js +0 -399
- package/dist/core/index.d.ts +0 -42
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -123
- package/dist/core/kubernetes-utils.d.ts +0 -38
- package/dist/core/kubernetes-utils.d.ts.map +0 -1
- package/dist/core/kubernetes-utils.js +0 -177
- package/dist/core/memory.d.ts +0 -45
- package/dist/core/memory.d.ts.map +0 -1
- package/dist/core/memory.js +0 -113
- package/dist/core/schema.d.ts +0 -187
- package/dist/core/schema.d.ts.map +0 -1
- package/dist/core/schema.js +0 -655
- package/dist/core/session-utils.d.ts +0 -29
- package/dist/core/session-utils.d.ts.map +0 -1
- package/dist/core/session-utils.js +0 -121
- package/dist/core/workflow.d.ts +0 -70
- package/dist/core/workflow.d.ts.map +0 -1
- package/dist/core/workflow.js +0 -161
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -32
- package/dist/interfaces/cli.d.ts +0 -74
- package/dist/interfaces/cli.d.ts.map +0 -1
- package/dist/interfaces/cli.js +0 -769
- package/dist/interfaces/mcp.d.ts +0 -30
- package/dist/interfaces/mcp.d.ts.map +0 -1
- package/dist/interfaces/mcp.js +0 -105
- package/dist/mcp/server.d.ts +0 -9
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -151
- package/dist/tools/answer-question.d.ts +0 -27
- package/dist/tools/answer-question.d.ts.map +0 -1
- package/dist/tools/answer-question.js +0 -696
- package/dist/tools/choose-solution.d.ts +0 -23
- package/dist/tools/choose-solution.d.ts.map +0 -1
- package/dist/tools/choose-solution.js +0 -171
- package/dist/tools/deploy-manifests.d.ts +0 -25
- package/dist/tools/deploy-manifests.d.ts.map +0 -1
- package/dist/tools/deploy-manifests.js +0 -74
- package/dist/tools/generate-manifests.d.ts +0 -23
- package/dist/tools/generate-manifests.d.ts.map +0 -1
- package/dist/tools/generate-manifests.js +0 -424
- package/dist/tools/index.d.ts +0 -11
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -34
- package/dist/tools/recommend.d.ts +0 -23
- package/dist/tools/recommend.d.ts.map +0 -1
- 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
|
+
});
|