223 lines
7.2 KiB
TypeScript
223 lines
7.2 KiB
TypeScript
// 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 }
|
|
);
|
|
});
|
|
});
|
|
});
|