test(platform-service): cover ai diagnostics routes

This commit is contained in:
saravanakumardb1 2026-03-21 10:45:41 -07:00
parent 7c99f5a5fa
commit 26283b402a

View File

@ -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();
});
});