690519:1631 224 to 226 AI #01
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
// File: src/modules/ai/intent-classifier/services/intent-analytics.service.spec.ts
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Unit tests สำหรับ IntentAnalyticsService (T033, US3).
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { IntentAnalyticsService } from './intent-analytics.service';
|
||||
import { AiAuditLog, AiAuditStatus } from '../../entities/ai-audit-log.entity';
|
||||
|
||||
/** สร้าง mock audit log */
|
||||
function mockLog(
|
||||
overrides: Partial<{
|
||||
method: string;
|
||||
intentCode: string;
|
||||
confidence: number;
|
||||
latencyMs: number;
|
||||
status: AiAuditStatus;
|
||||
}> = {}
|
||||
): AiAuditLog {
|
||||
const method = overrides.method ?? 'pattern';
|
||||
const intentCode = overrides.intentCode ?? 'GET_RFA';
|
||||
return {
|
||||
id: Math.floor(Math.random() * 1000),
|
||||
aiModel: 'intent-classifier',
|
||||
modelName: method === 'llm_fallback' ? 'gemma4:e4b' : 'pattern-match',
|
||||
aiSuggestionJson: {
|
||||
intentCode,
|
||||
confidence: overrides.confidence ?? 1.0,
|
||||
method,
|
||||
latencyMs: overrides.latencyMs ?? 3,
|
||||
},
|
||||
processingTimeMs: overrides.latencyMs ?? 3,
|
||||
confidenceScore: overrides.confidence ?? 1.0,
|
||||
status: overrides.status ?? AiAuditStatus.SUCCESS,
|
||||
createdAt: new Date(),
|
||||
} as AiAuditLog;
|
||||
}
|
||||
|
||||
describe('IntentAnalyticsService', () => {
|
||||
let service: IntentAnalyticsService;
|
||||
let mockQueryBuilder: Record<string, jest.Mock>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockQueryBuilder = {
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
IntentAnalyticsService,
|
||||
{
|
||||
provide: getRepositoryToken(AiAuditLog),
|
||||
useValue: {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<IntentAnalyticsService>(IntentAnalyticsService);
|
||||
});
|
||||
|
||||
describe('getAnalytics', () => {
|
||||
it('ควร return empty analytics เมื่อไม่มี data', async () => {
|
||||
mockQueryBuilder.getMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.totalRequests).toBe(0);
|
||||
expect(result.patternHitRate).toBe(0);
|
||||
expect(result.byMethod).toHaveLength(0);
|
||||
expect(result.byIntent).toHaveLength(0);
|
||||
expect(result.recalibration).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ควรคำนวณ patternHitRate ถูกต้อง', async () => {
|
||||
const logs = [
|
||||
mockLog({ method: 'pattern', intentCode: 'GET_RFA' }),
|
||||
mockLog({ method: 'pattern', intentCode: 'SUMMARIZE_DOCUMENT' }),
|
||||
mockLog({ method: 'pattern', intentCode: 'GET_DRAWING' }),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'GET_RFA',
|
||||
confidence: 0.85,
|
||||
latencyMs: 500,
|
||||
}),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.totalRequests).toBe(4);
|
||||
expect(result.patternHitRate).toBe(75); // 3/4 = 75%
|
||||
});
|
||||
|
||||
it('ควรนับ success/failed ถูกต้อง', async () => {
|
||||
const logs = [
|
||||
mockLog({ method: 'pattern', status: AiAuditStatus.SUCCESS }),
|
||||
mockLog({ method: 'pattern', status: AiAuditStatus.SUCCESS }),
|
||||
mockLog({ method: 'llm_error', status: AiAuditStatus.FAILED }),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.successCount).toBe(2);
|
||||
expect(result.failedCount).toBe(1);
|
||||
});
|
||||
|
||||
it('ควร group by method ถูกต้อง', async () => {
|
||||
const logs = [
|
||||
mockLog({ method: 'pattern', latencyMs: 2, confidence: 1.0 }),
|
||||
mockLog({ method: 'pattern', latencyMs: 4, confidence: 1.0 }),
|
||||
mockLog({ method: 'llm_fallback', latencyMs: 500, confidence: 0.8 }),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.byMethod).toHaveLength(2);
|
||||
const pattern = result.byMethod.find((m) => m.method === 'pattern');
|
||||
expect(pattern?.count).toBe(2);
|
||||
expect(pattern?.avgLatencyMs).toBe(3); // (2+4)/2
|
||||
expect(pattern?.avgConfidence).toBe(1.0);
|
||||
|
||||
const llm = result.byMethod.find((m) => m.method === 'llm_fallback');
|
||||
expect(llm?.count).toBe(1);
|
||||
expect(llm?.avgLatencyMs).toBe(500);
|
||||
});
|
||||
|
||||
it('ควร group by intent ถูกต้อง', async () => {
|
||||
const logs = [
|
||||
mockLog({ method: 'pattern', intentCode: 'GET_RFA' }),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'GET_RFA',
|
||||
confidence: 0.9,
|
||||
latencyMs: 400,
|
||||
}),
|
||||
mockLog({ method: 'pattern', intentCode: 'SUMMARIZE_DOCUMENT' }),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.byIntent).toHaveLength(2);
|
||||
const rfa = result.byIntent.find((i) => i.intentCode === 'GET_RFA');
|
||||
expect(rfa?.count).toBe(2);
|
||||
expect(rfa?.patternHits).toBe(1);
|
||||
expect(rfa?.llmHits).toBe(1);
|
||||
});
|
||||
|
||||
it('ควรสร้าง recalibration recommendations สำหรับ LLM-heavy intents', async () => {
|
||||
const logs = [
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'GET_DRAWING',
|
||||
confidence: 0.85,
|
||||
}),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'GET_DRAWING',
|
||||
confidence: 0.78,
|
||||
}),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'GET_DRAWING',
|
||||
confidence: 0.82,
|
||||
}),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'LIST_OVERDUE',
|
||||
confidence: 0.7,
|
||||
}),
|
||||
mockLog({ method: 'pattern', intentCode: 'GET_RFA' }),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
// GET_DRAWING ถูก LLM classify 3 ครั้ง → ควรอยู่อันดับ 1
|
||||
expect(result.recalibration.length).toBeGreaterThan(0);
|
||||
expect(result.recalibration[0].intentCode).toBe('GET_DRAWING');
|
||||
expect(result.recalibration[0].llmCallCount).toBe(3);
|
||||
});
|
||||
|
||||
it('ควรไม่ include FALLBACK ใน recalibration', async () => {
|
||||
const logs = [
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'FALLBACK',
|
||||
confidence: 0.2,
|
||||
}),
|
||||
mockLog({
|
||||
method: 'llm_fallback',
|
||||
intentCode: 'FALLBACK',
|
||||
confidence: 0.15,
|
||||
}),
|
||||
];
|
||||
mockQueryBuilder.getMany.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getAnalytics();
|
||||
|
||||
expect(result.recalibration).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ควรรับ date range parameters', async () => {
|
||||
mockQueryBuilder.getMany.mockResolvedValue([]);
|
||||
|
||||
const from = new Date('2026-01-01');
|
||||
const to = new Date('2026-01-31');
|
||||
await service.getAnalytics(from, to);
|
||||
|
||||
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
|
||||
'a.createdAt BETWEEN :from AND :to',
|
||||
{ from, to }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user