@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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Generate Manifests Tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
GENERATEMANIFESTS_TOOL_NAME,
|
|
7
|
+
GENERATEMANIFESTS_TOOL_DESCRIPTION,
|
|
8
|
+
GENERATEMANIFESTS_TOOL_INPUT_SCHEMA,
|
|
9
|
+
handleGenerateManifestsTool
|
|
10
|
+
} from '../../src/tools/generate-manifests';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
|
|
13
|
+
// Mock fs module
|
|
14
|
+
jest.mock('fs');
|
|
15
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
16
|
+
|
|
17
|
+
// Mock DotAI for schema retrieval tests
|
|
18
|
+
const mockDotAI = {
|
|
19
|
+
initialize: jest.fn(),
|
|
20
|
+
discovery: {
|
|
21
|
+
explainResource: jest.fn()
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
jest.mock('../../src/core/index', () => ({
|
|
26
|
+
DotAI: jest.fn(() => mockDotAI)
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock Claude integration
|
|
30
|
+
const mockClaudeIntegration = {
|
|
31
|
+
sendMessage: jest.fn()
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
jest.mock('../../src/core/claude', () => ({
|
|
35
|
+
ClaudeIntegration: jest.fn(() => mockClaudeIntegration)
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Mock child process for kubectl commands
|
|
39
|
+
jest.mock('child_process', () => ({
|
|
40
|
+
spawn: jest.fn()
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// Mock yaml library
|
|
44
|
+
jest.mock('js-yaml', () => ({
|
|
45
|
+
loadAll: jest.fn(),
|
|
46
|
+
dump: jest.fn()
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const mockLogger = {
|
|
50
|
+
debug: jest.fn(),
|
|
51
|
+
info: jest.fn(),
|
|
52
|
+
warn: jest.fn(),
|
|
53
|
+
error: jest.fn(),
|
|
54
|
+
fatal: jest.fn()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockContext: any = {
|
|
58
|
+
requestId: 'test-request',
|
|
59
|
+
logger: mockLogger,
|
|
60
|
+
dotAI: mockDotAI
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('Generate Manifests Tool', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
// Reset environment
|
|
67
|
+
delete process.env.DOT_AI_SESSION_DIR;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Input Validation', () => {
|
|
71
|
+
it('should validate solution ID format', async () => {
|
|
72
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
73
|
+
|
|
74
|
+
const args = {
|
|
75
|
+
solutionId: 'invalid-format'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
79
|
+
.rejects.toMatchObject({
|
|
80
|
+
message: 'Session directory does not exist: /test/session'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should require session directory from environment or args', async () => {
|
|
85
|
+
const args = {
|
|
86
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
90
|
+
.rejects.toMatchObject({
|
|
91
|
+
message: 'Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable'
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should accept session directory from args', async () => {
|
|
96
|
+
const args = {
|
|
97
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
98
|
+
sessionDir: '/nonexistent/path'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Should fail on directory validation, not on session dir config
|
|
102
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
103
|
+
.rejects.toMatchObject({
|
|
104
|
+
message: 'Session directory does not exist: /nonexistent/path'
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should accept session directory from environment', async () => {
|
|
109
|
+
process.env.DOT_AI_SESSION_DIR = '/nonexistent/path';
|
|
110
|
+
|
|
111
|
+
const args = {
|
|
112
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Should fail on directory validation, not on session dir config
|
|
116
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
117
|
+
.rejects.toMatchObject({
|
|
118
|
+
message: 'Session directory does not exist: /nonexistent/path'
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Tool Metadata', () => {
|
|
124
|
+
it('should have valid MCP tool metadata', () => {
|
|
125
|
+
expect(GENERATEMANIFESTS_TOOL_NAME).toBe('generateManifests');
|
|
126
|
+
expect(GENERATEMANIFESTS_TOOL_DESCRIPTION).toContain('Generate final Kubernetes manifests');
|
|
127
|
+
expect(GENERATEMANIFESTS_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Logging and Error Reporting', () => {
|
|
132
|
+
it('should log meaningful error messages', async () => {
|
|
133
|
+
const args = {
|
|
134
|
+
solutionId: 'invalid-format'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Should throw validation error before logging execution error
|
|
141
|
+
expect(error).toBeDefined();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle tool execution context properly', async () => {
|
|
146
|
+
const args = {
|
|
147
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Should fail with session directory error, not context error
|
|
154
|
+
expect((error as Error).message).toContain('Session directory must be specified via --session-dir parameter or DOT_AI_SESSION_DIR environment variable');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Schema Retrieval Functionality', () => {
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
jest.clearAllMocks();
|
|
162
|
+
|
|
163
|
+
// Setup basic file system mocks
|
|
164
|
+
mockFs.existsSync.mockImplementation((path: any) => {
|
|
165
|
+
if (typeof path === 'string') {
|
|
166
|
+
if (path.includes('/test/session')) return true;
|
|
167
|
+
if (path.includes('sol_2025-01-01T120000_abc123def456.json')) return true;
|
|
168
|
+
if (path.includes('.yaml')) return true; // Allow yaml file writes
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
mockFs.statSync.mockReturnValue({ isDirectory: () => true } as any);
|
|
174
|
+
mockFs.readdirSync.mockReturnValue([]);
|
|
175
|
+
mockFs.writeFileSync.mockImplementation(() => {});
|
|
176
|
+
mockFs.renameSync.mockImplementation(() => {});
|
|
177
|
+
mockFs.unlinkSync.mockImplementation(() => {});
|
|
178
|
+
|
|
179
|
+
// Mock Claude AI to return valid YAML
|
|
180
|
+
mockClaudeIntegration.sendMessage.mockResolvedValue({
|
|
181
|
+
content: `apiVersion: devopstoolkit.live/v1alpha1
|
|
182
|
+
kind: AppClaim
|
|
183
|
+
metadata:
|
|
184
|
+
name: test-webapp
|
|
185
|
+
namespace: default
|
|
186
|
+
spec:
|
|
187
|
+
namespace: default
|
|
188
|
+
image: nginx:latest
|
|
189
|
+
tag: latest
|
|
190
|
+
port: 80
|
|
191
|
+
host: test-webapp.local`
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Mock yaml parsing to succeed
|
|
195
|
+
const yaml = require('js-yaml');
|
|
196
|
+
yaml.loadAll.mockImplementation(() => {}); // No errors = valid YAML
|
|
197
|
+
|
|
198
|
+
// Mock kubectl dry-run to succeed
|
|
199
|
+
const { spawn } = require('child_process');
|
|
200
|
+
const mockSpawn = {
|
|
201
|
+
stdout: { on: jest.fn((event, cb) => event === 'data' ? cb('') : null) },
|
|
202
|
+
stderr: { on: jest.fn((event, cb) => event === 'data' ? cb('') : null) },
|
|
203
|
+
on: jest.fn((event, cb) => event === 'close' ? cb(0) : null) // Exit code 0 = success
|
|
204
|
+
};
|
|
205
|
+
spawn.mockReturnValue(mockSpawn);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should retrieve schemas for all resources in solution', async () => {
|
|
209
|
+
// Mock solution with AppClaim resource
|
|
210
|
+
const mockSolution = {
|
|
211
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
212
|
+
resources: [
|
|
213
|
+
{
|
|
214
|
+
kind: 'AppClaim',
|
|
215
|
+
apiVersion: 'devopstoolkit.live/v1alpha1',
|
|
216
|
+
group: 'devopstoolkit.live'
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
questions: { required: [], basic: [], advanced: [], open: {} }
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSolution));
|
|
223
|
+
|
|
224
|
+
// Mock kubectl explain output as raw string
|
|
225
|
+
const mockExplanation = `GROUP: devopstoolkit.live
|
|
226
|
+
KIND: AppClaim
|
|
227
|
+
VERSION: v1alpha1
|
|
228
|
+
|
|
229
|
+
DESCRIPTION:
|
|
230
|
+
AppClaim resource for application deployment
|
|
231
|
+
|
|
232
|
+
FIELDS:
|
|
233
|
+
apiVersion <string>
|
|
234
|
+
APIVersion defines the versioned schema
|
|
235
|
+
|
|
236
|
+
kind <string>
|
|
237
|
+
Kind is a string value representing the REST resource
|
|
238
|
+
|
|
239
|
+
metadata <Object>
|
|
240
|
+
Standard object metadata
|
|
241
|
+
|
|
242
|
+
spec <Object> -required-
|
|
243
|
+
|
|
244
|
+
id <string> -required-
|
|
245
|
+
ID of this application
|
|
246
|
+
|
|
247
|
+
parameters <Object> -required-
|
|
248
|
+
|
|
249
|
+
image <string> -required-
|
|
250
|
+
The container image
|
|
251
|
+
|
|
252
|
+
port <integer>
|
|
253
|
+
The application port
|
|
254
|
+
|
|
255
|
+
host <string>
|
|
256
|
+
The host address of the application`;
|
|
257
|
+
|
|
258
|
+
mockDotAI.discovery.explainResource.mockResolvedValue(mockExplanation);
|
|
259
|
+
|
|
260
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
261
|
+
|
|
262
|
+
const args = {
|
|
263
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// This should succeed now that we have schema retrieval
|
|
267
|
+
const result = await handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
|
|
268
|
+
|
|
269
|
+
// Verify schema retrieval was attempted
|
|
270
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledWith('AppClaim');
|
|
271
|
+
|
|
272
|
+
// Verify the result contains manifest data
|
|
273
|
+
expect(result.content).toBeDefined();
|
|
274
|
+
expect(result.content[0].type).toBe('text');
|
|
275
|
+
|
|
276
|
+
const response = JSON.parse(result.content[0].text);
|
|
277
|
+
expect(response.success).toBe(true);
|
|
278
|
+
expect(response.solutionId).toBe('sol_2025-01-01T120000_abc123def456');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle multiple resources in solution', async () => {
|
|
282
|
+
const mockSolution = {
|
|
283
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
284
|
+
resources: [
|
|
285
|
+
{
|
|
286
|
+
kind: 'Deployment',
|
|
287
|
+
apiVersion: 'apps/v1',
|
|
288
|
+
group: 'apps'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
kind: 'Service',
|
|
292
|
+
apiVersion: 'v1',
|
|
293
|
+
group: ''
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
questions: { required: [], basic: [], advanced: [], open: {} }
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSolution));
|
|
300
|
+
|
|
301
|
+
// Mock different explanations for each resource
|
|
302
|
+
const deploymentExplanation = `GROUP: apps
|
|
303
|
+
KIND: Deployment
|
|
304
|
+
VERSION: v1
|
|
305
|
+
|
|
306
|
+
FIELDS:
|
|
307
|
+
spec <Object>
|
|
308
|
+
replicas <integer>
|
|
309
|
+
Number of desired pods`;
|
|
310
|
+
|
|
311
|
+
const serviceExplanation = `GROUP:
|
|
312
|
+
KIND: Service
|
|
313
|
+
VERSION: v1
|
|
314
|
+
|
|
315
|
+
FIELDS:
|
|
316
|
+
spec <Object>
|
|
317
|
+
ports <[]Object>
|
|
318
|
+
List of ports that are exposed`;
|
|
319
|
+
|
|
320
|
+
mockDotAI.discovery.explainResource
|
|
321
|
+
.mockResolvedValueOnce(deploymentExplanation)
|
|
322
|
+
.mockResolvedValueOnce(serviceExplanation);
|
|
323
|
+
|
|
324
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
325
|
+
|
|
326
|
+
const args = {
|
|
327
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
|
|
331
|
+
|
|
332
|
+
// Should call explainResource for each resource type
|
|
333
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledTimes(2);
|
|
334
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledWith('Deployment');
|
|
335
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledWith('Service');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle schema retrieval errors gracefully', async () => {
|
|
339
|
+
const mockSolution = {
|
|
340
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
341
|
+
resources: [
|
|
342
|
+
{
|
|
343
|
+
kind: 'UnknownResource',
|
|
344
|
+
apiVersion: 'example.com/v1',
|
|
345
|
+
group: 'example.com'
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
questions: { required: [], basic: [], advanced: [], open: {} }
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSolution));
|
|
352
|
+
|
|
353
|
+
// Mock schema retrieval failure
|
|
354
|
+
mockDotAI.discovery.explainResource.mockRejectedValue(new Error('Resource not found in cluster'));
|
|
355
|
+
|
|
356
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
357
|
+
|
|
358
|
+
const args = {
|
|
359
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
363
|
+
.rejects.toMatchObject({
|
|
364
|
+
message: expect.stringContaining('Failed to retrieve schema for UnknownResource')
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Should have attempted schema retrieval
|
|
368
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledWith('UnknownResource');
|
|
369
|
+
|
|
370
|
+
// Should have logged the error
|
|
371
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
372
|
+
'Failed to retrieve schema for resource',
|
|
373
|
+
expect.any(Error),
|
|
374
|
+
expect.objectContaining({
|
|
375
|
+
resource: expect.objectContaining({ kind: 'UnknownResource' })
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle solutions with no resources', async () => {
|
|
381
|
+
const mockSolution = {
|
|
382
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
383
|
+
resources: [], // No resources
|
|
384
|
+
questions: { required: [], basic: [], advanced: [], open: {} }
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSolution));
|
|
388
|
+
|
|
389
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
390
|
+
|
|
391
|
+
const args = {
|
|
392
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const result = await handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId);
|
|
396
|
+
|
|
397
|
+
// Should not attempt schema retrieval
|
|
398
|
+
expect(mockDotAI.discovery.explainResource).not.toHaveBeenCalled();
|
|
399
|
+
|
|
400
|
+
// Should log warning about no resources
|
|
401
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('No resources found in solution for schema retrieval');
|
|
402
|
+
|
|
403
|
+
// Should still complete successfully
|
|
404
|
+
const response = JSON.parse(result.content[0].text);
|
|
405
|
+
expect(response.success).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should fail fast when schema retrieval fails', async () => {
|
|
409
|
+
const mockSolution = {
|
|
410
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456',
|
|
411
|
+
resources: [
|
|
412
|
+
{
|
|
413
|
+
kind: 'AppClaim',
|
|
414
|
+
apiVersion: 'devopstoolkit.live/v1alpha1',
|
|
415
|
+
group: 'devopstoolkit.live'
|
|
416
|
+
}
|
|
417
|
+
],
|
|
418
|
+
questions: { required: [], basic: [], advanced: [], open: {} }
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSolution));
|
|
422
|
+
|
|
423
|
+
// Mock schema retrieval failure
|
|
424
|
+
mockDotAI.discovery.explainResource.mockRejectedValue(new Error('Cluster connection failed'));
|
|
425
|
+
|
|
426
|
+
process.env.DOT_AI_SESSION_DIR = '/test/session';
|
|
427
|
+
|
|
428
|
+
const args = {
|
|
429
|
+
solutionId: 'sol_2025-01-01T120000_abc123def456'
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
await expect(handleGenerateManifestsTool(args, mockContext.dotAI, mockContext.logger, mockContext.requestId))
|
|
433
|
+
.rejects.toMatchObject({
|
|
434
|
+
message: expect.stringContaining('Failed to retrieve resource schemas')
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(mockDotAI.discovery.explainResource).toHaveBeenCalledWith('AppClaim');
|
|
438
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Schema retrieval failed', expect.any(Error));
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tool Metadata and Integration
|
|
3
|
+
*
|
|
4
|
+
* Tests the tool metadata exports and their availability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
RECOMMEND_TOOL_NAME,
|
|
9
|
+
RECOMMEND_TOOL_DESCRIPTION,
|
|
10
|
+
RECOMMEND_TOOL_INPUT_SCHEMA,
|
|
11
|
+
handleRecommendTool
|
|
12
|
+
} from '../../src/tools/recommend';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CHOOSESOLUTION_TOOL_NAME,
|
|
16
|
+
CHOOSESOLUTION_TOOL_DESCRIPTION,
|
|
17
|
+
CHOOSESOLUTION_TOOL_INPUT_SCHEMA,
|
|
18
|
+
handleChooseSolutionTool
|
|
19
|
+
} from '../../src/tools/choose-solution';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ANSWERQUESTION_TOOL_NAME,
|
|
23
|
+
ANSWERQUESTION_TOOL_DESCRIPTION,
|
|
24
|
+
ANSWERQUESTION_TOOL_INPUT_SCHEMA,
|
|
25
|
+
handleAnswerQuestionTool
|
|
26
|
+
} from '../../src/tools/answer-question';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
GENERATEMANIFESTS_TOOL_NAME,
|
|
30
|
+
GENERATEMANIFESTS_TOOL_DESCRIPTION,
|
|
31
|
+
GENERATEMANIFESTS_TOOL_INPUT_SCHEMA,
|
|
32
|
+
handleGenerateManifestsTool
|
|
33
|
+
} from '../../src/tools/generate-manifests';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
DEPLOYMANIFESTS_TOOL_NAME,
|
|
37
|
+
DEPLOYMANIFESTS_TOOL_DESCRIPTION,
|
|
38
|
+
DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA,
|
|
39
|
+
handleDeployManifestsTool
|
|
40
|
+
} from '../../src/tools/deploy-manifests';
|
|
41
|
+
|
|
42
|
+
describe('Tool Integration', () => {
|
|
43
|
+
|
|
44
|
+
describe('Tool Metadata Availability', () => {
|
|
45
|
+
test('should have all required tool metadata available', () => {
|
|
46
|
+
const toolNames = [
|
|
47
|
+
RECOMMEND_TOOL_NAME,
|
|
48
|
+
CHOOSESOLUTION_TOOL_NAME,
|
|
49
|
+
ANSWERQUESTION_TOOL_NAME,
|
|
50
|
+
GENERATEMANIFESTS_TOOL_NAME,
|
|
51
|
+
DEPLOYMANIFESTS_TOOL_NAME
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
expect(toolNames).toHaveLength(5);
|
|
55
|
+
expect(toolNames).toContain('recommend');
|
|
56
|
+
expect(toolNames).toContain('chooseSolution');
|
|
57
|
+
expect(toolNames).toContain('answerQuestion');
|
|
58
|
+
expect(toolNames).toContain('generateManifests');
|
|
59
|
+
expect(toolNames).toContain('deployManifests');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should have all tool handlers available', () => {
|
|
63
|
+
const handlers = [
|
|
64
|
+
handleRecommendTool,
|
|
65
|
+
handleChooseSolutionTool,
|
|
66
|
+
handleAnswerQuestionTool,
|
|
67
|
+
handleGenerateManifestsTool,
|
|
68
|
+
handleDeployManifestsTool
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
handlers.forEach(handler => {
|
|
72
|
+
expect(typeof handler).toBe('function');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Tool Metadata Structure', () => {
|
|
78
|
+
test('recommend tool should have valid metadata', () => {
|
|
79
|
+
expect(RECOMMEND_TOOL_NAME).toBe('recommend');
|
|
80
|
+
expect(RECOMMEND_TOOL_DESCRIPTION).toContain('Deploy, create, run, or setup applications');
|
|
81
|
+
expect(RECOMMEND_TOOL_INPUT_SCHEMA.intent).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('chooseSolution tool should have valid metadata', () => {
|
|
85
|
+
expect(CHOOSESOLUTION_TOOL_NAME).toBe('chooseSolution');
|
|
86
|
+
expect(CHOOSESOLUTION_TOOL_DESCRIPTION).toContain('Select a solution');
|
|
87
|
+
expect(CHOOSESOLUTION_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('answerQuestion tool should have valid metadata', () => {
|
|
91
|
+
expect(ANSWERQUESTION_TOOL_NAME).toBe('answerQuestion');
|
|
92
|
+
expect(ANSWERQUESTION_TOOL_DESCRIPTION).toContain('Process user answers');
|
|
93
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
94
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.stage).toBeDefined();
|
|
95
|
+
expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.answers).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('generateManifests tool should have valid metadata', () => {
|
|
99
|
+
expect(GENERATEMANIFESTS_TOOL_NAME).toBe('generateManifests');
|
|
100
|
+
expect(GENERATEMANIFESTS_TOOL_DESCRIPTION).toContain('Generate final Kubernetes manifests');
|
|
101
|
+
expect(GENERATEMANIFESTS_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('deployManifests tool should have valid metadata', () => {
|
|
105
|
+
expect(DEPLOYMANIFESTS_TOOL_NAME).toBe('deployManifests');
|
|
106
|
+
expect(DEPLOYMANIFESTS_TOOL_DESCRIPTION).toContain('Deploy Kubernetes manifests');
|
|
107
|
+
expect(DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
|
|
108
|
+
expect(DEPLOYMANIFESTS_TOOL_INPUT_SCHEMA.timeout).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|