@vfarcic/dot-ai 0.5.0 → 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 -121
- 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,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer Question Tool Tests - Stage-Based Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import {
|
|
9
|
+
ANSWERQUESTION_TOOL_NAME,
|
|
10
|
+
ANSWERQUESTION_TOOL_DESCRIPTION,
|
|
11
|
+
ANSWERQUESTION_TOOL_INPUT_SCHEMA,
|
|
12
|
+
handleAnswerQuestionTool
|
|
13
|
+
} from '../../src/tools/answer-question';
|
|
14
|
+
|
|
15
|
+
// Mock fs module
|
|
16
|
+
jest.mock('fs');
|
|
17
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
18
|
+
|
|
19
|
+
// Test constants
|
|
20
|
+
const TEST_SESSION_DIR = '/test/session';
|
|
21
|
+
const TEST_SOLUTION_ID = 'sol_2025-01-01T123456_abcdef';
|
|
22
|
+
const TEST_SOLUTION_PATH = path.join(TEST_SESSION_DIR, `${TEST_SOLUTION_ID}.json`);
|
|
23
|
+
|
|
24
|
+
// Test data
|
|
25
|
+
const TEST_SOLUTION = {
|
|
26
|
+
solutionId: TEST_SOLUTION_ID,
|
|
27
|
+
intent: 'deploy a web application',
|
|
28
|
+
type: 'single',
|
|
29
|
+
questions: {
|
|
30
|
+
required: [
|
|
31
|
+
{
|
|
32
|
+
id: 'name',
|
|
33
|
+
question: 'What name would you like to give to your application?',
|
|
34
|
+
type: 'text',
|
|
35
|
+
validation: {
|
|
36
|
+
required: true,
|
|
37
|
+
pattern: '^[a-z0-9-]+$',
|
|
38
|
+
message: 'Name must consist of lowercase letters, numbers, and hyphens'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'port',
|
|
43
|
+
question: 'What port does your application listen on?',
|
|
44
|
+
type: 'number',
|
|
45
|
+
validation: {
|
|
46
|
+
min: 1,
|
|
47
|
+
max: 65535
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
basic: [
|
|
52
|
+
{
|
|
53
|
+
id: 'replicas',
|
|
54
|
+
question: 'How many replicas do you need?',
|
|
55
|
+
type: 'number',
|
|
56
|
+
default: 3,
|
|
57
|
+
validation: {
|
|
58
|
+
min: 1
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
advanced: [
|
|
63
|
+
{
|
|
64
|
+
id: 'scaling-enabled',
|
|
65
|
+
question: 'Enable auto-scaling?',
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
default: false
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
open: {
|
|
71
|
+
question: 'Any additional requirements?',
|
|
72
|
+
placeholder: 'e.g., specific security requirements...'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const createMockToolContext = () => ({
|
|
78
|
+
requestId: 'test-request-123',
|
|
79
|
+
logger: {
|
|
80
|
+
debug: jest.fn(),
|
|
81
|
+
info: jest.fn(),
|
|
82
|
+
warn: jest.fn(),
|
|
83
|
+
error: jest.fn(),
|
|
84
|
+
fatal: jest.fn()
|
|
85
|
+
},
|
|
86
|
+
dotAI: {} as any // Mock DotAI object
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Helper to create solution with answers
|
|
90
|
+
const createSolutionWithAnswers = (answers: Record<string, any> = {}) => {
|
|
91
|
+
const solution = JSON.parse(JSON.stringify(TEST_SOLUTION));
|
|
92
|
+
|
|
93
|
+
// Apply answers to questions
|
|
94
|
+
const allQuestions = [
|
|
95
|
+
...solution.questions.required,
|
|
96
|
+
...solution.questions.basic,
|
|
97
|
+
...solution.questions.advanced
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const [questionId, answer] of Object.entries(answers)) {
|
|
101
|
+
const question = allQuestions.find(q => q.id === questionId);
|
|
102
|
+
if (question) {
|
|
103
|
+
question.answer = answer;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return solution;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
describe('Answer Question Tool Metadata', () => {
|
|
111
|
+
test('should have correct tool metadata', () => {
|
|
112
|
+
expect(ANSWERQUESTION_TOOL_NAME).toBe('answerQuestion');
|
|
113
|
+
expect(ANSWERQUESTION_TOOL_DESCRIPTION).toContain('Process user answers');
|
|
114
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
115
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.stage).toBeDefined();
|
|
116
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.answers).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('Answer Question Tool Handler - Stage-Based Implementation', () => {
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
jest.clearAllMocks();
|
|
123
|
+
|
|
124
|
+
// Default fs mocks - set up for successful operation by default
|
|
125
|
+
mockFs.existsSync.mockImplementation((filePath) => {
|
|
126
|
+
if (typeof filePath === 'string') {
|
|
127
|
+
if (filePath === TEST_SESSION_DIR) return true;
|
|
128
|
+
if (filePath === TEST_SOLUTION_PATH) return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
mockFs.statSync.mockReturnValue({ isDirectory: () => true } as any);
|
|
134
|
+
mockFs.readdirSync.mockReturnValue([]);
|
|
135
|
+
mockFs.writeFileSync.mockImplementation(() => {});
|
|
136
|
+
mockFs.unlinkSync.mockImplementation(() => {});
|
|
137
|
+
mockFs.renameSync.mockImplementation(() => {});
|
|
138
|
+
|
|
139
|
+
// Default solution file mock
|
|
140
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(TEST_SOLUTION));
|
|
141
|
+
|
|
142
|
+
// Reset environment
|
|
143
|
+
process.env.DOT_AI_SESSION_DIR = TEST_SESSION_DIR;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
jest.resetAllMocks();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Stage-Based Workflow Tests - Core validation for new implementation
|
|
151
|
+
describe('Stage-Based Workflow Validation', () => {
|
|
152
|
+
test('should handle required stage correctly', async () => {
|
|
153
|
+
const context = createMockToolContext();
|
|
154
|
+
|
|
155
|
+
const result = await handleAnswerQuestionTool({
|
|
156
|
+
solutionId: TEST_SOLUTION_ID,
|
|
157
|
+
stage: 'required',
|
|
158
|
+
answers: {
|
|
159
|
+
name: 'my-app',
|
|
160
|
+
port: 8080 // Both required questions need to be answered
|
|
161
|
+
}
|
|
162
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
163
|
+
|
|
164
|
+
const response = JSON.parse(result.content[0].text);
|
|
165
|
+
expect(response.status).toBe('stage_questions');
|
|
166
|
+
expect(response.currentStage).toBe('basic'); // Should progress to basic stage after completing required
|
|
167
|
+
expect(response.solutionId).toBe(TEST_SOLUTION_ID);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should reject invalid stage transition', async () => {
|
|
171
|
+
const context = createMockToolContext();
|
|
172
|
+
|
|
173
|
+
// Try to jump directly to advanced stage without completing required
|
|
174
|
+
const result = await handleAnswerQuestionTool({
|
|
175
|
+
solutionId: TEST_SOLUTION_ID,
|
|
176
|
+
stage: 'advanced',
|
|
177
|
+
answers: {
|
|
178
|
+
'scaling-enabled': true
|
|
179
|
+
}
|
|
180
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
181
|
+
|
|
182
|
+
const response = JSON.parse(result.content[0].text);
|
|
183
|
+
expect(response.status).toBe('stage_error');
|
|
184
|
+
expect(response.error).toBe('invalid_transition');
|
|
185
|
+
expect(response.expected).toBe('required');
|
|
186
|
+
expect(response.received).toBe('advanced');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should handle open stage completion', async () => {
|
|
190
|
+
const context = createMockToolContext();
|
|
191
|
+
|
|
192
|
+
// Mock solution with all previous stages complete
|
|
193
|
+
const completeSolution = createSolutionWithAnswers({
|
|
194
|
+
name: 'my-app',
|
|
195
|
+
port: 8080,
|
|
196
|
+
replicas: 3,
|
|
197
|
+
'scaling-enabled': false
|
|
198
|
+
});
|
|
199
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(completeSolution));
|
|
200
|
+
|
|
201
|
+
const result = await handleAnswerQuestionTool({
|
|
202
|
+
solutionId: TEST_SOLUTION_ID,
|
|
203
|
+
stage: 'open',
|
|
204
|
+
answers: {
|
|
205
|
+
open: 'N/A'
|
|
206
|
+
}
|
|
207
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
208
|
+
|
|
209
|
+
const response = JSON.parse(result.content[0].text);
|
|
210
|
+
expect(response.status).toBe('ready_for_manifest_generation');
|
|
211
|
+
expect(response.solutionData.userAnswers).toMatchObject({
|
|
212
|
+
name: 'my-app',
|
|
213
|
+
port: 8080,
|
|
214
|
+
replicas: 3,
|
|
215
|
+
'scaling-enabled': false,
|
|
216
|
+
open: 'N/A'
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should validate answers against current stage questions only', async () => {
|
|
221
|
+
const context = createMockToolContext();
|
|
222
|
+
|
|
223
|
+
const result = await handleAnswerQuestionTool({
|
|
224
|
+
solutionId: TEST_SOLUTION_ID,
|
|
225
|
+
stage: 'required',
|
|
226
|
+
answers: {
|
|
227
|
+
replicas: 3 // Wrong stage - replicas is basic, not required
|
|
228
|
+
}
|
|
229
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
230
|
+
|
|
231
|
+
const response = JSON.parse(result.content[0].text);
|
|
232
|
+
expect(response.status).toBe('stage_error');
|
|
233
|
+
expect(response.error).toBe('validation_failed');
|
|
234
|
+
expect(response.validationErrors[0]).toContain("Unknown question ID 'replicas' for stage 'required'");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('should include nextAction: answerQuestion for intermediate stages', async () => {
|
|
238
|
+
const context = createMockToolContext();
|
|
239
|
+
|
|
240
|
+
// Test required stage with some answers
|
|
241
|
+
const result = await handleAnswerQuestionTool({
|
|
242
|
+
solutionId: TEST_SOLUTION_ID,
|
|
243
|
+
stage: 'required',
|
|
244
|
+
answers: {
|
|
245
|
+
name: 'my-app',
|
|
246
|
+
port: 8080
|
|
247
|
+
}
|
|
248
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
249
|
+
|
|
250
|
+
const response = JSON.parse(result.content[0].text);
|
|
251
|
+
expect(response.status).toBe('stage_questions');
|
|
252
|
+
expect(response.currentStage).toBe('basic');
|
|
253
|
+
expect(response.nextAction).toBe('answerQuestion');
|
|
254
|
+
expect(response.nextAction).not.toBe('generateManifests');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('should only mention generateManifests when workflow complete', async () => {
|
|
258
|
+
const context = createMockToolContext();
|
|
259
|
+
|
|
260
|
+
// Mock solution with all previous stages complete
|
|
261
|
+
const completeSolution = createSolutionWithAnswers({
|
|
262
|
+
name: 'my-app',
|
|
263
|
+
port: 8080,
|
|
264
|
+
replicas: 3,
|
|
265
|
+
'scaling-enabled': false
|
|
266
|
+
});
|
|
267
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(completeSolution));
|
|
268
|
+
|
|
269
|
+
const result = await handleAnswerQuestionTool({
|
|
270
|
+
solutionId: TEST_SOLUTION_ID,
|
|
271
|
+
stage: 'open',
|
|
272
|
+
answers: {
|
|
273
|
+
open: 'N/A'
|
|
274
|
+
}
|
|
275
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
276
|
+
|
|
277
|
+
const response = JSON.parse(result.content[0].text);
|
|
278
|
+
expect(response.status).toBe('ready_for_manifest_generation');
|
|
279
|
+
expect(response.nextAction).toBe('generateManifests');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('should advance stage when empty answers provided for basic stage', async () => {
|
|
283
|
+
const context = createMockToolContext();
|
|
284
|
+
|
|
285
|
+
// Mock solution with required questions answered but basic questions unanswered
|
|
286
|
+
const solutionWithRequired = createSolutionWithAnswers({
|
|
287
|
+
name: 'my-app',
|
|
288
|
+
port: 8080
|
|
289
|
+
});
|
|
290
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithRequired));
|
|
291
|
+
|
|
292
|
+
const result = await handleAnswerQuestionTool({
|
|
293
|
+
solutionId: TEST_SOLUTION_ID,
|
|
294
|
+
stage: 'basic',
|
|
295
|
+
answers: {} // Empty answers = skip stage
|
|
296
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
297
|
+
|
|
298
|
+
const response = JSON.parse(result.content[0].text);
|
|
299
|
+
expect(response.status).toBe('stage_questions');
|
|
300
|
+
expect(response.currentStage).toBe('advanced'); // Should advance to next stage
|
|
301
|
+
expect(response.nextAction).toBe('answerQuestion');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('should advance stage when empty answers provided for advanced stage', async () => {
|
|
305
|
+
const context = createMockToolContext();
|
|
306
|
+
|
|
307
|
+
// Mock solution with required and basic questions answered but advanced questions unanswered
|
|
308
|
+
const solutionWithBasic = createSolutionWithAnswers({
|
|
309
|
+
name: 'my-app',
|
|
310
|
+
port: 8080,
|
|
311
|
+
replicas: 3
|
|
312
|
+
});
|
|
313
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithBasic));
|
|
314
|
+
|
|
315
|
+
const result = await handleAnswerQuestionTool({
|
|
316
|
+
solutionId: TEST_SOLUTION_ID,
|
|
317
|
+
stage: 'advanced',
|
|
318
|
+
answers: {} // Empty answers = skip stage
|
|
319
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
320
|
+
|
|
321
|
+
const response = JSON.parse(result.content[0].text);
|
|
322
|
+
expect(response.status).toBe('stage_questions');
|
|
323
|
+
expect(response.currentStage).toBe('open'); // Should advance to open stage
|
|
324
|
+
expect(response.nextAction).toBe('answerQuestion');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('should mark skipped questions with null answers', async () => {
|
|
328
|
+
const context = createMockToolContext();
|
|
329
|
+
|
|
330
|
+
// Mock solution with required questions answered
|
|
331
|
+
const solutionWithRequired = createSolutionWithAnswers({
|
|
332
|
+
name: 'my-app',
|
|
333
|
+
port: 8080
|
|
334
|
+
});
|
|
335
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithRequired));
|
|
336
|
+
|
|
337
|
+
// Capture what gets written to the file
|
|
338
|
+
let savedSolution: any;
|
|
339
|
+
mockFs.writeFileSync.mockImplementation((filePath, data) => {
|
|
340
|
+
if (typeof filePath === 'string' && (filePath === TEST_SOLUTION_PATH || filePath.includes('.tmp'))) {
|
|
341
|
+
if (typeof data === 'string' && data.includes('"solutionId"')) {
|
|
342
|
+
savedSolution = JSON.parse(data);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await handleAnswerQuestionTool({
|
|
348
|
+
solutionId: TEST_SOLUTION_ID,
|
|
349
|
+
stage: 'basic',
|
|
350
|
+
answers: {} // Empty answers = skip stage
|
|
351
|
+
}, context.dotAI, context.logger, context.requestId);
|
|
352
|
+
|
|
353
|
+
// Check that writeFileSync was called for the solution file
|
|
354
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
355
|
+
expect.stringContaining(TEST_SOLUTION_ID),
|
|
356
|
+
expect.any(String),
|
|
357
|
+
expect.any(String)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Verify all basic questions have null answers (marked as skipped)
|
|
361
|
+
expect(savedSolution).toBeDefined();
|
|
362
|
+
for (const question of savedSolution.questions.basic) {
|
|
363
|
+
expect(question.answer).toBe(null);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|