Skip to main content
Glama
operate.test.ts15.9 kB
/** * Integration Test: Operate Tool * * Tests the complete operational workflow via REST API against a real test cluster. * Validates AI-powered analysis, dry-run validation, pattern/policy integration, * and session management for Day 2 Kubernetes operations. */ import { describe, test, expect, beforeAll } from 'vitest'; import { IntegrationTest } from '../helpers/test-base.js'; describe.concurrent('Operate Tool Integration', () => { const integrationTest = new IntegrationTest(); const testNamespace = 'operate-test'; beforeAll(() => { // Verify we're using the test cluster const kubeconfig = process.env.KUBECONFIG; expect(kubeconfig).toContain('kubeconfig-test.yaml'); }); describe('Analysis Workflow', () => { test('should complete full workflow: create deployment → analyze update intent → execute approved changes → validate deployment updated', async () => { const testId = Date.now(); // SETUP: Create namespace await integrationTest.kubectl(`create namespace ${testNamespace}`); // SETUP: Create deployment with nginx:1.19 await integrationTest.kubectl(`apply -n ${testNamespace} -f - <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: test-api namespace: operate-test spec: replicas: 2 selector: matchLabels: app: test-api template: metadata: labels: app: test-api spec: containers: - name: nginx image: nginx:1.19 ports: - containerPort: 80 EOF`); // Wait for deployment to be ready await integrationTest.kubectl(`wait --for=condition=available --timeout=60s deployment/test-api -n ${testNamespace}`); // Verify deployment is running with correct image const deploymentJson = await integrationTest.kubectl(`get deployment test-api -n ${testNamespace} -o json`); const deployment = JSON.parse(deploymentJson); expect(deployment.spec.template.spec.containers[0].image).toBe('nginx:1.19'); // PHASE 1: AI Analysis - Request operation const analysisResponse = await integrationTest.httpClient.post( '/api/v1/tools/operate', { intent: `update test-api deployment in ${testNamespace} namespace to nginx:1.20 with zero downtime`, interaction_id: `operate_test_${testId}` } ); // Validate response structure with specific expectations const expectedAnalysisResponse = { success: true, data: { result: { status: 'awaiting_user_approval', sessionId: expect.stringMatching(/^opr-\d+-[a-f0-9]{8}$/), // Session ID format: opr-{timestamp}-{uuid8} analysis: { summary: expect.stringContaining('nginx'), // Should mention nginx in analysis proposedChanges: { create: expect.any(Array), // May be empty for simple update update: expect.arrayContaining([ expect.objectContaining({ kind: 'Deployment', name: 'test-api', namespace: testNamespace, changes: expect.stringContaining('nginx:1.20'), // Should mention new version rationale: expect.any(String) }) ]), delete: expect.any(Array) // Should be empty for update operation }, commands: expect.arrayContaining([ expect.stringContaining('kubectl') // Should have kubectl commands ]), dryRunValidation: { status: 'success', // AI should validate with dry-run details: expect.stringContaining('validated') // Should mention validation }, patternsApplied: expect.any(Array), // May be empty if no patterns match capabilitiesUsed: expect.any(Array), // May be empty for simple update policiesChecked: expect.any(Array), // May be empty if no policies match risks: { level: expect.stringMatching(/low|medium|high/), description: expect.any(String) }, validationIntent: expect.any(String) // AI provides validation guidance }, message: expect.stringContaining('proposal'), nextAction: expect.stringContaining('executeChoice') // Should mention how to execute }, tool: 'operate' } }; expect(analysisResponse).toMatchObject(expectedAnalysisResponse); // Extract session ID for next phase const sessionId = analysisResponse.data.result.sessionId; expect(sessionId).toBeTruthy(); // PHASE 2: Verify original deployment unchanged (no execution yet) const unchangedDeploymentJson = await integrationTest.kubectl(`get deployment test-api -n ${testNamespace} -o json`); const unchangedDeployment = JSON.parse(unchangedDeploymentJson); expect(unchangedDeployment.spec.template.spec.containers[0].image).toBe('nginx:1.19'); // Still old version // PHASE 3: Execute approved changes (session existence is implicitly validated by this succeeding) const executionResponse = await integrationTest.httpClient.post( '/api/v1/tools/operate', { sessionId, executeChoice: 1 // Execute via MCP } ); // Validate execution response structure const expectedExecutionResponse = { success: true, data: { result: { status: 'success', // Should succeed sessionId, execution: { results: expect.arrayContaining([ expect.objectContaining({ command: expect.stringContaining('kubectl'), success: true, output: expect.any(String), timestamp: expect.any(String) }) ]), validation: expect.stringMatching(/Validation successful|Validation completed/) // Should have real validation result }, message: expect.stringContaining('executed successfully') } } }; expect(executionResponse).toMatchObject(expectedExecutionResponse); // Verify validation ran (not the placeholder message) const validation = executionResponse.data.result.execution.validation; expect(validation).not.toContain('coming in future milestone'); // PHASE 4: Validate deployment actually updated // Wait a moment for k8s to propagate changes await new Promise(resolve => setTimeout(resolve, 2000)); const updatedDeploymentJson = await integrationTest.kubectl(`get deployment test-api -n ${testNamespace} -o json`); const updatedDeployment = JSON.parse(updatedDeploymentJson); expect(updatedDeployment.spec.template.spec.containers[0].image).toBe('nginx:1.20'); // Should be updated to 1.20 }, 300000); // 5 minute timeout for full workflow (analysis + execution) test('should apply organizational patterns: scale with HPA creation', async () => { const testId = Date.now(); const patternNamespace = 'operate-pattern-test'; // SETUP: Create HPA scaling pattern via MCP endpoint (same workflow as pattern tests) const createResponse = await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create' }); const sessionId = createResponse.data.result.workflow.sessionId; // Step 1: Provide description (capability description) await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'Horizontal scaling with HPA' }); // Step 2: Provide trigger keywords await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'applications, scaling, replicas, horizontal' }); // Step 3: Confirm trigger expansion (AI shows expanded list) await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'HorizontalPodAutoscaler, scaling, horizontal scaling' }); // Step 4: Provide suggested resources await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'HorizontalPodAutoscaler' }); // Step 5: Provide rationale await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'All scaling operations should use HorizontalPodAutoscaler for managing multiple replicas, even if both min and max are the same.' }); // Step 6: Provide creator name await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'integration-test' }); // Step 7: Confirm pattern creation after review const finalResponse = await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'create', sessionId, response: 'confirm' }); expect(finalResponse.data.result.success).toBe(true); const patternId = finalResponse.data.result.storage.patternId; // Verify pattern was actually stored const getPatternResponse = await integrationTest.httpClient.post('/api/v1/tools/manageOrgData', { dataType: 'pattern', operation: 'get', id: patternId }); expect(getPatternResponse.data.result.success).toBe(true); expect(getPatternResponse.data.result.data).toBeDefined(); // SETUP: Create namespace await integrationTest.kubectl(`create namespace ${patternNamespace}`); // SETUP: Create deployment with 2 replicas await integrationTest.kubectl(`apply -n ${patternNamespace} -f - <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: test-api namespace: ${patternNamespace} spec: replicas: 2 selector: matchLabels: app: test-api template: metadata: labels: app: test-api spec: containers: - name: nginx image: nginx:1.19 ports: - containerPort: 80 EOF`); // Wait for deployment to be ready await integrationTest.kubectl(`wait --for=condition=available --timeout=60s deployment/test-api -n ${patternNamespace}`); // PHASE 1: AI Analysis - Request scaling operation const analysisResponse = await integrationTest.httpClient.post( '/api/v1/tools/operate', { intent: `scale test-api deployment in ${patternNamespace} namespace to 4 replicas`, interaction_id: `operate_pattern_test_${testId}` } ); // Validate response structure expect(analysisResponse.success).toBe(true); expect(analysisResponse.data.result.status).toBe('awaiting_user_approval'); const analysis = analysisResponse.data.result.analysis; // PHASE 2: Verify pattern was applied expect(analysis.patternsApplied).toBeInstanceOf(Array); expect(analysis.patternsApplied.length).toBeGreaterThan(0); // patternsApplied is an array of strings (pattern names/descriptions) const appliedPattern = analysis.patternsApplied.find((p: string) => p.toLowerCase().includes('horizontal') || p.toLowerCase().includes('hpa') || p.toLowerCase().includes('scaling') ); expect(appliedPattern).toBeDefined(); // PHASE 3: Verify proposed changes include HPA creation (pattern-driven!) const proposedChanges = analysis.proposedChanges; // Should create HPA with min=4, max=4 (pattern says even if both are the same) const hpaCreation = proposedChanges.create.find((change: any) => change.kind === 'HorizontalPodAutoscaler' ); expect(hpaCreation).toBeDefined(); expect(hpaCreation.rationale).toMatch(/pattern|hpa|horizontal|scaling|autoscal/i); // Verify manifest has namespace and correct replica counts expect(hpaCreation.manifest).toBeDefined(); expect(hpaCreation.manifest).toContain(`namespace: ${patternNamespace}`); expect(hpaCreation.manifest).toMatch(/minReplicas:\s*4/); // maxReplicas should be >= minReplicas (AI may choose a reasonable default) expect(hpaCreation.manifest).toMatch(/maxReplicas:\s*\d+/); // PHASE 4: Verify commands include HPA creation (no manual scaling needed - HPA manages replicas) const hasHpaCommand = analysis.commands.some((cmd: string) => cmd.includes('HorizontalPodAutoscaler') || cmd.includes('autoscaling') || cmd.includes('hpa') ); expect(hasHpaCommand).toBe(true); // PHASE 5: Execute approved changes const operateSessionId = analysisResponse.data.result.sessionId; const executionResponse = await integrationTest.httpClient.post( '/api/v1/tools/operate', { sessionId: operateSessionId, executeChoice: 1 // Execute via MCP } ); // Validate execution response structure expect(executionResponse.success).toBe(true); // Debug: log execution response if status is not success if (executionResponse.data.result.status !== 'success') { console.log('Execution failed. Response:', JSON.stringify(executionResponse.data.result, null, 2)); } expect(executionResponse.data.result.status).toBe('success'); expect(executionResponse.data.result.execution.results).toBeDefined(); // PHASE 6: Verify HPA actually created in cluster // Wait for k8s to propagate changes await new Promise(resolve => setTimeout(resolve, 3000)); const hpaJson = await integrationTest.kubectl(`get hpa -n ${patternNamespace} -o json`); const hpaList = JSON.parse(hpaJson); expect(hpaList.items).toBeDefined(); expect(hpaList.items.length).toBeGreaterThan(0); // Find the HPA for test-api const testApiHpa = hpaList.items.find((hpa: any) => hpa.spec.scaleTargetRef.name === 'test-api' ); expect(testApiHpa).toBeDefined(); expect(testApiHpa.spec.minReplicas).toBe(4); expect(testApiHpa.spec.maxReplicas).toBe(4); // PHASE 7: Verify HPA is functional (managing deployment replicas) // HPA should maintain deployment at 4 replicas (both min and max are 4) const deploymentJson = await integrationTest.kubectl(`get deployment test-api -n ${patternNamespace} -o json`); const deployment = JSON.parse(deploymentJson); // HPA should have set replicas to 4 (or will soon) // Note: HPA reconciliation may take a moment, so we check the HPA's scaleTargetRef is correct expect(testApiHpa.spec.scaleTargetRef.kind).toBe('Deployment'); expect(testApiHpa.spec.scaleTargetRef.name).toBe('test-api'); }, 300000); // 5 minute timeout for full workflow }); describe('Error Handling', () => { test('should handle missing intent parameter', async () => { const errorResponse = await integrationTest.httpClient.post( '/api/v1/tools/operate', {} // Missing intent ); const expectedErrorResponse = { success: true, data: { result: { status: 'failed', message: expect.stringContaining('Invalid input') } } }; expect(errorResponse).toMatchObject(expectedErrorResponse); }, 300000); // 5 minute timeout (may involve AI processing even for error cases) }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vfarcic/dot-ai'

If you have feedback or need assistance with the MCP directory API, please join our Discord server