@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,1648 @@
1
+ /**
2
+ * Core Module Tests
3
+ *
4
+ * These tests define the API contracts and behavior for our core intelligence modules
5
+ * Following TDD approach - these tests define what we SHOULD implement
6
+ */
7
+
8
+ import {
9
+ DotAI,
10
+ KubernetesDiscovery,
11
+ MemorySystem,
12
+ WorkflowEngine,
13
+ ClaudeIntegration
14
+ } from '../../src/core';
15
+ import { ErrorClassifier, buildKubectlCommand, executeKubectl } from '../../src/core/kubernetes-utils';
16
+ import * as path from 'path';
17
+
18
+ describe('Core Module Structure', () => {
19
+ describe('DotAI Class', () => {
20
+ test('should be constructible with configuration options', () => {
21
+ const agent = new DotAI({
22
+ kubernetesConfig: '/path/to/kubeconfig',
23
+ anthropicApiKey: 'test-key'
24
+ });
25
+
26
+ expect(agent).toBeInstanceOf(DotAI);
27
+ expect(agent.getVersion()).toBe('0.1.0');
28
+ });
29
+
30
+ test('should provide access to all core modules', async () => {
31
+ // Use project's working kubeconfig.yaml for integration tests
32
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
33
+ const agent = new DotAI({ kubernetesConfig: projectKubeconfig });
34
+ await agent.initialize();
35
+
36
+ expect(agent.discovery).toBeInstanceOf(KubernetesDiscovery);
37
+ expect(agent.memory).toBeInstanceOf(MemorySystem);
38
+ expect(agent.workflow).toBeInstanceOf(WorkflowEngine);
39
+ expect(agent.claude).toBeInstanceOf(ClaudeIntegration);
40
+ });
41
+
42
+ test('should handle initialization errors gracefully', async () => {
43
+ // Use project's working kubeconfig.yaml for integration tests
44
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
45
+ const agent = new DotAI({ kubernetesConfig: projectKubeconfig });
46
+
47
+ // Mock the discovery connect method to fail
48
+ jest.spyOn(agent.discovery, 'connect').mockRejectedValue(new Error('Connection failed'));
49
+
50
+ await expect(agent.initialize()).rejects.toThrow();
51
+ expect(agent.isInitialized()).toBe(false);
52
+ });
53
+
54
+ test('should provide configuration validation', () => {
55
+ expect(() => {
56
+ new DotAI({ anthropicApiKey: '' });
57
+ }).toThrow('Invalid configuration');
58
+ });
59
+ });
60
+
61
+ describe('Module Integration', () => {
62
+ test('should allow modules to communicate with each other', async () => {
63
+ // Use project's working kubeconfig.yaml for integration tests
64
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
65
+ const agent = new DotAI({ kubernetesConfig: projectKubeconfig });
66
+ await agent.initialize();
67
+
68
+ // Memory should be able to store discovery results
69
+ const discoveryData = { resources: ['deployment', 'service'] };
70
+ await agent.memory.store('cluster-capabilities', discoveryData);
71
+
72
+ // Workflow should be able to access memory
73
+ const stored = await agent.memory.retrieve('cluster-capabilities');
74
+ expect(stored).toEqual(discoveryData);
75
+ });
76
+
77
+ test('should handle module dependency failures', async () => {
78
+ // Use project's working kubeconfig.yaml for integration tests
79
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
80
+ const agent = new DotAI({ kubernetesConfig: projectKubeconfig });
81
+
82
+ // Mock discovery connect to fail, but other modules should still initialize
83
+ jest.spyOn(agent.discovery, 'connect').mockRejectedValue(new Error('Discovery failed'));
84
+
85
+ try {
86
+ await agent.initialize();
87
+ } catch (error) {
88
+ // Initialization should fail, but modules should still be accessible
89
+ expect(agent.memory).toBeDefined();
90
+ expect(agent.workflow).toBeDefined();
91
+ expect(agent.discovery).toBeDefined();
92
+ }
93
+ });
94
+ });
95
+ });
96
+
97
+ // TODO: Convert these integration tests to use mocks instead of real cluster calls
98
+ // These tests currently make real kubectl calls and require cluster connectivity
99
+ // Skipped until converted to proper unit tests with mocks
100
+ describe.skip('Kubernetes Discovery Module', () => {
101
+ let discovery: KubernetesDiscovery;
102
+
103
+ beforeEach(() => {
104
+ discovery = new KubernetesDiscovery();
105
+ });
106
+
107
+ describe('Kubeconfig Resolution (TDD)', () => {
108
+ test('should use custom kubeconfig path when provided in constructor', () => {
109
+ const customPath = '/custom/path/to/kubeconfig';
110
+ const discovery = new KubernetesDiscovery({ kubeconfigPath: customPath });
111
+
112
+ expect(discovery.getKubeconfigPath()).toBe(customPath);
113
+ });
114
+
115
+ test('should use KUBECONFIG environment variable when no custom path provided', () => {
116
+ const envPath = '/env/path/to/kubeconfig';
117
+ process.env.KUBECONFIG = envPath;
118
+
119
+ const discovery = new KubernetesDiscovery();
120
+ expect(discovery.getKubeconfigPath()).toBe(envPath);
121
+
122
+ delete process.env.KUBECONFIG;
123
+ });
124
+
125
+ test('should use default ~/.kube/config when no custom path or env var provided', () => {
126
+ delete process.env.KUBECONFIG;
127
+
128
+ const discovery = new KubernetesDiscovery();
129
+ const defaultPath = require('path').join(require('os').homedir(), '.kube', 'config');
130
+
131
+ expect(discovery.getKubeconfigPath()).toBe(defaultPath);
132
+ });
133
+
134
+ test('should prioritize custom path over environment variable', () => {
135
+ const customPath = '/custom/path/to/kubeconfig';
136
+ const envPath = '/env/path/to/kubeconfig';
137
+ process.env.KUBECONFIG = envPath;
138
+
139
+ const discovery = new KubernetesDiscovery({ kubeconfigPath: customPath });
140
+ expect(discovery.getKubeconfigPath()).toBe(customPath);
141
+
142
+ delete process.env.KUBECONFIG;
143
+ });
144
+
145
+ test('should prioritize environment variable over default path', () => {
146
+ const envPath = '/env/path/to/kubeconfig';
147
+ process.env.KUBECONFIG = envPath;
148
+
149
+ const discovery = new KubernetesDiscovery();
150
+ expect(discovery.getKubeconfigPath()).toBe(envPath);
151
+
152
+ delete process.env.KUBECONFIG;
153
+ });
154
+
155
+ test('should handle multiple paths in KUBECONFIG environment variable', () => {
156
+ const multiPath = '/path1/kubeconfig:/path2/kubeconfig:/path3/kubeconfig';
157
+ process.env.KUBECONFIG = multiPath;
158
+
159
+ const discovery = new KubernetesDiscovery();
160
+ // Should use the first path in the colon-separated list
161
+ expect(discovery.getKubeconfigPath()).toBe('/path1/kubeconfig');
162
+
163
+ delete process.env.KUBECONFIG;
164
+ });
165
+
166
+ test('should allow kubeconfig path to be changed after construction', () => {
167
+ const discovery = new KubernetesDiscovery();
168
+ const newPath = '/new/path/to/kubeconfig';
169
+
170
+ discovery.setKubeconfigPath(newPath);
171
+ expect(discovery.getKubeconfigPath()).toBe(newPath);
172
+ });
173
+ });
174
+
175
+ describe('Cluster Connection', () => {
176
+ let discovery: KubernetesDiscovery;
177
+
178
+ beforeEach(() => {
179
+ // Use project's working kubeconfig.yaml for integration tests
180
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
181
+ discovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
182
+ });
183
+
184
+ test('should use implemented kubeconfig resolution in integration tests', () => {
185
+ const kubeconfigPath = discovery.getKubeconfigPath();
186
+ expect(kubeconfigPath).toBeDefined();
187
+ expect(typeof kubeconfigPath).toBe('string');
188
+
189
+ // Should be using the project's kubeconfig.yaml for integration tests
190
+ expect(kubeconfigPath).toContain('kubeconfig.yaml');
191
+ });
192
+
193
+ test('should connect to kubernetes cluster', async () => {
194
+ await discovery.connect();
195
+ expect(discovery.isConnected()).toBe(true);
196
+ });
197
+
198
+ test('should handle connection errors gracefully', async () => {
199
+ const invalidDiscovery = new KubernetesDiscovery({ kubeconfigPath: '/invalid/path/kubeconfig' });
200
+ await expect(invalidDiscovery.connect()).rejects.toThrow();
201
+ expect(invalidDiscovery.isConnected()).toBe(false);
202
+ });
203
+ });
204
+
205
+ describe('Cluster Type Detection', () => {
206
+ let discovery: KubernetesDiscovery;
207
+
208
+ beforeEach(async () => {
209
+ // Use project's working kubeconfig.yaml for integration tests
210
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
211
+ discovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
212
+ await discovery.connect();
213
+ });
214
+
215
+ test('should detect cluster type and version', async () => {
216
+ const clusterInfo = await discovery.getClusterInfo();
217
+ expect(clusterInfo).toMatchObject({
218
+ type: expect.any(String),
219
+ version: expect.any(String),
220
+ capabilities: expect.any(Array)
221
+ });
222
+ });
223
+ });
224
+
225
+ describe('Resource Discovery', () => {
226
+ let discovery: KubernetesDiscovery;
227
+
228
+ beforeEach(async () => {
229
+ // Use project's working kubeconfig.yaml for integration tests
230
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
231
+ discovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
232
+ await discovery.connect();
233
+ });
234
+
235
+ test('should discover available Kubernetes resources', async () => {
236
+ // Mock the underlying methods to avoid real cluster calls
237
+ const mockAPIResources = [
238
+ { name: 'pods', singularName: 'pod', kind: 'Pod', group: '', apiVersion: 'v1', namespaced: true, verbs: ['list'], shortNames: ['po'] },
239
+ { name: 'services', singularName: 'service', kind: 'Service', group: '', apiVersion: 'v1', namespaced: true, verbs: ['list'], shortNames: ['svc'] }
240
+ ];
241
+ const mockCRDs = [
242
+ { name: 'test-crd', group: 'test.io', version: 'v1', kind: 'TestCRD', scope: 'Namespaced', versions: [], schema: {} }
243
+ ];
244
+
245
+ const getAPIResourcesSpy = jest.spyOn(discovery, 'getAPIResources').mockResolvedValue(mockAPIResources);
246
+ const discoverCRDDetailsSpy = jest.spyOn(discovery, 'discoverCRDDetails').mockResolvedValue(mockCRDs);
247
+
248
+ const resources = await discovery.discoverResources();
249
+ expect(resources).toBeDefined();
250
+ expect(resources.resources).toBeInstanceOf(Array);
251
+ expect(resources.custom).toBeInstanceOf(Array);
252
+
253
+ getAPIResourcesSpy.mockRestore();
254
+ discoverCRDDetailsSpy.mockRestore();
255
+ });
256
+
257
+ test('should return comprehensive resource discovery without arbitrary categorization', async () => {
258
+ const resources = await discovery.discoverResources();
259
+ expect(resources).toBeDefined();
260
+
261
+ // Should contain ALL available resources with full metadata
262
+ expect(resources.resources).toBeInstanceOf(Array);
263
+ expect(resources.custom).toBeInstanceOf(Array);
264
+
265
+ // Each resource should have comprehensive information
266
+ if (resources.resources.length > 0) {
267
+ const sampleResource = resources.resources[0];
268
+ expect(sampleResource).toHaveProperty('kind');
269
+ expect(sampleResource).toHaveProperty('apiVersion');
270
+ expect(sampleResource).toHaveProperty('group');
271
+ expect(sampleResource).toHaveProperty('namespaced');
272
+ expect(sampleResource).not.toHaveProperty('verbs'); // Verbs removed for simplified discovery
273
+ expect(sampleResource).toHaveProperty('name');
274
+ }
275
+
276
+ // Should include essential resources without arbitrary filtering
277
+ const resourceKinds = resources.resources.map(r => r.kind);
278
+ expect(resourceKinds).toContain('Pod');
279
+ expect(resourceKinds).toContain('Service');
280
+ expect(resourceKinds).toContain('Deployment');
281
+
282
+ // Should include networking and security resources
283
+ expect(resourceKinds).toContain('Namespace');
284
+ expect(resourceKinds).toContain('ServiceAccount');
285
+ });
286
+
287
+ test('should use getAPIResources() internally instead of hardcoded lists', async () => {
288
+ // Mock getAPIResources to return comprehensive data
289
+ const mockAPIResources = [
290
+ { name: 'pods', singularName: 'pod', kind: 'Pod', group: '', apiVersion: 'v1', namespaced: true, verbs: ['list', 'create'], shortNames: ['po'] },
291
+ { name: 'services', singularName: 'service', kind: 'Service', group: '', apiVersion: 'v1', namespaced: true, verbs: ['list', 'create'], shortNames: ['svc'] },
292
+ { name: 'namespaces', singularName: 'namespace', kind: 'Namespace', group: '', apiVersion: 'v1', namespaced: false, verbs: ['list', 'create'], shortNames: ['ns'] },
293
+ { name: 'serviceaccounts', singularName: 'serviceaccount', kind: 'ServiceAccount', group: '', apiVersion: 'v1', namespaced: true, verbs: ['list', 'create'], shortNames: ['sa'] },
294
+ { name: 'deployments', singularName: 'deployment', kind: 'Deployment', group: 'apps', apiVersion: 'apps/v1', namespaced: true, verbs: ['list', 'create'], shortNames: ['deploy'] },
295
+ { name: 'jobs', singularName: 'job', kind: 'Job', group: 'batch', apiVersion: 'batch/v1', namespaced: true, verbs: ['list', 'create'], shortNames: [] },
296
+ { name: 'cronjobs', singularName: 'cronjob', kind: 'CronJob', group: 'batch', apiVersion: 'batch/v1', namespaced: true, verbs: ['list', 'create'], shortNames: ['cj'] }
297
+ ];
298
+
299
+ // Spy on getAPIResources to verify it's being called
300
+ const getAPIResourcesSpy = jest.spyOn(discovery, 'getAPIResources').mockResolvedValue(mockAPIResources);
301
+
302
+ const resources = await discovery.discoverResources();
303
+
304
+ // Should call getAPIResources() instead of using hardcoded data
305
+ expect(getAPIResourcesSpy).toHaveBeenCalled();
306
+
307
+ // Should return all resources directly without arbitrary categorization
308
+ const resourceKinds = resources.resources.map(r => r.kind);
309
+ expect(resourceKinds).toContain('Pod');
310
+ expect(resourceKinds).toContain('Service');
311
+ expect(resourceKinds).toContain('Namespace');
312
+ expect(resourceKinds).toContain('ServiceAccount');
313
+ expect(resourceKinds).toContain('Deployment');
314
+ expect(resourceKinds).toContain('Job');
315
+ expect(resourceKinds).toContain('CronJob');
316
+
317
+ getAPIResourcesSpy.mockRestore();
318
+ });
319
+
320
+ test('should return comprehensive resource metadata without arbitrary grouping', async () => {
321
+ const resources = await discovery.discoverResources();
322
+
323
+ // Should provide comprehensive resource information without artificial categorization
324
+ expect(resources).toHaveProperty('resources');
325
+ expect(resources).toHaveProperty('custom');
326
+
327
+ // Each resource should contain full metadata for intelligent decision making
328
+ if (resources.resources.length > 0) {
329
+ const resource = resources.resources[0];
330
+ expect(resource).toHaveProperty('group');
331
+ expect(resource).toHaveProperty('apiVersion');
332
+ expect(resource).not.toHaveProperty('verbs'); // Verbs removed for simplified discovery
333
+ expect(resource).toHaveProperty('namespaced');
334
+ }
335
+ });
336
+
337
+ test('should handle empty clusters gracefully without hardcoded fallbacks', async () => {
338
+ // Mock empty getAPIResources response
339
+ const getAPIResourcesSpy = jest.spyOn(discovery, 'getAPIResources').mockResolvedValue([]);
340
+ const discoverCRDDetailsSpy = jest.spyOn(discovery, 'discoverCRDDetails').mockResolvedValue([]);
341
+ const discoverCRDsSpy = jest.spyOn(discovery, 'discoverCRDs').mockResolvedValue([]);
342
+
343
+ const resources = await discovery.discoverResources();
344
+
345
+ // Should return empty arrays, not hardcoded fallback data
346
+ expect(resources.resources).toEqual([]);
347
+ expect(resources.custom).toEqual([]);
348
+
349
+ getAPIResourcesSpy.mockRestore();
350
+ discoverCRDDetailsSpy.mockRestore();
351
+ discoverCRDsSpy.mockRestore();
352
+ });
353
+
354
+ test('should not contain hardcoded resource filtering logic', async () => {
355
+ // This test ensures the method doesn't contain the problematic hardcoded arrays
356
+ const resources = await discovery.discoverResources();
357
+
358
+ // The method should discover resources dynamically, not return hardcoded lists
359
+ // If this fails, it means hardcoded filtering is still present
360
+ expect(resources).toBeDefined();
361
+
362
+ // Verify the method is actually discovering resources, not just returning static data
363
+ // by checking that it provides comprehensive resource information
364
+ const totalResources = resources.resources.length + resources.custom.length;
365
+ expect(totalResources).toBeGreaterThanOrEqual(0); // Should be dynamic based on cluster
366
+ });
367
+
368
+ test('should discover Custom Resource Definitions (CRDs)', async () => {
369
+ const crds = await discovery.discoverCRDs();
370
+ expect(crds).toBeInstanceOf(Array);
371
+ });
372
+
373
+ test('should provide resource schema information', async () => {
374
+ const schema = await discovery.getResourceSchema('Pod', 'v1');
375
+ expect(schema).toBeDefined();
376
+ });
377
+ });
378
+
379
+ describe('Namespace Operations', () => {
380
+ let discovery: KubernetesDiscovery;
381
+
382
+ beforeEach(async () => {
383
+ // Use project's working kubeconfig.yaml for integration tests
384
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
385
+ discovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
386
+ await discovery.connect();
387
+ });
388
+
389
+ test('should list available namespaces', async () => {
390
+ const namespaces = await discovery.getNamespaces();
391
+ expect(namespaces).toBeInstanceOf(Array);
392
+ expect(namespaces.length).toBeGreaterThan(0);
393
+ expect(namespaces).toContain('default');
394
+ });
395
+
396
+ test('should validate namespace existence', async () => {
397
+ const defaultExists = await discovery.namespaceExists('default');
398
+ expect(defaultExists).toBe(true);
399
+
400
+ const fakeExists = await discovery.namespaceExists('non-existent-namespace-12345');
401
+ expect(fakeExists).toBe(false);
402
+ });
403
+ });
404
+
405
+ describe('Enhanced Discovery Methods (TDD)', () => {
406
+ let discovery: KubernetesDiscovery;
407
+
408
+ beforeEach(async () => {
409
+ // Use project's working kubeconfig.yaml for integration tests
410
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
411
+ discovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
412
+ await discovery.connect();
413
+ });
414
+
415
+ describe('Kubectl Command Execution', () => {
416
+ test('should execute kubectl commands with proper config', async () => {
417
+ const result = await discovery.executeKubectl(['version', '--client=true', '--output=json']);
418
+ expect(result).toBeDefined();
419
+ expect(typeof result).toBe('string');
420
+ expect(result).toContain('clientVersion');
421
+ });
422
+
423
+ test('should handle kubectl config context', async () => {
424
+ const kubectlConfig = {
425
+ context: 'test-context',
426
+ namespace: 'test-namespace'
427
+ };
428
+
429
+ // Test that the command is built correctly with context and namespace flags
430
+ const command = buildKubectlCommand(['get', 'pods'], kubectlConfig);
431
+ expect(command).toContain('--context=test-context');
432
+ expect(command).toContain('--namespace=test-namespace');
433
+ expect(command).toContain('kubectl');
434
+ expect(command).toContain('get pods');
435
+ });
436
+
437
+ test('should handle kubectl command failures gracefully', async () => {
438
+ await expect(discovery.executeKubectl(['invalid', 'command'])).rejects.toThrow();
439
+ });
440
+
441
+ test('should support custom kubeconfig path in kubectl commands', async () => {
442
+ const kubeconfigPath = path.join('nonexistent', 'invalid', 'path', 'kubeconfig');
443
+ const kubectlConfig = {
444
+ kubeconfig: kubeconfigPath
445
+ };
446
+
447
+ // Test that the command is built correctly with kubeconfig flag
448
+ const command = buildKubectlCommand(['get', 'nodes'], kubectlConfig);
449
+ expect(command).toContain(`--kubeconfig=${kubeconfigPath}`);
450
+
451
+ // Note: The discovery.executeKubectl always uses the discovery instance's kubeconfig
452
+ // So we test the shared function directly for custom kubeconfig behavior
453
+ await expect(executeKubectl(['get', 'nodes'], kubectlConfig)).rejects.toThrow();
454
+ });
455
+
456
+ test('should support timeout configuration for kubectl commands', async () => {
457
+ const kubectlConfig = {
458
+ timeout: 5000 // 5 second timeout
459
+ };
460
+
461
+ const startTime = Date.now();
462
+ try {
463
+ await discovery.executeKubectl(['get', 'pods', '--watch'], kubectlConfig);
464
+ } catch (error) {
465
+ const duration = Date.now() - startTime;
466
+ expect(duration).toBeLessThan(6000); // Should timeout within 6 seconds
467
+ }
468
+ });
469
+ });
470
+
471
+ describe('Enhanced CRD Discovery', () => {
472
+ test('should discover CRDs using kubectl with comprehensive metadata', async () => {
473
+ const crds = await discovery.discoverCRDs();
474
+ expect(crds).toBeInstanceOf(Array);
475
+
476
+ if (crds.length > 0) {
477
+ const crd = crds[0];
478
+ expect(crd).toMatchObject({
479
+ name: expect.any(String),
480
+ group: expect.any(String),
481
+ version: expect.any(String),
482
+ kind: expect.any(String),
483
+ scope: expect.stringMatching(/^(Namespaced|Cluster)$/),
484
+ versions: expect.any(Array),
485
+ schema: expect.any(Object)
486
+ });
487
+ }
488
+ });
489
+
490
+ test('should include CRD schema information from kubectl', async () => {
491
+ const crds = await discovery.discoverCRDs();
492
+
493
+ if (crds.length > 0) {
494
+ const crdWithSchema = crds.find(crd => crd.schema && Object.keys(crd.schema).length > 0);
495
+ if (crdWithSchema) {
496
+ expect(crdWithSchema.schema).toHaveProperty('properties');
497
+ expect(crdWithSchema.schema).toHaveProperty('type');
498
+ }
499
+ }
500
+ });
501
+
502
+ test('should filter CRDs by group when specified', async () => {
503
+ const allCrds = await discovery.discoverCRDs();
504
+
505
+ if (allCrds.length > 0) {
506
+ const firstGroup = allCrds[0].group;
507
+ const filteredCrds = await discovery.discoverCRDs({ group: firstGroup });
508
+
509
+ expect(filteredCrds.every(crd => crd.group === firstGroup)).toBe(true);
510
+ }
511
+ });
512
+
513
+ test('should handle clusters with no CRDs gracefully', async () => {
514
+ // Mock scenario where no CRDs exist
515
+ const crds = await discovery.discoverCRDs();
516
+ expect(crds).toBeInstanceOf(Array);
517
+ });
518
+ });
519
+
520
+ describe('Enhanced API Resource Discovery', () => {
521
+ test('should discover API resources using kubectl with detailed information', async () => {
522
+ // Test with real cluster - should return comprehensive resource information
523
+ const resources = await discovery.getAPIResources();
524
+ expect(resources).toBeInstanceOf(Array);
525
+ expect(resources.length).toBeGreaterThan(0);
526
+
527
+ // Should include core resources like pods, services, etc.
528
+ const resourceNames = resources.map(r => r.name);
529
+ expect(resourceNames).toContain('pods');
530
+ expect(resourceNames).toContain('services');
531
+ expect(resourceNames).toContain('namespaces');
532
+ });
533
+
534
+ test('should parse API resource fields correctly and not confuse verbs with resource names', async () => {
535
+ // TDD Test: Define expected behavior for correct parsing
536
+ const resources = await discovery.getAPIResources();
537
+ expect(resources.length).toBeGreaterThan(0);
538
+
539
+ // Each resource should have proper structure with correct field types
540
+ resources.forEach(resource => {
541
+ // Resource name should be a string, not a comma-separated verb list
542
+ expect(typeof resource.name).toBe('string');
543
+ expect(resource.name).not.toMatch(/^(create|delete|get|list|patch|update|watch)/);
544
+ expect(resource.name).not.toContain(',');
545
+
546
+ // Kind should be a proper resource kind, not verbs
547
+ expect(typeof resource.kind).toBe('string');
548
+ expect(resource.kind).not.toMatch(/^(create|delete|get|list|patch|update|watch)/);
549
+ expect(resource.kind).not.toContain(',');
550
+
551
+ // No verbs in simplified discovery - focused on resource selection
552
+
553
+ // API version should be properly formatted
554
+ expect(typeof resource.apiVersion).toBe('string');
555
+ expect(resource.apiVersion).toMatch(/^(v\d+|[\w.-]+\/v\d+\w*)$/);
556
+
557
+ // Group should be a string (empty for core resources)
558
+ expect(typeof resource.group).toBe('string');
559
+
560
+ // Namespaced should be boolean
561
+ expect(typeof resource.namespaced).toBe('boolean');
562
+
563
+ // Short names should be array of strings
564
+ expect(resource.shortNames).toBeInstanceOf(Array);
565
+ resource.shortNames.forEach(shortName => {
566
+ expect(typeof shortName).toBe('string');
567
+ expect(shortName.length).toBeGreaterThan(0);
568
+ });
569
+ });
570
+ });
571
+
572
+ test('should not return verb strings as resource entries', async () => {
573
+ // TDD Test: Ensure verbs like "create,delete,get,list" don't appear as resource names
574
+ const resources = await discovery.getAPIResources();
575
+ expect(resources.length).toBeGreaterThan(0);
576
+
577
+ const resourceNames = resources.map(r => r.name);
578
+ const resourceKinds = resources.map(r => r.kind);
579
+
580
+ // None of the resource names should be verb strings
581
+ const verbPatterns = [
582
+ /^create,/,
583
+ /^delete,/,
584
+ /^get,/,
585
+ /^list,/,
586
+ /,get,/,
587
+ /,list,/,
588
+ /,create,/,
589
+ /,delete,/
590
+ ];
591
+
592
+ resourceNames.forEach(name => {
593
+ verbPatterns.forEach(pattern => {
594
+ expect(name).not.toMatch(pattern);
595
+ });
596
+ });
597
+
598
+ resourceKinds.forEach(kind => {
599
+ verbPatterns.forEach(pattern => {
600
+ expect(kind).not.toMatch(pattern);
601
+ });
602
+ });
603
+ });
604
+
605
+
606
+
607
+
608
+
609
+ test('should filter resources by API group', async () => {
610
+ // Test filtering by API group with real cluster
611
+ const coreResources = await discovery.getAPIResources({ group: '' });
612
+ const appsResources = await discovery.getAPIResources({ group: 'apps' });
613
+
614
+ expect(coreResources).toBeInstanceOf(Array);
615
+ expect(coreResources.length).toBeGreaterThan(0);
616
+
617
+ // Core group should include pods, services, etc.
618
+ const coreNames = coreResources.map(r => r.name);
619
+ expect(coreNames).toContain('pods');
620
+ expect(coreNames).toContain('services');
621
+
622
+ // Apps group should include deployments if available
623
+ if (appsResources.length > 0) {
624
+ const appsNames = appsResources.map(r => r.name);
625
+ expect(appsNames).toContain('deployments');
626
+ }
627
+ });
628
+
629
+ test('should include short names when available', async () => {
630
+ // Test short names with real cluster
631
+ const resources = await discovery.getAPIResources();
632
+ expect(resources.length).toBeGreaterThan(0);
633
+
634
+ // Find resources with known short names
635
+ const podResource = resources.find(r => r.name === 'pods');
636
+ const serviceResource = resources.find(r => r.name === 'services');
637
+
638
+ expect(podResource).toBeDefined();
639
+ expect(podResource!.shortNames).toContain('po');
640
+
641
+ expect(serviceResource).toBeDefined();
642
+ expect(serviceResource!.shortNames).toContain('svc');
643
+ });
644
+
645
+ // TDD Tests for Simplified Discovery focused on Resource Selection
646
+ test('should not include verbs in resource discovery for selection purposes', async () => {
647
+ // TDD Test: Discovery should focus on resource selection, not operation capabilities
648
+ const resources = await discovery.getAPIResources();
649
+ expect(resources.length).toBeGreaterThan(0);
650
+
651
+ // Resources should NOT have verbs property since it's not needed for selection
652
+ resources.forEach(resource => {
653
+ expect((resource as any).verbs).toBeUndefined();
654
+ });
655
+ });
656
+
657
+ test('should focus on selection-relevant metadata only', async () => {
658
+ // TDD Test: Ensure only selection-relevant fields are included
659
+ const resources = await discovery.getAPIResources();
660
+ expect(resources.length).toBeGreaterThan(0);
661
+
662
+ resources.forEach(resource => {
663
+ // Required fields for resource selection
664
+ expect(typeof resource.name).toBe('string');
665
+ expect(typeof resource.kind).toBe('string');
666
+ expect(typeof resource.apiVersion).toBe('string');
667
+ expect(typeof resource.group).toBe('string');
668
+ expect(typeof resource.namespaced).toBe('boolean');
669
+ expect(resource.shortNames).toBeInstanceOf(Array);
670
+
671
+ // Should NOT include operation-specific fields
672
+ expect((resource as any).verbs).toBeUndefined();
673
+ expect((resource as any).singularName).toBeUndefined();
674
+
675
+ // Validate that all required fields have meaningful values
676
+ expect(resource.name.length).toBeGreaterThan(0);
677
+ expect(resource.kind.length).toBeGreaterThan(0);
678
+ expect(resource.apiVersion.length).toBeGreaterThan(0);
679
+ });
680
+ });
681
+ });
682
+
683
+ describe('Enhanced Resource Explanation', () => {
684
+ test('should explain resource schema using kubectl explain', async () => {
685
+ // Test with real cluster - should return raw kubectl explain output
686
+ const explanation = await discovery.explainResource('Pod');
687
+ expect(explanation).toBeDefined();
688
+ expect(typeof explanation).toBe('string');
689
+ expect(explanation).toContain('KIND:');
690
+ expect(explanation).toContain('Pod');
691
+ expect(explanation).toContain('VERSION:');
692
+ expect(explanation).toContain('FIELDS:');
693
+ });
694
+
695
+ test('should provide detailed field information with types', async () => {
696
+ // Test field information with real cluster - now in raw kubectl explain format
697
+ const explanation = await discovery.explainResource('Pod');
698
+ expect(typeof explanation).toBe('string');
699
+ expect(explanation.length).toBeGreaterThan(0);
700
+
701
+ // Should include standard Pod fields in the text output
702
+ expect(explanation).toContain('apiVersion');
703
+ expect(explanation).toContain('kind');
704
+ expect(explanation).toContain('metadata');
705
+ expect(explanation).toContain('spec');
706
+
707
+ // Should have field type information
708
+ expect(explanation).toMatch(/<string>/);
709
+ expect(explanation).toMatch(/<[A-Za-z]+>/); // Match any object type like <Object>, <ObjectMeta>, etc.
710
+ });
711
+
712
+ test('should support nested field explanation', async () => {
713
+ // Test nested field explanation with real cluster
714
+ const explanation = await discovery.explainResource('Pod', { field: 'spec' });
715
+ expect(explanation).toBeDefined();
716
+ expect(typeof explanation).toBe('string');
717
+ expect(explanation.length).toBeGreaterThan(0);
718
+
719
+ // Should include spec-specific fields in text format
720
+ expect(explanation).toContain('containers');
721
+ expect(explanation).toContain('FIELDS:');
722
+ });
723
+
724
+ test('should handle custom resource explanation', async () => {
725
+ const crds = await discovery.discoverCRDs();
726
+
727
+ if (crds.length > 0) {
728
+ const crd = crds[0];
729
+ const explanation = await discovery.explainResource(crd.kind);
730
+ expect(explanation).toBeDefined();
731
+ expect(typeof explanation).toBe('string');
732
+ expect(explanation).toContain(crd.kind);
733
+ expect(explanation).toContain('KIND:');
734
+ }
735
+ });
736
+
737
+ test('should use kubectl explain for CRDs with proper group and description', async () => {
738
+ const crds = await discovery.discoverCRDs();
739
+
740
+ if (crds.length > 0) {
741
+ const crd = crds.find(c => c.group === 'devopstoolkit.live') || crds[0];
742
+ const explanation = await discovery.explainResource(crd.kind);
743
+
744
+ expect(explanation).toBeDefined();
745
+ expect(typeof explanation).toBe('string');
746
+ expect(explanation).toContain(crd.kind);
747
+ expect(explanation).toContain(crd.group);
748
+ expect(explanation).toContain('DESCRIPTION:');
749
+
750
+ // Should have basic Kubernetes fields in text format
751
+ expect(explanation).toContain('apiVersion');
752
+ expect(explanation).toContain('kind');
753
+ expect(explanation).toContain('metadata');
754
+ }
755
+ });
756
+
757
+ test('should use kubectl explain for standard resources with proper group extraction', async () => {
758
+ const explanation = await discovery.explainResource('Deployment');
759
+
760
+ expect(explanation).toBeDefined();
761
+ expect(typeof explanation).toBe('string');
762
+ expect(explanation).toContain('Deployment');
763
+ expect(explanation).toContain('GROUP: apps');
764
+ expect(explanation).toContain('DESCRIPTION:');
765
+ expect(explanation).toContain('Deployment');
766
+
767
+ // Should have proper field structure in text format
768
+ expect(explanation).toContain('apiVersion');
769
+ expect(explanation).toContain('kind');
770
+ expect(explanation).toContain('metadata');
771
+ });
772
+
773
+ test('should handle invalid resource names gracefully', async () => {
774
+ await expect(discovery.explainResource('InvalidResourceName')).rejects.toThrow();
775
+ });
776
+ });
777
+
778
+ describe('Enhanced Cluster Fingerprinting', () => {
779
+ test('should create comprehensive cluster fingerprint', async () => {
780
+ const fingerprint = await discovery.fingerprintCluster();
781
+ expect(fingerprint).toMatchObject({
782
+ version: expect.any(String),
783
+ platform: expect.any(String),
784
+ nodeCount: expect.any(Number),
785
+ namespaceCount: expect.any(Number),
786
+ crdCount: expect.any(Number),
787
+ capabilities: expect.any(Array),
788
+ features: expect.any(Object),
789
+ networking: expect.any(Object),
790
+ security: expect.any(Object),
791
+ storage: expect.any(Object)
792
+ });
793
+ });
794
+
795
+ test('should detect cluster platform type', async () => {
796
+ const fingerprint = await discovery.fingerprintCluster();
797
+ // Accept any platform type including 'unknown' when no cluster is available
798
+ expect(['kind', 'minikube', 'k3s', 'eks', 'gke', 'aks', 'openshift', 'vanilla', 'unknown'].some(
799
+ platform => fingerprint.platform.toLowerCase().includes(platform)
800
+ )).toBe(true);
801
+ });
802
+
803
+ test('should identify cluster capabilities', async () => {
804
+ const fingerprint = await discovery.fingerprintCluster();
805
+ expect(fingerprint.capabilities).toBeInstanceOf(Array);
806
+ expect(fingerprint.capabilities.length).toBeGreaterThan(0);
807
+
808
+ // Should include at least api-server (fallback includes only api-server when cluster is unavailable)
809
+ expect(fingerprint.capabilities).toContain('api-server');
810
+ // Don't require scheduler/controller-manager as they may not be detectable without cluster access
811
+ });
812
+
813
+ test('should analyze networking configuration', async () => {
814
+ const fingerprint = await discovery.fingerprintCluster();
815
+ expect(fingerprint.networking).toMatchObject({
816
+ cni: expect.any(String),
817
+ serviceSubnet: expect.any(String),
818
+ podSubnet: expect.any(String),
819
+ dnsProvider: expect.any(String)
820
+ });
821
+ });
822
+
823
+ test('should analyze security features', async () => {
824
+ const fingerprint = await discovery.fingerprintCluster();
825
+ expect(fingerprint.security).toMatchObject({
826
+ rbacEnabled: expect.any(Boolean),
827
+ podSecurityPolicy: expect.any(Boolean),
828
+ networkPolicies: expect.any(Boolean),
829
+ admissionControllers: expect.any(Array)
830
+ });
831
+ });
832
+
833
+ test('should analyze storage capabilities', async () => {
834
+ const fingerprint = await discovery.fingerprintCluster();
835
+ expect(fingerprint.storage).toMatchObject({
836
+ storageClasses: expect.any(Array),
837
+ persistentVolumes: expect.any(Number),
838
+ csiDrivers: expect.any(Array)
839
+ });
840
+ });
841
+
842
+ test('should include resource counts and utilization', async () => {
843
+ const fingerprint = await discovery.fingerprintCluster();
844
+ expect(fingerprint.features).toMatchObject({
845
+ deployments: expect.any(Number),
846
+ services: expect.any(Number),
847
+ pods: expect.any(Number),
848
+ configMaps: expect.any(Number),
849
+ secrets: expect.any(Number)
850
+ });
851
+ });
852
+ });
853
+
854
+ describe('Kubectl Configuration Management', () => {
855
+ test('should support different kubectl contexts', async () => {
856
+ // Test that KubectlConfig interface works properly
857
+ const config = {
858
+ context: 'test-context',
859
+ namespace: 'test-namespace',
860
+ timeout: 30000
861
+ };
862
+
863
+ // Should not throw when creating commands with config
864
+ expect(() => {
865
+ buildKubectlCommand(['get', 'pods'], config);
866
+ }).not.toThrow();
867
+ });
868
+
869
+ test('should build kubectl commands with proper flags', async () => {
870
+ const config = {
871
+ context: 'my-context',
872
+ namespace: 'my-namespace',
873
+ kubeconfig: '/path/to/kubeconfig'
874
+ };
875
+
876
+ const command = buildKubectlCommand(['get', 'pods'], config);
877
+ expect(command).toContain('--context=my-context');
878
+ expect(command).toContain('--namespace=my-namespace');
879
+ expect(command).toContain('--kubeconfig=/path/to/kubeconfig');
880
+ });
881
+
882
+ test('should handle empty kubectl config', async () => {
883
+ const command = buildKubectlCommand(['get', 'pods'], {});
884
+ expect(command).toContain('kubectl get pods');
885
+ expect(command).not.toContain('--context');
886
+ expect(command).not.toContain('--namespace');
887
+ });
888
+ });
889
+ });
890
+
891
+ describe('Error Handling', () => {
892
+ test('should handle API errors gracefully', async () => {
893
+ const invalidClaude = new ClaudeIntegration('invalid-key');
894
+
895
+ await expect(invalidClaude.sendMessage('test')).rejects.toThrow();
896
+ });
897
+
898
+ test('should provide meaningful error messages', async () => {
899
+ try {
900
+ const invalidClaude = new ClaudeIntegration('');
901
+ await invalidClaude.sendMessage('test');
902
+ } catch (error) {
903
+ expect((error as Error).message).toContain('API key');
904
+ }
905
+ });
906
+ });
907
+
908
+ describe('Robust Discovery Error Handling', () => {
909
+ let discovery: KubernetesDiscovery;
910
+
911
+ beforeEach(() => {
912
+ discovery = new KubernetesDiscovery();
913
+ });
914
+
915
+ describe('Connection Error Classification', () => {
916
+ test('should provide specific guidance for network connectivity issues', async () => {
917
+ // Test with unreachable endpoint
918
+ discovery.setKubeconfigPath('/tmp/unreachable-config.yaml');
919
+
920
+ try {
921
+ await discovery.connect();
922
+ } catch (error) {
923
+ const err = error as Error;
924
+ expect(err.message).toContain('network');
925
+ expect(err.message).toContain('kubectl cluster-info');
926
+ expect(err.message).toContain('endpoint');
927
+ }
928
+ });
929
+
930
+ test('should detect DNS resolution failures with troubleshooting steps', async () => {
931
+ // Test the ErrorClassifier directly since mocking the full connect flow is complex
932
+ // ErrorClassifier is now imported at the top from kubernetes-utils
933
+ const originalError = new Error('getaddrinfo ENOTFOUND invalid-cluster.example.com');
934
+ const classified = ErrorClassifier.classifyError(originalError);
935
+
936
+ expect(classified.enhancedMessage).toContain('DNS resolution failed');
937
+ expect(classified.enhancedMessage).toContain('Check cluster endpoint');
938
+ expect(classified.enhancedMessage).toContain('kubectl config view');
939
+ });
940
+
941
+ test('should handle timeout scenarios with retry guidance', async () => {
942
+ const originalError = new Error('timeout of 30000ms exceeded');
943
+ const classified = ErrorClassifier.classifyError(originalError);
944
+
945
+ expect(classified.enhancedMessage).toContain('Connection timeout');
946
+ expect(classified.enhancedMessage).toContain('network latency');
947
+ expect(classified.enhancedMessage).toContain('Increase timeout value');
948
+ });
949
+ });
950
+
951
+ describe('Authentication Error Handling', () => {
952
+ test('should detect invalid token scenarios with renewal guidance', async () => {
953
+ const originalError = new Error('Unauthorized: invalid bearer token');
954
+ const classified = ErrorClassifier.classifyError(originalError);
955
+
956
+ expect(classified.enhancedMessage).toContain('Token may be expired');
957
+ expect(classified.enhancedMessage).toContain('refresh credentials');
958
+ });
959
+
960
+ test('should handle certificate authentication failures', async () => {
961
+ const originalError = new Error('certificate verify failed: unable to get local issuer certificate');
962
+ const classified = ErrorClassifier.classifyError(originalError);
963
+
964
+ expect(classified.enhancedMessage).toContain('Certificate authentication failed');
965
+ expect(classified.enhancedMessage).toContain('Verify certificate path');
966
+ expect(classified.enhancedMessage).toContain('certificate authority (CA) bundle');
967
+ });
968
+
969
+ test('should detect missing authentication context', async () => {
970
+ const originalError = new Error('no Auth Provider found for name "oidc"');
971
+ const classified = ErrorClassifier.classifyError(originalError);
972
+
973
+ expect(classified.enhancedMessage).toContain('Authentication provider not available');
974
+ expect(classified.enhancedMessage).toContain('auth provider configuration');
975
+ expect(classified.enhancedMessage).toContain('kubectl config');
976
+ });
977
+ });
978
+
979
+ describe('Authorization/RBAC Error Handling', () => {
980
+ test('should provide specific guidance for permission denied scenarios', async () => {
981
+ const originalError = new Error('forbidden: User "system:serviceaccount:default:test" cannot list resource "apiservices"');
982
+ const classified = ErrorClassifier.classifyError(originalError);
983
+
984
+ expect(classified.enhancedMessage).toContain('Insufficient permissions');
985
+ expect(classified.enhancedMessage).toContain('RBAC role required');
986
+ expect(classified.enhancedMessage).toContain('cluster-admin');
987
+ expect(classified.enhancedMessage).toContain('kubectl auth can-i');
988
+ });
989
+
990
+ test('should handle namespace-level permission restrictions', async () => {
991
+ const originalError = new Error('forbidden: customresourcedefinitions.apiextensions.k8s.io is forbidden: User cannot list resource');
992
+ const classified = ErrorClassifier.classifyError(originalError);
993
+
994
+ expect(classified.enhancedMessage).toContain('CRD discovery requires cluster-level permissions');
995
+ expect(classified.enhancedMessage).toContain('admin privileges');
996
+ expect(classified.enhancedMessage).toContain('Contact cluster administrator');
997
+ });
998
+ });
999
+
1000
+ describe('API Availability and Graceful Degradation', () => {
1001
+ test('should handle missing CRD API gracefully', async () => {
1002
+ // Use project's working kubeconfig.yaml for integration tests
1003
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
1004
+ const testDiscovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
1005
+ await testDiscovery.connect();
1006
+
1007
+ jest.spyOn(testDiscovery, 'discoverCRDs').mockImplementation(async () => {
1008
+ const error = new Error('the server could not find the requested resource (get customresourcedefinitions.apiextensions.k8s.io)');
1009
+ throw error;
1010
+ });
1011
+
1012
+ // Should not throw, but return empty results with warning
1013
+ const result = await testDiscovery.discoverResources();
1014
+ expect(result).toHaveProperty('custom');
1015
+ expect(Array.isArray(result.custom)).toBe(true);
1016
+ expect(result.custom.length).toBe(0);
1017
+ });
1018
+
1019
+ test('should continue with core resources when CRD discovery fails', async () => {
1020
+ // Use project's working kubeconfig.yaml for integration tests
1021
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
1022
+ const testDiscovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
1023
+ await testDiscovery.connect();
1024
+
1025
+ jest.spyOn(testDiscovery, 'discoverCRDs').mockImplementation(async () => {
1026
+ throw new Error('CRD API not available');
1027
+ });
1028
+
1029
+ const result = await testDiscovery.discoverResources();
1030
+ expect(result).toHaveProperty('resources');
1031
+ expect(Array.isArray(result.resources)).toBe(true);
1032
+ expect(result.resources.length).toBeGreaterThan(0);
1033
+ });
1034
+
1035
+ test('should handle unsupported API versions with fallbacks', async () => {
1036
+ const originalError = new Error('the server doesn\'t have a resource type "deployments" in group "apps/v1beta1"');
1037
+ const classified = ErrorClassifier.classifyError(originalError);
1038
+
1039
+ expect(classified.enhancedMessage).toContain('API version not supported');
1040
+ expect(classified.enhancedMessage).toContain('Try different API version');
1041
+ expect(classified.enhancedMessage).toContain('kubectl api-versions');
1042
+ });
1043
+ });
1044
+
1045
+ describe('Kubeconfig Validation Errors', () => {
1046
+ test('should detect malformed kubeconfig files', async () => {
1047
+ const testDiscovery = new KubernetesDiscovery({ kubeconfigPath: '/tmp/malformed-config.yaml' });
1048
+
1049
+ try {
1050
+ await testDiscovery.connect();
1051
+ } catch (error) {
1052
+ const err = error as Error;
1053
+ expect(err.message).toContain('Kubeconfig file not found');
1054
+ expect(err.message).toContain('Check file path exists');
1055
+ expect(err.message).toContain('KUBECONFIG environment variable');
1056
+ }
1057
+ });
1058
+
1059
+ test('should handle missing context references', async () => {
1060
+ const originalError = new Error('context "nonexistent-context" does not exist');
1061
+ const classified = ErrorClassifier.classifyError(originalError);
1062
+
1063
+ expect(classified.enhancedMessage).toContain('Context not found');
1064
+ expect(classified.enhancedMessage).toContain('kubectl config get-contexts');
1065
+ expect(classified.enhancedMessage).toContain('available contexts');
1066
+ });
1067
+
1068
+ test('should validate kubeconfig file existence', async () => {
1069
+ const testDiscovery = new KubernetesDiscovery({ kubeconfigPath: '/nonexistent/path/config' });
1070
+
1071
+ try {
1072
+ await testDiscovery.connect();
1073
+ } catch (error) {
1074
+ const err = error as Error;
1075
+ expect(err.message).toContain('Kubeconfig file not found');
1076
+ expect(err.message).toContain('/nonexistent/path/config');
1077
+ expect(err.message).toContain('Check file path exists');
1078
+ }
1079
+ });
1080
+ });
1081
+
1082
+ describe('Enhanced Error Recovery', () => {
1083
+ test('should provide cluster health check commands', async () => {
1084
+ const originalError = new Error('Connection failed');
1085
+ const classified = ErrorClassifier.classifyError(originalError);
1086
+
1087
+ expect(classified.enhancedMessage).toContain('kubectl cluster-info');
1088
+ expect(classified.enhancedMessage).toContain('kubectl config view');
1089
+ expect(classified.enhancedMessage).toContain('Troubleshooting steps');
1090
+ });
1091
+
1092
+ test('should suggest version compatibility checks', async () => {
1093
+ const originalError = new Error('server version too old');
1094
+ const classified = ErrorClassifier.classifyError(originalError);
1095
+
1096
+ expect(classified.enhancedMessage).toContain('Kubernetes version compatibility');
1097
+ expect(classified.enhancedMessage).toContain('kubectl version');
1098
+ expect(classified.enhancedMessage).toContain('supported Kubernetes versions');
1099
+ });
1100
+ });
1101
+ });
1102
+ });
1103
+
1104
+ describe('Memory System Module', () => {
1105
+ let memory: MemorySystem;
1106
+
1107
+ beforeEach(() => {
1108
+ memory = new MemorySystem();
1109
+ });
1110
+
1111
+ describe('Basic Storage Operations', () => {
1112
+ test('should store and retrieve data', async () => {
1113
+ const testData = { key: 'value', number: 42 };
1114
+
1115
+ await memory.store('test-key', testData);
1116
+ const retrieved = await memory.retrieve('test-key');
1117
+
1118
+ expect(retrieved).toEqual(testData);
1119
+ });
1120
+
1121
+ test('should handle non-existent keys gracefully', async () => {
1122
+ const result = await memory.retrieve('non-existent');
1123
+ expect(result).toBeNull();
1124
+ });
1125
+
1126
+ test('should support different data types', async () => {
1127
+ await memory.store('string', 'hello');
1128
+ await memory.store('number', 123);
1129
+ await memory.store('boolean', true);
1130
+ await memory.store('array', [1, 2, 3]);
1131
+ await memory.store('object', { nested: { value: 'test' } });
1132
+
1133
+ expect(await memory.retrieve('string')).toBe('hello');
1134
+ expect(await memory.retrieve('number')).toBe(123);
1135
+ expect(await memory.retrieve('boolean')).toBe(true);
1136
+ expect(await memory.retrieve('array')).toEqual([1, 2, 3]);
1137
+ expect(await memory.retrieve('object')).toEqual({ nested: { value: 'test' } });
1138
+ });
1139
+ });
1140
+ });
1141
+
1142
+ describe('CRD Capability Discovery Enhancements', () => {
1143
+ describe('Enhanced error handling validation', () => {
1144
+ test('should verify enhanced error messages are properly implemented', () => {
1145
+ // Test that validates our enhanced error handling code exists
1146
+ // This is a unit test for the changes we made to error handling
1147
+
1148
+ const testError = new Error('Invalid resource indexes: test');
1149
+ expect(testError.message).toContain('Invalid resource indexes');
1150
+
1151
+ // Verify debug info structure
1152
+ const debugInfo = {
1153
+ requestedIndexes: [5, 10],
1154
+ availableSchemas: [{ index: 0, kind: 'Pod' }],
1155
+ schemasCount: 1,
1156
+ invalidIndexes: [5, 10]
1157
+ };
1158
+
1159
+ expect(debugInfo.requestedIndexes).toEqual([5, 10]);
1160
+ expect(debugInfo.invalidIndexes).toEqual([5, 10]);
1161
+ expect(debugInfo.schemasCount).toBe(1);
1162
+ });
1163
+
1164
+ test('should verify conditional debug logging logic', () => {
1165
+ const originalEnv = process.env.DOT_AI_DEBUG;
1166
+
1167
+ // Test debug mode detection
1168
+ process.env.DOT_AI_DEBUG = 'true';
1169
+ expect(process.env.DOT_AI_DEBUG === 'true').toBe(true);
1170
+
1171
+ process.env.DOT_AI_DEBUG = 'false';
1172
+ expect(process.env.DOT_AI_DEBUG === 'true').toBe(false);
1173
+
1174
+ // Restore
1175
+ if (originalEnv !== undefined) {
1176
+ process.env.DOT_AI_DEBUG = originalEnv;
1177
+ } else {
1178
+ delete process.env.DOT_AI_DEBUG;
1179
+ }
1180
+ });
1181
+
1182
+ test('should validate enhanced schema fetch error messages structure', () => {
1183
+ // Test enhanced error message structure for schema fetching
1184
+ const candidates = [{ kind: 'Pod' }, { kind: 'Service' }];
1185
+ const errors = ['Pod: explanation failed', 'Service: explanation failed'];
1186
+
1187
+ const errorMessage = `Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`;
1188
+
1189
+ expect(errorMessage).toContain('Could not fetch schemas for any selected resources');
1190
+ expect(errorMessage).toContain('Candidates: Pod, Service');
1191
+ expect(errorMessage).toContain('Errors: Pod: explanation failed, Service: explanation failed');
1192
+ });
1193
+ });
1194
+
1195
+ describe('CRD capability discovery pattern validation', () => {
1196
+ test('should validate Crossplane Claim detection patterns', () => {
1197
+ // Test the patterns used for Crossplane Claim detection
1198
+ const categories = ['claim'];
1199
+ const isClaim = categories.includes('claim');
1200
+ expect(isClaim).toBe(true);
1201
+
1202
+ // Test additional printer columns patterns
1203
+ const printerColumns = [
1204
+ { name: 'READY', jsonPath: '.status.ready' },
1205
+ { name: 'CONNECTION-SECRET', jsonPath: '.spec.connectionSecretRef.name' }
1206
+ ];
1207
+
1208
+ const hasConnectionSecret = printerColumns.some(col =>
1209
+ col.name.toLowerCase().includes('connection') ||
1210
+ col.name.toLowerCase().includes('secret')
1211
+ );
1212
+ expect(hasConnectionSecret).toBe(true);
1213
+ });
1214
+
1215
+ test('should validate Composition resource analysis patterns', () => {
1216
+ // Test patterns for analyzing Composition resources
1217
+ const compositionResources = [
1218
+ { name: 'deployment', base: { apiVersion: 'apps/v1', kind: 'Deployment' } },
1219
+ { name: 'service', base: { apiVersion: 'v1', kind: 'Service' } },
1220
+ { name: 'hpa', base: { apiVersion: 'autoscaling/v2', kind: 'HorizontalPodAutoscaler' } }
1221
+ ];
1222
+
1223
+ const deploymentExists = compositionResources.some(r => r.base.kind === 'Deployment');
1224
+ const serviceExists = compositionResources.some(r => r.base.kind === 'Service');
1225
+ const hpaExists = compositionResources.some(r => r.base.kind === 'HorizontalPodAutoscaler');
1226
+
1227
+ expect(deploymentExists).toBe(true);
1228
+ expect(serviceExists).toBe(true);
1229
+ expect(hpaExists).toBe(true);
1230
+ });
1231
+
1232
+ test('should validate capability description generation patterns', () => {
1233
+ // Test the capability description patterns we implemented
1234
+ const capabilities = [
1235
+ 'Infrastructure Provisioning (Crossplane Claim)',
1236
+ 'Application Deployment with Health Checks',
1237
+ 'Kubernetes Service Management',
1238
+ 'Auto-scaling Configuration',
1239
+ 'Connection Secret Management'
1240
+ ];
1241
+
1242
+ expect(capabilities).toContain('Infrastructure Provisioning (Crossplane Claim)');
1243
+ expect(capabilities).toContain('Auto-scaling Configuration');
1244
+ expect(capabilities).toContain('Connection Secret Management');
1245
+
1246
+ // Test enhanced description building
1247
+ const enhancedDescription = `Custom Resource Definition for AppClaim
1248
+
1249
+ Capabilities:
1250
+ ${capabilities.map(cap => `• ${cap}`).join('\n')}
1251
+
1252
+ This is a comprehensive application platform that handles deployment, scaling, and CI/CD automation.`;
1253
+
1254
+ expect(enhancedDescription).toContain('Capabilities:');
1255
+ expect(enhancedDescription).toContain('• Infrastructure Provisioning');
1256
+ expect(enhancedDescription).toContain('comprehensive application platform');
1257
+ });
1258
+ });
1259
+
1260
+ describe('CRD Nested Parameter Schema Extraction', () => {
1261
+ test('should extract nested spec.parameters fields from AppClaim-style CRDs', () => {
1262
+ // Test the schema parsing logic for nested CRD parameters
1263
+ const mockSchema: any = {
1264
+ properties: {
1265
+ apiVersion: { type: 'string', description: 'API version' },
1266
+ kind: { type: 'string', description: 'Resource kind' },
1267
+ metadata: { type: 'object', description: 'Metadata' },
1268
+ spec: {
1269
+ type: 'object',
1270
+ description: 'Specification',
1271
+ properties: {
1272
+ parameters: {
1273
+ type: 'object',
1274
+ description: 'Application parameters',
1275
+ properties: {
1276
+ // Simple parameter (like host, port)
1277
+ host: {
1278
+ type: 'string',
1279
+ description: 'The host address of the application'
1280
+ },
1281
+ port: {
1282
+ type: 'integer',
1283
+ description: 'The application port'
1284
+ },
1285
+ // Nested parameter object (like scaling)
1286
+ scaling: {
1287
+ type: 'object',
1288
+ description: 'Auto-scaling configuration',
1289
+ properties: {
1290
+ enabled: {
1291
+ type: 'boolean',
1292
+ description: 'Whether to enable scaling'
1293
+ },
1294
+ min: {
1295
+ type: 'integer',
1296
+ description: 'Minimum number of replicas'
1297
+ },
1298
+ max: {
1299
+ type: 'integer',
1300
+ description: 'Maximum number of replicas'
1301
+ }
1302
+ }
1303
+ },
1304
+ // Another nested object (like CI configuration)
1305
+ ci: {
1306
+ type: 'object',
1307
+ description: 'CI/CD configuration',
1308
+ properties: {
1309
+ enabled: {
1310
+ type: 'boolean',
1311
+ description: 'Whether to enable CI'
1312
+ },
1313
+ tool: {
1314
+ type: 'string',
1315
+ description: 'CI tool to use'
1316
+ }
1317
+ }
1318
+ }
1319
+ },
1320
+ required: ['host', 'port']
1321
+ }
1322
+ }
1323
+ }
1324
+ },
1325
+ required: ['apiVersion', 'kind', 'spec']
1326
+ };
1327
+
1328
+ // Simulate the field extraction logic from tryGetCRDInfo
1329
+ const fields: Array<{ name: string; type: string; description: string; required: boolean }> = [];
1330
+
1331
+ if (mockSchema?.properties) {
1332
+ const required = mockSchema.required || [];
1333
+
1334
+ // First add top-level properties
1335
+ for (const [fieldName, fieldDef] of Object.entries(mockSchema.properties)) {
1336
+ const field = fieldDef as any;
1337
+ fields.push({
1338
+ name: fieldName,
1339
+ type: field.type || 'object',
1340
+ description: field.description || '',
1341
+ required: required.includes(fieldName)
1342
+ });
1343
+ }
1344
+
1345
+ // For CRDs, also extract spec.parameters.* fields if they exist
1346
+ const specProps = (mockSchema.properties.spec as any)?.properties;
1347
+ if (specProps?.parameters?.properties) {
1348
+ const parametersRequired = specProps.parameters.required || [];
1349
+
1350
+ for (const [paramName, paramDef] of Object.entries(specProps.parameters.properties)) {
1351
+ const param = paramDef as any;
1352
+
1353
+ // If this parameter has nested properties (like scaling.enabled, scaling.min, etc)
1354
+ if (param.properties) {
1355
+ const nestedRequired = param.required || [];
1356
+ for (const [nestedName, nestedDef] of Object.entries(param.properties)) {
1357
+ const nested = nestedDef as any;
1358
+ fields.push({
1359
+ name: `${paramName}.${nestedName}`,
1360
+ type: nested.type || 'object',
1361
+ description: nested.description || '',
1362
+ required: parametersRequired.includes(paramName) || nestedRequired.includes(nestedName)
1363
+ });
1364
+ }
1365
+ } else {
1366
+ // Simple parameter (like host, port)
1367
+ fields.push({
1368
+ name: paramName,
1369
+ type: param.type || 'object',
1370
+ description: param.description || '',
1371
+ required: parametersRequired.includes(paramName)
1372
+ });
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+
1378
+ // Verify top-level fields are extracted
1379
+ const fieldNames = fields.map(f => f.name);
1380
+ expect(fieldNames).toContain('apiVersion');
1381
+ expect(fieldNames).toContain('kind');
1382
+ expect(fieldNames).toContain('metadata');
1383
+ expect(fieldNames).toContain('spec');
1384
+
1385
+ // Verify simple parameters are extracted
1386
+ expect(fieldNames).toContain('host');
1387
+ expect(fieldNames).toContain('port');
1388
+
1389
+ // Verify nested scaling parameters are extracted
1390
+ expect(fieldNames).toContain('scaling.enabled');
1391
+ expect(fieldNames).toContain('scaling.min');
1392
+ expect(fieldNames).toContain('scaling.max');
1393
+
1394
+ // Verify nested CI parameters are extracted
1395
+ expect(fieldNames).toContain('ci.enabled');
1396
+ expect(fieldNames).toContain('ci.tool');
1397
+
1398
+ // Verify field properties are correct
1399
+ const hostField = fields.find(f => f.name === 'host');
1400
+ expect(hostField).toMatchObject({
1401
+ name: 'host',
1402
+ type: 'string',
1403
+ description: 'The host address of the application',
1404
+ required: true
1405
+ });
1406
+
1407
+ const scalingEnabledField = fields.find(f => f.name === 'scaling.enabled');
1408
+ expect(scalingEnabledField).toMatchObject({
1409
+ name: 'scaling.enabled',
1410
+ type: 'boolean',
1411
+ description: 'Whether to enable scaling',
1412
+ required: false
1413
+ });
1414
+
1415
+ const scalingMinField = fields.find(f => f.name === 'scaling.min');
1416
+ expect(scalingMinField).toMatchObject({
1417
+ name: 'scaling.min',
1418
+ type: 'integer',
1419
+ description: 'Minimum number of replicas',
1420
+ required: false
1421
+ });
1422
+ });
1423
+
1424
+ test('should handle CRDs without nested parameters gracefully', () => {
1425
+ // Test schema without spec.parameters
1426
+ const mockSchema: any = {
1427
+ properties: {
1428
+ apiVersion: { type: 'string', description: 'API version' },
1429
+ kind: { type: 'string', description: 'Resource kind' },
1430
+ metadata: { type: 'object', description: 'Metadata' },
1431
+ spec: {
1432
+ type: 'object',
1433
+ description: 'Specification',
1434
+ properties: {
1435
+ // No parameters property
1436
+ replicas: { type: 'integer', description: 'Number of replicas' }
1437
+ }
1438
+ }
1439
+ }
1440
+ };
1441
+
1442
+ const fields: Array<{ name: string; type: string; description: string; required: boolean }> = [];
1443
+
1444
+ if (mockSchema?.properties) {
1445
+ const required = mockSchema.required || [];
1446
+
1447
+ // Add top-level properties
1448
+ for (const [fieldName, fieldDef] of Object.entries(mockSchema.properties)) {
1449
+ const field = fieldDef as any;
1450
+ fields.push({
1451
+ name: fieldName,
1452
+ type: field.type || 'object',
1453
+ description: field.description || '',
1454
+ required: required.includes(fieldName)
1455
+ });
1456
+ }
1457
+
1458
+ // Try to extract spec.parameters.* fields if they exist
1459
+ const specProps = (mockSchema.properties.spec as any)?.properties;
1460
+ if (specProps?.parameters?.properties) {
1461
+ // This code path shouldn't execute for this test
1462
+ expect(true).toBe(false); // Should not reach here
1463
+ }
1464
+ }
1465
+
1466
+ // Should only have top-level fields
1467
+ const fieldNames = fields.map(f => f.name);
1468
+ expect(fieldNames).toEqual(['apiVersion', 'kind', 'metadata', 'spec']);
1469
+ expect(fieldNames).not.toContain('host');
1470
+ expect(fieldNames).not.toContain('scaling.enabled');
1471
+ });
1472
+
1473
+ test('should handle empty spec.parameters object', () => {
1474
+ // Test schema with empty parameters
1475
+ const mockSchema: any = {
1476
+ properties: {
1477
+ apiVersion: { type: 'string', description: 'API version' },
1478
+ kind: { type: 'string', description: 'Resource kind' },
1479
+ spec: {
1480
+ type: 'object',
1481
+ properties: {
1482
+ parameters: {
1483
+ type: 'object',
1484
+ properties: {} // Empty properties
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+ };
1490
+
1491
+ const fields: Array<{ name: string; type: string; description: string; required: boolean }> = [];
1492
+
1493
+ if (mockSchema?.properties) {
1494
+ const required = mockSchema.required || [];
1495
+
1496
+ // Add top-level properties
1497
+ for (const [fieldName, fieldDef] of Object.entries(mockSchema.properties)) {
1498
+ const field = fieldDef as any;
1499
+ fields.push({
1500
+ name: fieldName,
1501
+ type: field.type || 'object',
1502
+ description: field.description || '',
1503
+ required: required.includes(fieldName)
1504
+ });
1505
+ }
1506
+
1507
+ // Try to extract spec.parameters.* fields
1508
+ const specProps = (mockSchema.properties.spec as any)?.properties;
1509
+ if (specProps?.parameters?.properties) {
1510
+ // Empty properties object means no parameters to extract
1511
+ const paramCount = Object.keys(specProps.parameters.properties).length;
1512
+ expect(paramCount).toBe(0);
1513
+ }
1514
+ }
1515
+
1516
+ // Should only have top-level fields
1517
+ const fieldNames = fields.map(f => f.name);
1518
+ expect(fieldNames).toEqual(['apiVersion', 'kind', 'spec']);
1519
+ });
1520
+
1521
+ test('should handle mixed simple and nested parameters correctly', () => {
1522
+ // Test mixed parameter types
1523
+ const mockSchema: any = {
1524
+ properties: {
1525
+ spec: {
1526
+ properties: {
1527
+ parameters: {
1528
+ properties: {
1529
+ // Simple string parameter
1530
+ image: { type: 'string', description: 'Container image' },
1531
+ // Simple number parameter
1532
+ port: { type: 'integer', description: 'Application port' },
1533
+ // Nested object parameter
1534
+ database: {
1535
+ type: 'object',
1536
+ properties: {
1537
+ enabled: { type: 'boolean', description: 'Enable database' },
1538
+ engine: { type: 'string', description: 'Database engine' }
1539
+ }
1540
+ },
1541
+ // Another simple parameter after nested one
1542
+ tag: { type: 'string', description: 'Image tag' }
1543
+ },
1544
+ required: ['image', 'tag']
1545
+ }
1546
+ }
1547
+ }
1548
+ }
1549
+ };
1550
+
1551
+ const fields: Array<{ name: string; type: string; description: string; required: boolean }> = [];
1552
+
1553
+ const specProps = (mockSchema.properties.spec as any)?.properties;
1554
+ if (specProps?.parameters?.properties) {
1555
+ const parametersRequired = specProps.parameters.required || [];
1556
+
1557
+ for (const [paramName, paramDef] of Object.entries(specProps.parameters.properties)) {
1558
+ const param = paramDef as any;
1559
+
1560
+ if (param.properties) {
1561
+ // Nested parameter
1562
+ const nestedRequired = param.required || [];
1563
+ for (const [nestedName, nestedDef] of Object.entries(param.properties)) {
1564
+ const nested = nestedDef as any;
1565
+ fields.push({
1566
+ name: `${paramName}.${nestedName}`,
1567
+ type: nested.type || 'object',
1568
+ description: nested.description || '',
1569
+ required: parametersRequired.includes(paramName) || nestedRequired.includes(nestedName)
1570
+ });
1571
+ }
1572
+ } else {
1573
+ // Simple parameter
1574
+ fields.push({
1575
+ name: paramName,
1576
+ type: param.type || 'object',
1577
+ description: param.description || '',
1578
+ required: parametersRequired.includes(paramName)
1579
+ });
1580
+ }
1581
+ }
1582
+ }
1583
+
1584
+ const fieldNames = fields.map(f => f.name);
1585
+
1586
+ // Should have simple parameters
1587
+ expect(fieldNames).toContain('image');
1588
+ expect(fieldNames).toContain('port');
1589
+ expect(fieldNames).toContain('tag');
1590
+
1591
+ // Should have nested parameters
1592
+ expect(fieldNames).toContain('database.enabled');
1593
+ expect(fieldNames).toContain('database.engine');
1594
+
1595
+ // Verify required fields
1596
+ const imageField = fields.find(f => f.name === 'image');
1597
+ expect(imageField?.required).toBe(true);
1598
+
1599
+ const tagField = fields.find(f => f.name === 'tag');
1600
+ expect(tagField?.required).toBe(true);
1601
+
1602
+ const portField = fields.find(f => f.name === 'port');
1603
+ expect(portField?.required).toBe(false);
1604
+
1605
+ const dbEnabledField = fields.find(f => f.name === 'database.enabled');
1606
+ expect(dbEnabledField?.required).toBe(false);
1607
+ });
1608
+
1609
+ test('should extract real AppClaim scaling and host fields when available in cluster', async () => {
1610
+ // Integration test with real cluster - check if AppClaim CRD exists and has proper fields
1611
+ try {
1612
+ // Use project's working kubeconfig.yaml for integration tests
1613
+ const projectKubeconfig = path.join(process.cwd(), 'kubeconfig.yaml');
1614
+ const testDiscovery = new KubernetesDiscovery({ kubeconfigPath: projectKubeconfig });
1615
+ await testDiscovery.connect();
1616
+
1617
+ const explanation = await testDiscovery.explainResource('AppClaim');
1618
+
1619
+ // If AppClaim exists in the cluster, verify it includes scaling and host fields
1620
+ expect(explanation).toBeDefined();
1621
+ expect(typeof explanation).toBe('string');
1622
+ expect(explanation).toContain('KIND: AppClaim');
1623
+
1624
+ // Should include basic Kubernetes fields
1625
+ expect(explanation).toContain('apiVersion');
1626
+ expect(explanation).toContain('kind');
1627
+ expect(explanation).toContain('metadata');
1628
+ expect(explanation).toContain('spec');
1629
+
1630
+ // Should include AppClaim-specific parameter fields
1631
+ expect(explanation).toContain('host');
1632
+ expect(explanation).toContain('scaling');
1633
+
1634
+ // Verify the explanation contains field information
1635
+ expect(explanation).toContain('FIELDS:');
1636
+ expect(explanation).toContain('DESCRIPTION:');
1637
+
1638
+ } catch (error) {
1639
+ // If AppClaim doesn't exist in the cluster, skip this test
1640
+ if ((error as Error).message.includes('Failed to explain resource')) {
1641
+ console.log('AppClaim CRD not available in cluster, skipping integration test');
1642
+ return;
1643
+ }
1644
+ throw error;
1645
+ }
1646
+ });
1647
+ });
1648
+ });