690619:1226 240 #03
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// File: frontend/lib/services/__tests__/admin-ai.service.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-19: เพิ่ม regression tests สำหรับ AI Admin response normalization
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { adminAiService } from '../admin-ai.service';
|
||||
import { adminAiPromptService } from '../admin-ai-prompt.service';
|
||||
|
||||
const promptVersion = {
|
||||
publicId: '0195aaaa-aaaa-7000-8000-aaaaaaaaaaaa',
|
||||
promptType: 'ocr_system',
|
||||
versionNumber: 1,
|
||||
version: 3,
|
||||
template: 'OCR prompt',
|
||||
isActive: true,
|
||||
createdAt: '2026-06-19T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('admin AI service normalization', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('ควร unwrap VRAM response ที่ถูกห่อ data ซ้อนกัน', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
totalVramMb: 16384,
|
||||
usedVramMb: 4096,
|
||||
freeVramMb: 12288,
|
||||
hasCapacity: true,
|
||||
loadedModels: [
|
||||
{
|
||||
modelId: 'np-dms-ai:latest',
|
||||
modelName: 'np-dms-ai:latest',
|
||||
vramUsageMB: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await adminAiService.getVramStatus();
|
||||
|
||||
expect(result.totalVRAMMB).toBe(16384);
|
||||
expect(result.usedVRAMMB).toBe(4096);
|
||||
expect(result.usagePercent).toBe(25);
|
||||
expect(result.canLoadModel).toBe(true);
|
||||
expect(result.loadedModels).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ควรไม่แสดง OOM Guard เมื่อ backend ยังไม่ส่ง total VRAM', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
loadedModels: [],
|
||||
hasCapacity: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await adminAiService.getVramStatus();
|
||||
|
||||
expect(result.totalVRAMMB).toBe(0);
|
||||
expect(result.usedVRAMMB).toBe(0);
|
||||
expect(result.canLoadModel).toBe(true);
|
||||
});
|
||||
|
||||
it('ควรคืน prompt list เป็น array เมื่อ response ถูกห่อ data ซ้อนกัน', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
data: [promptVersion],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await adminAiPromptService.getPrompts('ocr_system');
|
||||
|
||||
expect(result).toEqual([promptVersion]);
|
||||
});
|
||||
|
||||
it('ควรคืน empty array เมื่อ prompt payload ไม่ใช่ array', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
data: {
|
||||
message: 'unexpected payload',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await adminAiPromptService.getPrompts('ocr_system');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// File: frontend/lib/services/admin-ai-prompt.service.ts
|
||||
// Change Log
|
||||
// - 2026-06-17: Created adminAiPromptService for prompt management UI (Feature 238)
|
||||
// - 2026-06-19: Normalize prompt response envelopes to prevent non-array prompt history crashes
|
||||
|
||||
import client from '../api/client';
|
||||
|
||||
@@ -18,6 +19,23 @@ export interface AiPromptVersion {
|
||||
createdBy?: number;
|
||||
}
|
||||
|
||||
const extractData = <T>(value: unknown): T => {
|
||||
let current = value;
|
||||
for (let depth = 0; depth < 3; depth += 1) {
|
||||
if (current && typeof current === 'object' && 'data' in current) {
|
||||
current = (current as { data: unknown }).data;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return current as T;
|
||||
};
|
||||
|
||||
const normalizePromptList = (value: unknown): AiPromptVersion[] => {
|
||||
const data = extractData<unknown>(value);
|
||||
return Array.isArray(data) ? (data as AiPromptVersion[]) : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Service สำหรับจัดการ AI Prompt Versions ใน Admin Console
|
||||
*/
|
||||
@@ -26,10 +44,10 @@ export const adminAiPromptService = {
|
||||
* ดึงรายการ prompt versions ทั้งหมดสำหรับ prompt_type ที่กำหนด
|
||||
*/
|
||||
async getPrompts(promptType: string): Promise<AiPromptVersion[]> {
|
||||
const response = await client.get<{ data: AiPromptVersion[] }>(
|
||||
const response = await client.get<unknown>(
|
||||
`/ai/prompts/${promptType}`
|
||||
);
|
||||
return response.data.data;
|
||||
return normalizePromptList(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -41,7 +59,7 @@ export const adminAiPromptService = {
|
||||
contextConfig?: Record<string, unknown>
|
||||
): Promise<AiPromptVersion> {
|
||||
const idempotencyKey = crypto.randomUUID();
|
||||
const response = await client.post<{ data: AiPromptVersion }>(
|
||||
const response = await client.post<unknown>(
|
||||
`/ai/prompts/${promptType}`,
|
||||
{ template, contextConfig },
|
||||
{
|
||||
@@ -50,7 +68,7 @@ export const adminAiPromptService = {
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data.data;
|
||||
return extractData<AiPromptVersion>(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -62,7 +80,7 @@ export const adminAiPromptService = {
|
||||
expectedVersion?: number
|
||||
): Promise<AiPromptVersion> {
|
||||
const idempotencyKey = crypto.randomUUID();
|
||||
const response = await client.post<{ data: AiPromptVersion }>(
|
||||
const response = await client.post<unknown>(
|
||||
`/ai/prompts/${promptType}/${versionNumber}/activate`,
|
||||
expectedVersion !== undefined ? { expectedVersion } : {},
|
||||
{
|
||||
@@ -71,7 +89,7 @@ export const adminAiPromptService = {
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data.data;
|
||||
return extractData<AiPromptVersion>(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -91,10 +109,10 @@ export const adminAiPromptService = {
|
||||
versionNumber: number,
|
||||
manualNote: string | null
|
||||
): Promise<AiPromptVersion> {
|
||||
const response = await client.patch<{ data: AiPromptVersion }>(
|
||||
const response = await client.patch<unknown>(
|
||||
`/ai/prompts/${promptType}/${versionNumber}/note`,
|
||||
{ manualNote }
|
||||
);
|
||||
return response.data.data;
|
||||
return extractData<AiPromptVersion>(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// - 2026-06-13: T027-T029 — เพิ่ม getSandboxProfile, saveSandboxProfile, resetSandboxProfile สำหรับ sandbox parameter management
|
||||
// - 2026-06-13: T042-T043 — เพิ่ม applyProfile และ getProductionDefaults สำหรับปรับใช้และดึงค่า production parameters
|
||||
// - 2026-06-13: US4 — อัปเดต submitSandboxExtract และ submitSandboxAiExtract ให้รองรับ project/contract publicId
|
||||
// - 2026-06-19: แก้ response envelope ซ้อนกันเพื่อป้องกัน VRAM แสดง 0/0 และ OOM Guard ผิดพลาด
|
||||
|
||||
import api from '../api/client';
|
||||
import { AiJobResponse } from '../../types/ai';
|
||||
@@ -172,10 +173,15 @@ export interface ExecutionProfile {
|
||||
}
|
||||
|
||||
const extractData = <T>(value: unknown): T => {
|
||||
if (value && typeof value === 'object' && 'data' in value) {
|
||||
return (value as { data: T }).data;
|
||||
let current = value;
|
||||
for (let depth = 0; depth < 3; depth += 1) {
|
||||
if (current && typeof current === 'object' && 'data' in current) {
|
||||
current = (current as { data: unknown }).data;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return value as T;
|
||||
return current as T;
|
||||
};
|
||||
|
||||
const normalizeLoadedModels = (models: Array<string | LoadedModelInfo> | undefined): LoadedModelInfo[] => {
|
||||
@@ -199,6 +205,7 @@ const normalizeVramStatus = (value: unknown): VramStatusResponse => {
|
||||
const totalVRAMMB = raw.totalVRAMMB ?? raw.totalVramMb ?? 0;
|
||||
const usedVRAMMB = raw.usedVRAMMB ?? raw.usedVramMb ?? 0;
|
||||
const usagePercent = raw.usagePercent ?? (totalVRAMMB > 0 ? Math.round((usedVRAMMB / totalVRAMMB) * 100) : 0);
|
||||
const hasKnownCapacity = totalVRAMMB > 0;
|
||||
|
||||
// Backend now sends loadedModels with vramUsageMB directly
|
||||
const loadedModels = normalizeLoadedModels(raw.loadedModels);
|
||||
@@ -209,7 +216,7 @@ const normalizeVramStatus = (value: unknown): VramStatusResponse => {
|
||||
usagePercent,
|
||||
thresholdPercent: raw.thresholdPercent ?? 90,
|
||||
loadedModels,
|
||||
canLoadModel: raw.canLoadModel ?? raw.hasCapacity ?? false,
|
||||
canLoadModel: hasKnownCapacity ? (raw.canLoadModel ?? raw.hasCapacity ?? false) : true,
|
||||
lastUpdated: raw.lastUpdated ?? new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user