feat(ai): implement unified prompt management UX/UI (ADR-037)
CI / CD Pipeline / build (push) Failing after 3m23s
CI / CD Pipeline / deploy (push) Has been skipped

- Add context config endpoints (GET/PUT /api/ai/prompts/:type/:version/context-config)
- Add execution profile endpoints (CRUD /api/ai/execution-profiles)
- Add sandbox RAG Prep endpoint (POST /api/ai/admin/sandbox/rag-prep)
- Create Prompt Management UI with multi-type support
- Add ContextConfigEditor, PromptEditor, RuntimeParametersPanel components
- Add SandboxTabs for 3-step workflow (OCR, Extract, RAG Prep)
- Add database deltas for ai_execution_profiles and additional prompt types
- Update quickstart.md with production backend URLs
- Add comprehensive test coverage for new features
This commit is contained in:
2026-06-14 19:55:43 +07:00
parent 56f9544cb0
commit 67da186672
64 changed files with 6327 additions and 6107 deletions
@@ -3,6 +3,7 @@
// - 2026-06-03: สร้าง unit test สำหรับ OllamaService ครอบคลุม generate() model option,
// getOcrModelName(), และ loadModel() keepAlive param ตาม ADR-034
// - 2026-06-13: ADR-036 — อัปเดต expected model tags เป็น np-dms-ai/np-dms-ocr
// - 2026-06-14: เพิ่ม tests สำหรับ generateEmbedding, checkHealth, unloadModel เพื่อเพิ่ม branch coverage
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
@@ -132,5 +133,125 @@ describe('OllamaService (ADR-034)', () => {
expect(result).toBe(false);
expect(mockedAxios.post).not.toHaveBeenCalled();
});
it('ควรคืน false และ log error เมื่อ axios throw ระหว่าง loadModel', async () => {
mockedAxios.get = jest
.fn()
.mockRejectedValueOnce(new Error('ECONNREFUSED'));
const result = await service.loadModel('np-dms-ai:latest');
expect(result).toBe(false);
});
});
describe('getEmbeddingModelName()', () => {
it('ควรคืน nomic-embed-text เป็น embedding model', () => {
expect(service.getEmbeddingModelName()).toBe('nomic-embed-text');
});
});
describe('generateEmbedding()', () => {
it('ควรคืน embedding vector เมื่อ Ollama ตอบกลับสำเร็จ', async () => {
const mockVector = [0.1, 0.2, 0.3];
mockedAxios.post = jest.fn().mockResolvedValueOnce({
data: { embedding: mockVector },
});
const result = await service.generateEmbedding('test text');
expect(result).toEqual(mockVector);
expect(mockedAxios.post).toHaveBeenCalledWith(
expect.stringContaining('/api/embeddings'),
expect.objectContaining({
model: 'nomic-embed-text',
prompt: 'test text',
}),
expect.anything()
);
});
it('ควร throw error เมื่อ Ollama embedding ล้มเหลว', async () => {
mockedAxios.post = jest
.fn()
.mockRejectedValueOnce(new Error('Embedding failed'));
await expect(service.generateEmbedding('test')).rejects.toThrow(
'Embedding failed'
);
});
});
describe('checkHealth()', () => {
it('ควรคืน HEALTHY พร้อมโมเดลที่โหลดอยู่จาก /api/ps เมื่อ Ollama ตอบกลับสำเร็จ', async () => {
mockedAxios.get = jest
.fn()
.mockResolvedValueOnce({ data: {} }) // /api/tags
.mockResolvedValueOnce({
data: { models: [{ name: 'np-dms-ai:latest' }] },
}); // /api/ps
const result = await service.checkHealth();
expect(result.status).toBe('HEALTHY');
expect(result.models).toContain('np-dms-ai:latest');
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
});
it('ควรคืน HEALTHY พร้อม fallback models เมื่อ /api/ps ไม่มีข้อมูล', async () => {
mockedAxios.get = jest
.fn()
.mockResolvedValueOnce({ data: {} }) // /api/tags OK
.mockResolvedValueOnce({ data: { models: [] } }); // /api/ps empty
const result = await service.checkHealth();
expect(result.status).toBe('HEALTHY');
expect(result.models).toContain('np-dms-ai:latest'); // fallback
});
it('ควรคืน HEALTHY แม้ /api/ps throw error (graceful degradation)', async () => {
mockedAxios.get = jest
.fn()
.mockResolvedValueOnce({ data: {} }) // /api/tags OK
.mockRejectedValueOnce(new Error('ps endpoint error')); // /api/ps fails
const result = await service.checkHealth();
expect(result.status).toBe('HEALTHY');
});
it('ควรคืน DEGRADED เมื่อ /api/tags timeout', async () => {
mockedAxios.get = jest
.fn()
.mockRejectedValueOnce(new Error('timeout error'));
const result = await service.checkHealth();
expect(result.status).toBe('DEGRADED');
expect(result.error).toContain('timeout');
});
it('ควรคืน DEGRADED เมื่อ error message มี code ECONNABORTED', async () => {
mockedAxios.get = jest
.fn()
.mockRejectedValueOnce(new Error('code ECONNABORTED'));
const result = await service.checkHealth();
expect(result.status).toBe('DEGRADED');
});
it('ควรคืน DOWN เมื่อ connection ถูกปฏิเสธ (ไม่ใช่ timeout)', async () => {
mockedAxios.get = jest
.fn()
.mockRejectedValueOnce(new Error('ECONNREFUSED'));
const result = await service.checkHealth();
expect(result.status).toBe('DOWN');
});
});
describe('unloadModel()', () => {
it('ควรคืน true เมื่อ unload สำเร็จ', async () => {
mockedAxios.post = jest.fn().mockResolvedValueOnce({ data: {} });
const result = await service.unloadModel('np-dms-ocr:latest');
expect(result).toBe(true);
expect(mockedAxios.post).toHaveBeenCalledWith(
expect.stringContaining('/api/generate'),
expect.objectContaining({ model: 'np-dms-ocr:latest', keep_alive: 0 }),
expect.anything()
);
});
it('ควรคืน false เมื่อ unload ล้มเหลว', async () => {
mockedAxios.post = jest
.fn()
.mockRejectedValueOnce(new Error('Unload failed'));
const result = await service.unloadModel('np-dms-ocr:latest');
expect(result).toBe(false);
});
});
describe('generate() error path', () => {
it('ควร throw error เมื่อ Ollama generate ล้มเหลว', async () => {
mockedAxios.post = jest
.fn()
.mockRejectedValueOnce(new Error('LLM timeout'));
await expect(service.generate('test prompt')).rejects.toThrow(
'LLM timeout'
);
});
});
});