From 26283b402a278554135e7dc4319a4ce382c5197a Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 10:45:41 -0700 Subject: [PATCH] test(platform-service): cover ai diagnostics routes --- .../src/modules/ai-diagnostics/routes.test.ts | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 services/platform-service/src/modules/ai-diagnostics/routes.test.ts diff --git a/services/platform-service/src/modules/ai-diagnostics/routes.test.ts b/services/platform-service/src/modules/ai-diagnostics/routes.test.ts new file mode 100644 index 00000000..ade6b3fe --- /dev/null +++ b/services/platform-service/src/modules/ai-diagnostics/routes.test.ts @@ -0,0 +1,352 @@ +import Fastify from 'fastify'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const repositoryMock = { + saveNaturalLanguageQuery: vi.fn(), + findClustersByProduct: vi.fn(), + getLatestInsightForCluster: vi.fn(), + getErrorClusterById: vi.fn(), + findRelatedClusters: vi.fn(), + createDiagnosticInsight: vi.fn(), + updateErrorCluster: vi.fn(), + updateInsightFeedback: vi.fn(), + getActiveAlerts: vi.fn(), + getTopErrorTypes: vi.fn(), + getQueryHistory: vi.fn(), + acknowledgeAlert: vi.fn(), + resolveAlert: vi.fn(), +}; + +const queryParserMock = { + parseQuery: vi.fn(), + validateQuery: vi.fn(), + generateQuerySuggestions: vi.fn(), +}; + +const queryExecutorMock = { + executeQuery: vi.fn(), +}; + +const llmAnalyzerMock = { + analyzeRootCause: vi.fn(), + generatePatternSummary: vi.fn(), +}; + +const telemetryLinkingMock = { + aggregateClusterContext: vi.fn(), +}; + +vi.mock('./repository.js', () => repositoryMock); +vi.mock('./query-parser.js', () => queryParserMock); +vi.mock('./query-executor.js', () => queryExecutorMock); +vi.mock('./llm-analyzer.js', () => llmAnalyzerMock); +vi.mock('./telemetry-linking.js', () => telemetryLinkingMock); + +async function buildApp(payload?: { sub: string; productId?: string; role?: string }) { + const aiDiagnosticsRoutes = (await import('./routes.js')).default; + const app = Fastify({ logger: false }); + + if (payload) { + app.addHook('onRequest', async request => { + request.jwtPayload = payload; + }); + } + + await app.register(aiDiagnosticsRoutes, { prefix: '/api/ai-diagnostics' }); + return app; +} + +describe('aiDiagnosticsRoutes', () => { + beforeEach(() => { + vi.clearAllMocks(); + + queryParserMock.parseQuery.mockReturnValue({ + intent: 'root_cause', + entities: {}, + }); + queryParserMock.validateQuery.mockReturnValue({ + valid: true, + errors: [], + warnings: [], + }); + queryParserMock.generateQuerySuggestions.mockReturnValue([ + 'Investigate iOS crash spikes', + 'Compare Android regressions', + ]); + + queryExecutorMock.executeQuery.mockResolvedValue({ + aiResponse: 'Likely tied to the latest mobile release.', + confidence: 0.87, + supportingData: [{ type: 'cluster', id: 'ec_1', relevanceScore: 0.92 }], + executionTimeMs: 42, + }); + + repositoryMock.saveNaturalLanguageQuery.mockResolvedValue(undefined); + repositoryMock.findClustersByProduct.mockResolvedValue([]); + repositoryMock.getLatestInsightForCluster.mockResolvedValue(null); + repositoryMock.getErrorClusterById.mockResolvedValue(null); + repositoryMock.findRelatedClusters.mockResolvedValue([]); + repositoryMock.createDiagnosticInsight.mockResolvedValue(undefined); + repositoryMock.updateErrorCluster.mockResolvedValue(undefined); + repositoryMock.updateInsightFeedback.mockResolvedValue(undefined); + repositoryMock.getActiveAlerts.mockResolvedValue([]); + repositoryMock.getTopErrorTypes.mockResolvedValue([]); + repositoryMock.getQueryHistory.mockResolvedValue([]); + repositoryMock.acknowledgeAlert.mockResolvedValue(undefined); + repositoryMock.resolveAlert.mockResolvedValue(undefined); + + telemetryLinkingMock.aggregateClusterContext.mockResolvedValue({ + contextSummary: 'Most events originated from iOS 18 devices.', + }); + + llmAnalyzerMock.analyzeRootCause.mockResolvedValue({ + analysisType: 'root_cause', + generatedAt: '2026-03-21T12:00:00.000Z', + rootCauseCategory: 'logic', + hypothesis: 'A nil access happens after release gating is bypassed.', + reasoning: 'The stack signature clusters around the same view transition.', + confidence: 'high', + confidenceScore: 0.93, + evidence: [ + { + type: 'stack_trace', + description: 'Frames consistently point to the same code path.', + strength: 'strong', + }, + ], + suggestedInvestigation: ['Inspect the view transition guard.'], + potentialFixDirection: 'Reinstate the guard before state mutation.', + similarResolvedIssues: [], + feedbackStats: { helpful: 0, notHelpful: 0, engineerNotes: [] }, + modelUsed: 'gpt-4o-mini', + promptTokens: 120, + completionTokens: 60, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('rejects non-admin access', async () => { + const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'member' }); + + const res = await app.inject({ + method: 'GET', + url: '/api/ai-diagnostics/query-history', + }); + + expect(res.statusCode).toBe(401); + await app.close(); + }); + + it('POST /ai-diagnostics/query executes and persists a parsed query', async () => { + const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/ai-diagnostics/query', + payload: { + query: 'Why did iOS crash yesterday?', + productId: 'chronomind', + timeRange: { + start: '2026-03-20T00:00:00.000Z', + end: '2026-03-21T00:00:00.000Z', + }, + }, + }); + + expect(res.statusCode).toBe(200); + expect(queryParserMock.parseQuery).toHaveBeenCalledWith('Why did iOS crash yesterday?'); + expect(queryExecutorMock.executeQuery).toHaveBeenCalledWith( + expect.objectContaining({ + entities: expect.objectContaining({ + products: ['chronomind'], + timeRange: { + start: '2026-03-20T00:00:00.000Z', + end: '2026-03-21T00:00:00.000Z', + }, + }), + }), + { productId: 'chronomind', userId: 'admin_1' } + ); + expect(repositoryMock.saveNaturalLanguageQuery).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 'admin_1', + productId: 'chronomind', + rawQuery: 'Why did iOS crash yesterday?', + parsedIntent: 'root_cause', + }) + ); + + const body = res.json(); + expect(body.success).toBe(true); + expect(body.result.aiResponse).toContain('Likely tied'); + + await app.close(); + }); + + it('POST /ai-diagnostics/query returns 400 when parsed query validation fails', async () => { + queryParserMock.validateQuery.mockReturnValue({ + valid: false, + errors: ['missing product scope'], + warnings: [], + }); + + const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' }); + const res = await app.inject({ + method: 'POST', + url: '/api/ai-diagnostics/query', + payload: { + query: 'show me crashes', + }, + }); + + expect(res.statusCode).toBe(400); + expect(queryExecutorMock.executeQuery).not.toHaveBeenCalled(); + expect(repositoryMock.saveNaturalLanguageQuery).not.toHaveBeenCalled(); + expect(res.json()).toEqual({ + error: 'Invalid query', + details: ['missing product scope'], + }); + + await app.close(); + }); + + it('GET /ai-diagnostics/clusters enriches clusters with latest insights when requested', async () => { + repositoryMock.findClustersByProduct.mockResolvedValue([ + { + id: 'ec_1', + productId: 'lysnrai', + fingerprintHash: 'fp_1', + firstSeenAt: '2026-03-20T00:00:00.000Z', + lastSeenAt: '2026-03-21T00:00:00.000Z', + occurrenceCount: 10, + uniqueUsers: 4, + errorType: 'TypeError', + messageTemplate: 'Cannot read property', + stackSignature: 'stack-signature', + relatedClusterIds: [], + status: 'active', + createdAt: '2026-03-20T00:00:00.000Z', + updatedAt: '2026-03-21T00:00:00.000Z', + ttl: 7776000, + }, + ]); + repositoryMock.getLatestInsightForCluster.mockResolvedValue({ + id: 'di_1', + clusterId: 'ec_1', + productId: 'lysnrai', + analysisType: 'root_cause', + generatedAt: '2026-03-21T12:00:00.000Z', + rootCauseCategory: 'logic', + hypothesis: 'State is stale during retry.', + reasoning: 'All samples align to the same path.', + confidence: 'high', + confidenceScore: 0.9, + evidence: [], + suggestedInvestigation: [], + feedbackStats: { helpful: 0, notHelpful: 0, engineerNotes: [] }, + modelUsed: 'gpt-4o-mini', + promptTokens: 1, + completionTokens: 1, + createdAt: '2026-03-21T12:00:00.000Z', + updatedAt: '2026-03-21T12:00:00.000Z', + ttl: 7776000, + }); + + const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' }); + const res = await app.inject({ + method: 'GET', + url: '/api/ai-diagnostics/clusters?productId=lysnrai&includeInsights=true', + }); + + expect(res.statusCode).toBe(200); + expect(repositoryMock.findClustersByProduct).toHaveBeenCalledWith('lysnrai', { + status: undefined, + limit: 50, + }); + expect(repositoryMock.getLatestInsightForCluster).toHaveBeenCalledWith('ec_1', 'lysnrai'); + expect(res.json().clusters[0].latestInsight.id).toBe('di_1'); + + await app.close(); + }); + + it('POST /ai-diagnostics/clusters/:id/analyze stores a new insight and updates the cluster', async () => { + repositoryMock.getErrorClusterById.mockResolvedValue({ + id: 'ec_1', + productId: 'lysnrai', + fingerprintHash: 'fp_1', + firstSeenAt: '2026-03-20T00:00:00.000Z', + lastSeenAt: '2026-03-21T00:00:00.000Z', + occurrenceCount: 10, + uniqueUsers: 4, + errorType: 'TypeError', + messageTemplate: 'Cannot read property', + stackSignature: 'stack-signature', + relatedClusterIds: [], + status: 'active', + createdAt: '2026-03-20T00:00:00.000Z', + updatedAt: '2026-03-21T00:00:00.000Z', + ttl: 7776000, + }); + + const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' }); + const res = await app.inject({ + method: 'POST', + url: '/api/ai-diagnostics/clusters/ec_1/analyze?productId=lysnrai', + payload: { + analysisType: 'root_cause', + modelPreference: 'gpt-4o-mini', + }, + }); + + expect(res.statusCode).toBe(200); + expect(telemetryLinkingMock.aggregateClusterContext).toHaveBeenCalledWith( + expect.objectContaining({ id: 'ec_1' }), + [] + ); + expect(llmAnalyzerMock.analyzeRootCause).toHaveBeenCalledWith( + expect.objectContaining({ + cluster: expect.objectContaining({ id: 'ec_1' }), + analysisType: 'root_cause', + }) + ); + expect(repositoryMock.createDiagnosticInsight).toHaveBeenCalledWith( + expect.objectContaining({ + clusterId: 'ec_1', + productId: 'lysnrai', + hypothesis: 'A nil access happens after release gating is bypassed.', + }) + ); + expect(repositoryMock.updateErrorCluster).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'ec_1', + insightId: expect.stringMatching(/^di_/), + }) + ); + + await app.close(); + }); + + it('POST /ai-diagnostics/alerts/:id/acknowledge records the admin user and note', async () => { + const app = await buildApp({ sub: 'admin_2', productId: 'lysnrai', role: 'admin' }); + + const res = await app.inject({ + method: 'POST', + url: '/api/ai-diagnostics/alerts/pa_1/acknowledge', + payload: { + note: 'Assigned to the mobile on-call engineer.', + }, + }); + + expect(res.statusCode).toBe(200); + expect(repositoryMock.acknowledgeAlert).toHaveBeenCalledWith('pa_1', { + acknowledgedBy: 'admin_2', + note: 'Assigned to the mobile on-call engineer.', + }); + expect(res.json()).toEqual({ success: true, message: 'Alert acknowledged' }); + + await app.close(); + }); +});