690519:1631 224 to 226 AI #01
CI / CD Pipeline / build (push) Failing after 3m57s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-05-19 16:31:50 +07:00
parent 3e25097470
commit ea5499123e
127 changed files with 12387 additions and 42 deletions
@@ -0,0 +1,239 @@
// File: hooks/ai/__tests__/use-intent-classification.test.ts
// Change Log
// - 2026-05-19: สร้าง Unit tests สำหรับ Intent Classification hooks (T038).
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { createTestQueryClient } from '@/lib/test-utils';
import {
useIntentDefinitions,
useIntentDefinition,
useIntentPatterns,
useCreateIntentDefinition,
useClassifyIntent,
} from '../use-intent-classification';
import { aiIntentService } from '@/lib/services/ai-intent.service';
// Mock service
vi.mock('@/lib/services/ai-intent.service', () => ({
aiIntentService: {
getDefinitions: vi.fn(),
getDefinition: vi.fn(),
getPatterns: vi.fn(),
createDefinition: vi.fn(),
updateDefinition: vi.fn(),
createPattern: vi.fn(),
updatePattern: vi.fn(),
deletePattern: vi.fn(),
classify: vi.fn(),
},
}));
describe('use-intent-classification hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useIntentDefinitions', () => {
it('ควรดึง definitions สำเร็จ', async () => {
const mockData = [
{ publicId: 'uuid-1', intentCode: 'GET_RFA', category: 'read' },
{ publicId: 'uuid-2', intentCode: 'SUMMARIZE_DOCUMENT', category: 'read' },
];
vi.mocked(aiIntentService.getDefinitions).mockResolvedValue(mockData);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useIntentDefinitions(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockData);
expect(aiIntentService.getDefinitions).toHaveBeenCalledWith(undefined);
});
it('ควรส่ง filter params ไปด้วย', async () => {
vi.mocked(aiIntentService.getDefinitions).mockResolvedValue([]);
const { wrapper } = createTestQueryClient();
renderHook(
() => useIntentDefinitions({ category: 'read', isActive: true }),
{ wrapper },
);
await waitFor(() => {
expect(aiIntentService.getDefinitions).toHaveBeenCalledWith({
category: 'read',
isActive: true,
});
});
});
});
describe('useIntentDefinition', () => {
it('ควรดึง definition ตาม intentCode', async () => {
const mockDef = {
publicId: 'uuid-1',
intentCode: 'GET_RFA',
descriptionTh: 'ดึง RFA',
descriptionEn: 'Get RFA',
category: 'read',
isActive: true,
};
vi.mocked(aiIntentService.getDefinition).mockResolvedValue(mockDef);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentDefinition('GET_RFA'),
{ wrapper },
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockDef);
expect(aiIntentService.getDefinition).toHaveBeenCalledWith('GET_RFA');
});
it('ควรไม่ fetch เมื่อ intentCode เป็นค่าว่าง', () => {
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentDefinition(''),
{ wrapper },
);
// enabled: !!intentCode → false → ไม่ fetch
expect(result.current.fetchStatus).toBe('idle');
expect(aiIntentService.getDefinition).not.toHaveBeenCalled();
});
});
describe('useIntentPatterns', () => {
it('ควรดึง patterns ตาม intentCode', async () => {
const mockPatterns = [
{ publicId: 'p-1', intentCode: 'GET_RFA', patternType: 'keyword', patternValue: 'rfa' },
];
vi.mocked(aiIntentService.getPatterns).mockResolvedValue(mockPatterns);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useIntentPatterns('GET_RFA'),
{ wrapper },
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockPatterns);
});
});
describe('useCreateIntentDefinition', () => {
it('ควรเรียก createDefinition สำเร็จ', async () => {
const newDef = {
intentCode: 'TEST_INTENT',
descriptionTh: 'ทดสอบ',
descriptionEn: 'Test',
category: 'utility' as const,
};
vi.mocked(aiIntentService.createDefinition).mockResolvedValue({
publicId: 'new-uuid',
...newDef,
isActive: true,
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useCreateIntentDefinition(),
{ wrapper },
);
result.current.mutate(newDef);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(aiIntentService.createDefinition).toHaveBeenCalledWith(newDef);
});
});
describe('useClassifyIntent', () => {
it('ควร classify query สำเร็จ', async () => {
const mockResult = {
intentCode: 'SUMMARIZE_DOCUMENT',
confidence: 1.0,
method: 'pattern',
latencyMs: 3,
};
vi.mocked(aiIntentService.classify).mockResolvedValue(mockResult);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({ query: 'สรุปเอกสาร' });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockResult);
expect(aiIntentService.classify).toHaveBeenCalledWith('สรุปเอกสาร', undefined);
});
it('ควรส่ง projectPublicId ไปด้วย (ถ้ามี)', async () => {
vi.mocked(aiIntentService.classify).mockResolvedValue({
intentCode: 'GET_RFA',
confidence: 0.9,
method: 'llm_fallback',
latencyMs: 500,
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({
query: 'show rfa',
projectPublicId: 'proj-uuid-123',
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(aiIntentService.classify).toHaveBeenCalledWith('show rfa', 'proj-uuid-123');
});
it('ควร handle error state', async () => {
vi.mocked(aiIntentService.classify).mockRejectedValue(new Error('Network error'));
const { wrapper } = createTestQueryClient();
const { result } = renderHook(
() => useClassifyIntent(),
{ wrapper },
);
result.current.mutate({ query: 'test' });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeInstanceOf(Error);
});
});
});
@@ -0,0 +1,133 @@
// File: hooks/ai/use-intent-classification.ts
// Change Log
// - 2026-05-19: สร้าง TanStack Query hooks สำหรับ Intent Classification (ADR-024).
// Hooks สำหรับ Intent Classification — ใช้ TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
aiIntentService,
IntentCategory,
CreateIntentDefinitionDto,
UpdateIntentDefinitionDto,
CreateIntentPatternDto,
UpdateIntentPatternDto,
ClassificationAnalytics,
} from '@/lib/services/ai-intent.service';
// === Query Keys ===
const KEYS = {
definitions: ['ai', 'intent-definitions'] as const,
definition: (code: string) => ['ai', 'intent-definitions', code] as const,
patterns: (code: string) => ['ai', 'intent-patterns', code] as const,
analytics: ['ai', 'intent-analytics'] as const,
};
// === Query Hooks ===
/** ดึง Intent Definitions ทั้งหมด */
export function useIntentDefinitions(params?: {
category?: IntentCategory;
isActive?: boolean;
}) {
return useQuery({
queryKey: [...KEYS.definitions, params],
queryFn: () => aiIntentService.getDefinitions(params),
});
}
/** ดึง Intent Definition ตาม intentCode */
export function useIntentDefinition(intentCode: string) {
return useQuery({
queryKey: KEYS.definition(intentCode),
queryFn: () => aiIntentService.getDefinition(intentCode),
enabled: !!intentCode,
});
}
/** ดึง Patterns ตาม intentCode */
export function useIntentPatterns(intentCode: string) {
return useQuery({
queryKey: KEYS.patterns(intentCode),
queryFn: () => aiIntentService.getPatterns(intentCode),
enabled: !!intentCode,
});
}
// === Mutation Hooks ===
/** สร้าง Intent Definition ใหม่ */
export function useCreateIntentDefinition() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateIntentDefinitionDto) =>
aiIntentService.createDefinition(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.definitions });
},
});
}
/** อัปเดต Intent Definition */
export function useUpdateIntentDefinition(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: UpdateIntentDefinitionDto) =>
aiIntentService.updateDefinition(intentCode, dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.definitions });
queryClient.invalidateQueries({ queryKey: KEYS.definition(intentCode) });
},
});
}
/** สร้าง Pattern ใหม่ */
export function useCreateIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateIntentPatternDto) =>
aiIntentService.createPattern(intentCode, dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** อัปเดต Pattern */
export function useUpdateIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { publicId: string; dto: UpdateIntentPatternDto }) =>
aiIntentService.updatePattern(data.publicId, data.dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** ลบ Pattern (soft delete) */
export function useDeleteIntentPattern(intentCode: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (publicId: string) => aiIntentService.deletePattern(publicId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: KEYS.patterns(intentCode) });
},
});
}
/** ดึง Classification Analytics */
export function useIntentAnalytics(params?: { from?: string; to?: string }) {
return useQuery<ClassificationAnalytics>({
queryKey: [...KEYS.analytics, params],
queryFn: () => aiIntentService.getAnalytics(params),
staleTime: 60_000, // 1 นาที cache
});
}
/** Classify query (สำหรับ Test Console) */
export function useClassifyIntent() {
return useMutation({
mutationFn: (data: { query: string; projectPublicId?: string }) =>
aiIntentService.classify(data.query, data.projectPublicId),
});
}