690615:1449 237 #01
CI / CD Pipeline / build (push) Failing after 3m41s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-06-15 14:49:26 +07:00
parent b46c0874f2
commit 4dde6570c1
54 changed files with 7802 additions and 727 deletions
+82
View File
@@ -0,0 +1,82 @@
// File: lib/__tests__/auth.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect, vi } from 'vitest';
import { getJwtExpiry, unwrapApiResponse, isTokenPayload } from '../auth';
// Mock NextAuth
vi.mock('next-auth', () => ({
default: vi.fn(() => ({
handlers: { GET: vi.fn(), POST: vi.fn() },
auth: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn(),
})),
}));
describe('auth.ts helper functions', () => {
describe('getJwtExpiry', () => {
it('ควรคำนวณ expiry time จาก valid JWT token', () => {
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODAwMDAwMDB9.test';
const expiry = getJwtExpiry(token);
expect(expiry).toBe(1680000000000);
});
it('ควร return Date.now() เมื่อ token ไม่ valid', () => {
const invalidToken = 'invalid.token.here';
const expiry = getJwtExpiry(invalidToken);
expect(expiry).toBeLessThanOrEqual(Date.now() + 1000);
});
});
describe('unwrapApiResponse', () => {
it('ควร return value ทันทีเมื่อไม่ใช่ object', () => {
const value = 'test string';
const result = unwrapApiResponse(value);
expect(result).toBe('test string');
});
it('ควร unwrap data เมื่อไม่มี access_token', () => {
const value = { data: { some: 'value' } };
const result = unwrapApiResponse(value);
expect(result).toEqual({ some: 'value' });
});
it('ควร return value เมื่อมี access_token', () => {
const value = { access_token: 'test_token' };
const result = unwrapApiResponse(value);
expect(result).toEqual({ access_token: 'test_token' });
});
it('ควร unwrap data ซ้อนกันสูงสุด 5 ชั้น', () => {
const value = { data: { data: { data: { data: { access_token: 'test_token' } } } } };
const result = unwrapApiResponse(value);
expect(result).toEqual({ access_token: 'test_token' });
});
});
describe('isTokenPayload', () => {
it('ควร return true เมื่อมี access_token เป็น string', () => {
const value = { access_token: 'test_token' };
expect(isTokenPayload(value)).toBe(true);
});
it('ควร return false เมื่อไม่มี access_token', () => {
const value = { some: 'value' };
expect(isTokenPayload(value)).toBe(false);
});
it('ควร return false เมื่อ access_token ไม่ใช่ string', () => {
const value = { access_token: 123 };
expect(isTokenPayload(value)).toBe(false);
});
it('ควร return false เมื่อ value เป็น null', () => {
const value = null;
expect(isTokenPayload(value)).toBe(false);
});
});
});
+123
View File
@@ -0,0 +1,123 @@
// File: lib/api/__tests__/admin.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect, vi } from 'vitest';
import { adminApi } from '../admin';
describe('adminApi', () => {
describe('getUsers', () => {
it('ควร return array of users', async () => {
const users = await adminApi.getUsers();
expect(Array.isArray(users)).toBe(true);
expect(users.length).toBeGreaterThan(0);
});
it('ควร return users ที่มี publicId, username, email', async () => {
const users = await adminApi.getUsers();
expect(users[0]).toHaveProperty('publicId');
expect(users[0]).toHaveProperty('username');
expect(users[0]).toHaveProperty('email');
});
});
describe('createUser', () => {
it('ควร create user ใหม่และ return user object', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isActive: true,
roles: [2],
};
const newUser = await adminApi.createUser(userData);
expect(newUser).toHaveProperty('publicId');
expect(newUser.username).toBe('testuser');
expect(newUser.email).toBe('test@example.com');
});
it('ควร assign userId ใหม่ให้ user', async () => {
const userData = {
username: 'newuser',
email: 'new@example.com',
firstName: 'New',
lastName: 'User',
isActive: true,
roles: [2],
};
const newUser = await adminApi.createUser(userData);
expect(newUser.userId).toBeGreaterThan(0);
});
});
describe('getOrganizations', () => {
it('ควร return array of organizations', async () => {
const orgs = await adminApi.getOrganizations();
expect(Array.isArray(orgs)).toBe(true);
expect(orgs.length).toBeGreaterThan(0);
});
it('ควร return organizations ที่มี publicId, orgCode, orgName', async () => {
const orgs = await adminApi.getOrganizations();
expect(orgs[0]).toHaveProperty('publicId');
expect(orgs[0]).toHaveProperty('orgCode');
expect(orgs[0]).toHaveProperty('orgName');
});
});
describe('createOrganization', () => {
it('ควร create organization ใหม่และ return org object', async () => {
const orgData = {
publicId: 'org-003',
orgCode: 'TEST',
orgName: 'Test Organization',
description: 'Test description',
};
const newOrg = await adminApi.createOrganization(orgData);
expect(newOrg).toHaveProperty('publicId');
expect(newOrg.orgCode).toBe('TEST');
expect(newOrg.orgName).toBe('Test Organization');
});
it('ควร assign orgId ใหม่ให้ organization', async () => {
const orgData = {
publicId: 'org-004',
orgCode: 'TEST2',
orgName: 'Test Organization 2',
description: 'Test description 2',
};
const newOrg = await adminApi.createOrganization(orgData);
expect(newOrg.orgId).toBeGreaterThan(0);
});
});
describe('getAuditLogs', () => {
it('ควร return array of audit logs', async () => {
const logs = await adminApi.getAuditLogs();
expect(Array.isArray(logs)).toBe(true);
expect(logs.length).toBeGreaterThan(0);
});
it('ควร return logs ที่มี publicId, userName, action', async () => {
const logs = await adminApi.getAuditLogs();
expect(logs[0]).toHaveProperty('publicId');
expect(logs[0]).toHaveProperty('userName');
expect(logs[0]).toHaveProperty('action');
});
});
});
+34
View File
@@ -0,0 +1,34 @@
// File: lib/api/__tests__/ai.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect } from 'vitest';
import { extractData } from '../ai';
describe('ai.ts helper functions', () => {
describe('extractData', () => {
it('ควร return value ทันทีเมื่อไม่ใช่ object', () => {
const value = 'test string';
const result = extractData(value);
expect(result).toBe('test string');
});
it('ควร return value ทันทีเมื่อไม่มี data property', () => {
const value = { some: 'value' };
const result = extractData(value);
expect(result).toEqual({ some: 'value' });
});
it('ควร unwrap data เมื่อมี data property', () => {
const value = { data: { some: 'value' } };
const result = extractData(value);
expect(result).toEqual({ some: 'value' });
});
it('ควร unwrap data ซ้อนกันสูงสุด 5 ชั้น', () => {
const value = { data: { data: { data: { data: { data: 'final' } } } } };
const result = extractData(value);
expect(result).toBe('final');
});
});
});
@@ -0,0 +1,79 @@
// File: lib/api/__tests__/dashboard.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect } from 'vitest';
import { dashboardApi } from '../dashboard';
describe('dashboardApi', () => {
describe('getStats', () => {
it('ควร return dashboard stats', async () => {
const stats = await dashboardApi.getStats();
expect(stats).toHaveProperty('totalDocuments');
expect(stats).toHaveProperty('documentsThisMonth');
expect(stats).toHaveProperty('pendingApprovals');
expect(stats).toHaveProperty('approved');
expect(stats).toHaveProperty('totalRfas');
expect(stats).toHaveProperty('totalCirculations');
});
it('ควร return numbers สำหรับ stats', async () => {
const stats = await dashboardApi.getStats();
expect(typeof stats.totalDocuments).toBe('number');
expect(typeof stats.documentsThisMonth).toBe('number');
expect(typeof stats.pendingApprovals).toBe('number');
});
});
describe('getRecentActivity', () => {
it('ควร return array of activity logs', async () => {
const activities = await dashboardApi.getRecentActivity();
expect(Array.isArray(activities)).toBe(true);
expect(activities.length).toBeGreaterThan(0);
});
it('ควร return activities ที่มี id, user, action, description', async () => {
const activities = await dashboardApi.getRecentActivity();
expect(activities[0]).toHaveProperty('id');
expect(activities[0]).toHaveProperty('user');
expect(activities[0]).toHaveProperty('action');
expect(activities[0]).toHaveProperty('description');
});
it('ควร return activities ที่มี user.name และ user.initials', async () => {
const activities = await dashboardApi.getRecentActivity();
expect(activities[0].user).toHaveProperty('name');
expect(activities[0].user).toHaveProperty('initials');
});
});
describe('getPendingTasks', () => {
it('ควร return array of pending tasks', async () => {
const tasks = await dashboardApi.getPendingTasks();
expect(Array.isArray(tasks)).toBe(true);
expect(tasks.length).toBeGreaterThan(0);
});
it('ควร return tasks ที่มี publicId, workflowCode, currentState', async () => {
const tasks = await dashboardApi.getPendingTasks();
expect(tasks[0]).toHaveProperty('publicId');
expect(tasks[0]).toHaveProperty('workflowCode');
expect(tasks[0]).toHaveProperty('currentState');
});
it('ควร return tasks ที่มี entityType, documentNumber, subject', async () => {
const tasks = await dashboardApi.getPendingTasks();
expect(tasks[0]).toHaveProperty('entityType');
expect(tasks[0]).toHaveProperty('documentNumber');
expect(tasks[0]).toHaveProperty('subject');
});
});
});
@@ -0,0 +1,64 @@
// File: lib/api/__tests__/drawings.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect } from 'vitest';
import { drawingApi } from '../drawings';
describe('drawingApi', () => {
describe('getAll', () => {
it('ควร return array of drawings พร้อม meta', async () => {
const result = await drawingApi.getAll();
expect(result).toHaveProperty('data');
expect(result).toHaveProperty('meta');
expect(Array.isArray(result.data)).toBe(true);
});
it('ควร return drawings ที่มี publicId, drawingNumber, title', async () => {
const result = await drawingApi.getAll();
expect(result.data[0]).toHaveProperty('publicId');
expect(result.data[0]).toHaveProperty('drawingNumber');
expect(result.data[0]).toHaveProperty('title');
});
it('ควร return meta.total เท่ากับจำนวน drawings', async () => {
const result = await drawingApi.getAll();
expect(result.meta.total).toBe(result.data.length);
});
});
describe('getById', () => {
it('ควร return drawing เมื่อ id ถูกต้อง', async () => {
const drawing = await drawingApi.getById('dwg-001');
expect(drawing).toBeDefined();
expect(drawing?.publicId).toBe('dwg-001');
});
it('ควร return undefined เมื่อ id ไม่ถูกต้อง', async () => {
const drawing = await drawingApi.getById('non-existent');
expect(drawing).toBeUndefined();
});
});
describe('getByContract', () => {
it('ควร return array of drawings สำหรับ contract', async () => {
const result = await drawingApi.getByContract('contract-001');
expect(result).toHaveProperty('data');
expect(Array.isArray(result.data)).toBe(true);
});
it('ควร return drawings ที่มี discipline, status, revision', async () => {
const result = await drawingApi.getByContract('contract-001');
expect(result.data[0]).toHaveProperty('discipline');
expect(result.data[0]).toHaveProperty('status');
expect(result.data[0]).toHaveProperty('revision');
});
});
});
@@ -0,0 +1,66 @@
// File: lib/api/__tests__/notifications.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect, beforeEach } from 'vitest';
import { notificationApi } from '../notifications';
describe('notificationApi', () => {
beforeEach(() => {
// Reset mock data before each test
// Note: This is a simplified reset since the mock is in the same file
});
describe('getUnread', () => {
it('ควร return notifications พร้อม unreadCount', async () => {
const result = await notificationApi.getUnread();
expect(result).toHaveProperty('items');
expect(result).toHaveProperty('unreadCount');
expect(Array.isArray(result.items)).toBe(true);
});
it('ควร return notifications ที่มี publicId, title, message', async () => {
const result = await notificationApi.getUnread();
expect(result.items[0]).toHaveProperty('publicId');
expect(result.items[0]).toHaveProperty('title');
expect(result.items[0]).toHaveProperty('message');
});
it('ควร return notifications ที่มี type, isRead, createdAt', async () => {
const result = await notificationApi.getUnread();
expect(result.items[0]).toHaveProperty('type');
expect(result.items[0]).toHaveProperty('isRead');
expect(result.items[0]).toHaveProperty('createdAt');
});
it('ควร count unread notifications อย่างถูกต้อง', async () => {
const result = await notificationApi.getUnread();
expect(typeof result.unreadCount).toBe('number');
expect(result.unreadCount).toBeGreaterThanOrEqual(0);
});
});
describe('markAsRead', () => {
it('ควร mark notification เป็น read', async () => {
await notificationApi.markAsRead(1);
const result = await notificationApi.getUnread();
const notification = result.items.find((n) => n.notificationId === 1);
expect(notification?.isRead).toBe(true);
});
it('ควรไม่ affect notifications อื่น', async () => {
await notificationApi.markAsRead(1);
const result = await notificationApi.getUnread();
const otherNotification = result.items.find((n) => n.notificationId === 2);
expect(otherNotification?.isRead).toBe(false);
});
});
});
@@ -0,0 +1,232 @@
// File: lib/api/__tests__/numbering.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect, vi } from 'vitest';
import { numberingApi } from '../numbering';
// Mock apiClient
vi.mock('@/lib/api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
import apiClient from '@/lib/api/client';
describe('numberingApi', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getTemplates', () => {
it('ควร return array of templates', async () => {
const mockTemplates = [{ id: 1, formatTemplate: 'TEST-{YYYY}-{NNNN}' }];
(apiClient.get as any).mockResolvedValue({ data: mockTemplates });
const result = await numberingApi.getTemplates();
expect(Array.isArray(result)).toBe(true);
expect(result).toEqual(mockTemplates);
});
it('ควร handle nested data structure', async () => {
const mockTemplates = [{ id: 1, formatTemplate: 'TEST-{YYYY}-{NNNN}' }];
(apiClient.get as any).mockResolvedValue({ data: { data: mockTemplates } });
const result = await numberingApi.getTemplates();
expect(Array.isArray(result)).toBe(true);
expect(result).toEqual(mockTemplates);
});
});
describe('getTemplatesByProject', () => {
it('ควร call API ด้วย projectId parameter', async () => {
(apiClient.get as any).mockResolvedValue({ data: [] });
await numberingApi.getTemplatesByProject(1);
expect(apiClient.get).toHaveBeenCalledWith('/admin/document-numbering/templates?projectId=1');
});
});
describe('getTemplate', () => {
it('ควร return template เมื่อ id ถูกต้อง', async () => {
const mockTemplates = [{ id: 1, formatTemplate: 'TEST-{YYYY}-{NNNN}' }];
(apiClient.get as any).mockResolvedValue({ data: mockTemplates });
const result = await numberingApi.getTemplate(1);
expect(result).toEqual(mockTemplates[0]);
});
it('ควร return undefined เมื่อ id ไม่พบ', async () => {
const mockTemplates = [{ id: 1, formatTemplate: 'TEST-{YYYY}-{NNNN}' }];
(apiClient.get as any).mockResolvedValue({ data: mockTemplates });
const result = await numberingApi.getTemplate(999);
expect(result).toBeUndefined();
});
});
describe('saveTemplate', () => {
it('ควร call API ด้วย DTO ที่ clean แล้ว', async () => {
const mockTemplate = { id: 1, formatTemplate: 'TEST-{YYYY}-{NNNN}' };
(apiClient.post as any).mockResolvedValue({ data: mockTemplate });
const dto = {
projectId: 1,
correspondenceTypeId: null,
formatTemplate: 'TEST-{YYYY}-{NNNN}',
};
const result = await numberingApi.saveTemplate(dto);
expect(apiClient.post).toHaveBeenCalledWith('/admin/document-numbering/templates', expect.any(Object));
expect(result).toEqual(mockTemplate);
});
});
describe('deleteTemplate', () => {
it('ควร call API ด้วย id', async () => {
(apiClient.delete as any).mockResolvedValue({});
await numberingApi.deleteTemplate(1);
expect(apiClient.delete).toHaveBeenCalledWith('/admin/document-numbering/templates/1');
});
});
describe('getAuditLogs', () => {
it('ควร return array of audit logs', async () => {
const mockLogs = [{ id: 1, generatedNumber: 'TEST-001' }];
(apiClient.get as any).mockResolvedValue({ data: mockLogs });
const result = await numberingApi.getAuditLogs();
expect(Array.isArray(result)).toBe(true);
expect(result).toEqual(mockLogs);
});
it('ควร call API ด้วย limit parameter', async () => {
(apiClient.get as any).mockResolvedValue({ data: [] });
await numberingApi.getAuditLogs(50);
expect(apiClient.get).toHaveBeenCalledWith('/document-numbering/logs/audit?limit=50');
});
});
describe('getErrorLogs', () => {
it('ควร return array of error logs', async () => {
const mockErrors = [{ id: 1, errorMessage: 'Test error' }];
(apiClient.get as any).mockResolvedValue({ data: mockErrors });
const result = await numberingApi.getErrorLogs();
expect(Array.isArray(result)).toBe(true);
expect(result).toEqual(mockErrors);
});
});
describe('getMetrics', () => {
it('ควร return metrics ที่มี audit และ errors', async () => {
const mockMetrics = { audit: [], errors: [] };
(apiClient.get as any).mockResolvedValue({ data: mockMetrics });
const result = await numberingApi.getMetrics();
expect(result).toHaveProperty('audit');
expect(result).toHaveProperty('errors');
});
});
describe('manualOverride', () => {
it('ควร call API ด้วย DTO', async () => {
const mockResponse = { success: true, message: 'Override successful' };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const dto = { projectId: 1, correspondenceTypeId: null, year: 2026, newValue: 100 };
const result = await numberingApi.manualOverride(dto);
expect(apiClient.post).toHaveBeenCalledWith('/admin/document-numbering/manual-override', dto);
expect(result).toEqual(mockResponse);
});
});
describe('voidAndReplace', () => {
it('ควร call API ด้วย DTO', async () => {
const mockResponse = { newNumber: 'TEST-002', auditId: 123 };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const dto = { documentId: 1, reason: 'Test reason' };
const result = await numberingApi.voidAndReplace(dto);
expect(apiClient.post).toHaveBeenCalledWith('/admin/document-numbering/void-and-replace', dto);
expect(result).toEqual(mockResponse);
});
});
describe('cancelNumber', () => {
it('ควร call API ด้วย DTO', async () => {
const mockResponse = { success: true };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const dto = { documentNumber: 'TEST-001', reason: 'Test reason' };
const result = await numberingApi.cancelNumber(dto);
expect(apiClient.post).toHaveBeenCalledWith('/admin/document-numbering/cancel', dto);
expect(result).toEqual(mockResponse);
});
});
describe('bulkImport', () => {
it('ควร call API ด้วย items array', async () => {
const mockResponse = { imported: 10, errors: [] };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const items = [{ projectId: 1, correspondenceTypeId: null, year: 2026, lastNumber: 100 }];
const result = await numberingApi.bulkImport(items);
expect(apiClient.post).toHaveBeenCalledWith('/admin/document-numbering/bulk-import', items);
expect(result).toEqual(mockResponse);
});
});
describe('updateCounter', () => {
it('ควร call API ด้วย counterId และ sequence', async () => {
(apiClient.patch as any).mockResolvedValue({});
await numberingApi.updateCounter(1, 100);
expect(apiClient.patch).toHaveBeenCalledWith('/document-numbering/counters/1', { sequence: 100 });
});
});
describe('previewNumber', () => {
it('ควร return preview number', async () => {
const mockResponse = { previewNumber: 'TEST-2026-0001', nextSequence: 1, isDefault: true };
(apiClient.post as any).mockResolvedValue({ data: mockResponse });
const ctx = { projectId: 1, originatorOrganizationId: 1, correspondenceTypeId: 1 };
const result = await numberingApi.previewNumber(ctx);
expect(apiClient.post).toHaveBeenCalledWith('/document-numbering/preview', ctx);
expect(result).toEqual(mockResponse);
});
});
describe('generateTestNumber', () => {
it('ควร return mock test number', async () => {
const result = await numberingApi.generateTestNumber(1, { organizationId: '1', disciplineId: '1' });
expect(result).toHaveProperty('number');
expect(result.number).toMatch(/^TEST-\d{4}-\d{4}$/);
});
});
});
@@ -0,0 +1,133 @@
// File: lib/api/__tests__/workflows.test.ts
// Change Log:
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
import { describe, it, expect, beforeEach } from 'vitest';
import { workflowApi } from '../workflows';
describe('workflowApi', () => {
beforeEach(() => {
// Reset mock data before each test
// Note: This is a simplified reset since the mock is in the same file
});
describe('getWorkflows', () => {
it('ควร return array of workflows', async () => {
const workflows = await workflowApi.getWorkflows();
expect(Array.isArray(workflows)).toBe(true);
expect(workflows.length).toBeGreaterThan(0);
});
it('ควร return workflows ที่มี publicId, workflowName, workflowType', async () => {
const workflows = await workflowApi.getWorkflows();
expect(workflows[0]).toHaveProperty('publicId');
expect(workflows[0]).toHaveProperty('workflowName');
expect(workflows[0]).toHaveProperty('workflowType');
});
it('ควร return workflows ที่มี dslDefinition, version, isActive', async () => {
const workflows = await workflowApi.getWorkflows();
expect(workflows[0]).toHaveProperty('dslDefinition');
expect(workflows[0]).toHaveProperty('version');
expect(workflows[0]).toHaveProperty('isActive');
});
});
describe('getWorkflow', () => {
it('ควร return workflow เมื่อ id ถูกต้อง', async () => {
const workflow = await workflowApi.getWorkflow('wf-001');
expect(workflow).toBeDefined();
expect(workflow?.publicId).toBe('wf-001');
});
it('ควร return undefined เมื่อ id ไม่ถูกต้อง', async () => {
const workflow = await workflowApi.getWorkflow('non-existent');
expect(workflow).toBeUndefined();
});
});
describe('createWorkflow', () => {
it('ควร create workflow ใหม่และ return workflow object', async () => {
const data = {
workflowName: 'Test Workflow',
description: 'Test description',
workflowType: 'RFA',
dslDefinition: 'name: Test\nsteps: []',
};
const newWorkflow = await workflowApi.createWorkflow(data);
expect(newWorkflow).toHaveProperty('publicId');
expect(newWorkflow.workflowName).toBe('Test Workflow');
expect(newWorkflow.version).toBe(1);
expect(newWorkflow.isActive).toBe(true);
});
it('ควร assign workflowId ใหม่ให้ workflow', async () => {
const data = {
workflowName: 'New Workflow',
description: 'New description',
workflowType: 'CORRESPONDENCE',
dslDefinition: 'name: New\nsteps: []',
};
const newWorkflow = await workflowApi.createWorkflow(data);
expect(newWorkflow.workflowId).toBeGreaterThan(0);
});
});
describe('updateWorkflow', () => {
it('ควร update workflow และ return updated object', async () => {
const data = {
workflowName: 'Updated Workflow',
description: 'Updated description',
};
const updatedWorkflow = await workflowApi.updateWorkflow('wf-001', data);
expect(updatedWorkflow.workflowName).toBe('Updated Workflow');
expect(updatedWorkflow.description).toBe('Updated description');
});
it('ควร throw error เมื่อ workflow ไม่พบ', async () => {
const data = { workflowName: 'Test' };
await expect(workflowApi.updateWorkflow('non-existent', data)).rejects.toThrow('Workflow not found');
});
});
describe('validateDSL', () => {
it('ควร return valid=true เมื่อ DSL ถูกต้อง', async () => {
const dsl = 'name: Test Workflow\nsteps:\n - name: Step 1\n type: REVIEW';
const result = await workflowApi.validateDSL(dsl);
expect(result.valid).toBe(true);
expect(result.errors).toEqual([]);
});
it('ควร return valid=false เมื่อ DSL ไม่มี name', async () => {
const dsl = 'invalid dsl without name or steps';
const result = await workflowApi.validateDSL(dsl);
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
it('ควร return valid=false เมื่อ DSL ไม่มี steps', async () => {
const dsl = 'name: Test Workflow';
const result = await workflowApi.validateDSL(dsl);
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
});
+1 -1
View File
@@ -50,7 +50,7 @@ interface WrappedData<T> {
data?: T;
}
const extractData = <T>(value: unknown): T => {
export const extractData = <T>(value: unknown): T => {
let current: unknown = value;
for (let index = 0; index < 5; index += 1) {
if (!current || typeof current !== 'object' || !('data' in current)) {
+3 -3
View File
@@ -17,7 +17,7 @@ const baseUrl =
'http://localhost:3001/api';
// Helper to parse JWT expiry
function getJwtExpiry(token: string): number {
export function getJwtExpiry(token: string): number {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000; // Convert to ms
@@ -44,7 +44,7 @@ interface LoginPayload extends TokenPayload {
};
}
function unwrapApiResponse(value: unknown): unknown {
export function unwrapApiResponse(value: unknown): unknown {
let current = value;
for (let i = 0; i < 5; i += 1) {
@@ -67,7 +67,7 @@ function unwrapApiResponse(value: unknown): unknown {
return current;
}
function isTokenPayload(value: unknown): value is TokenPayload {
export function isTokenPayload(value: unknown): value is TokenPayload {
return !!value && typeof value === 'object' && typeof (value as Record<string, unknown>).access_token === 'string';
}
+70 -33
View File
@@ -18,7 +18,6 @@
// - 2026-06-13: T042-T043 — เพิ่ม applyProfile และ getProductionDefaults สำหรับปรับใช้และดึงค่า production parameters
// - 2026-06-13: US4 — อัปเดต submitSandboxExtract และ submitSandboxAiExtract ให้รองรับ project/contract publicId
import api from '../api/client';
import { AiJobResponse } from '../../types/ai';
import { PromptType, PromptVersion, ContextConfig } from '../types/ai-prompts';
@@ -155,6 +154,21 @@ export interface SandboxProfileParams {
keepAliveSeconds: number;
}
export interface ExecutionProfile {
id: number;
profileName: string;
canonicalModel?: 'np-dms-ai' | 'np-dms-ocr';
temperature: number;
topP: number;
repeatPenalty: number;
maxTokens: number | null;
numCtx: number | null;
keepAlive: number;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
const extractData = <T>(value: unknown): T => {
if (value && typeof value === 'object' && 'data' in value) {
return (value as { data: T }).data;
@@ -162,9 +176,7 @@ const extractData = <T>(value: unknown): T => {
return value as T;
};
const normalizeLoadedModels = (
models: Array<string | LoadedModelInfo> | undefined
): LoadedModelInfo[] => {
const normalizeLoadedModels = (models: Array<string | LoadedModelInfo> | undefined): LoadedModelInfo[] => {
if (!Array.isArray(models)) {
return [];
}
@@ -184,9 +196,7 @@ const normalizeVramStatus = (value: unknown): VramStatusResponse => {
const raw = extractData<RawVramStatusResponse>(value);
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 usagePercent = raw.usagePercent ?? (totalVRAMMB > 0 ? Math.round((usedVRAMMB / totalVRAMMB) * 100) : 0);
return {
totalVRAMMB,
@@ -199,6 +209,10 @@ const normalizeVramStatus = (value: unknown): VramStatusResponse => {
};
};
const createIdempotencyKey = (): string => {
return globalThis.crypto?.randomUUID?.() ?? `idem-${Date.now()}`;
};
/** Service สำหรับเรียก AI Admin Console API ผ่าน DMS Backend เท่านั้น */
export const adminAiService = {
getStatus: async (): Promise<AiAdminSettings> => {
@@ -356,26 +370,18 @@ export const adminAiService = {
updates: Partial<SandboxProfileParams>,
idempotencyKey: string
): Promise<SandboxProfileParams> => {
const { data } = await api.put(
`/ai/sandbox-profiles/${encodeURIComponent(profileName)}`,
updates,
{ headers: { 'Idempotency-Key': idempotencyKey } }
);
const { data } = await api.put(`/ai/sandbox-profiles/${encodeURIComponent(profileName)}`, updates, {
headers: { 'Idempotency-Key': idempotencyKey },
});
return extractData<SandboxProfileParams>(data);
},
resetSandboxProfile: async (profileName: string): Promise<SandboxProfileParams> => {
const { data } = await api.post(
`/ai/sandbox-profiles/${encodeURIComponent(profileName)}/reset`,
{}
);
const { data } = await api.post(`/ai/sandbox-profiles/${encodeURIComponent(profileName)}/reset`, {});
return extractData<SandboxProfileParams>(data);
},
applyProfile: async (
profileName: string,
idempotencyKey: string
): Promise<SandboxProfileParams> => {
applyProfile: async (profileName: string, idempotencyKey: string): Promise<SandboxProfileParams> => {
const { data } = await api.post(
`/ai/profiles/${encodeURIComponent(profileName)}/apply`,
{},
@@ -415,7 +421,9 @@ export const adminAiService = {
type: PromptType,
updates: { template: string; contextConfig?: ContextConfig | null; manualNote?: string }
): Promise<PromptVersion> => {
const { data } = await api.post(`/ai/prompts/${type}`, updates);
const { data } = await api.post(`/ai/prompts/${type}`, updates, {
headers: { 'Idempotency-Key': createIdempotencyKey() },
});
return extractData<PromptVersion>(data);
},
@@ -424,15 +432,15 @@ export const adminAiService = {
},
activatePrompt: async (type: PromptType, versionNumber: number): Promise<PromptVersion> => {
const { data } = await api.post(`/ai/prompts/${type}/${versionNumber}/activate`);
const { data } = await api.post(
`/ai/prompts/${type}/${versionNumber}/activate`,
{},
{ headers: { 'Idempotency-Key': createIdempotencyKey() } }
);
return extractData<PromptVersion>(data);
},
updatePromptNote: async (
type: PromptType,
versionNumber: number,
manualNote: string
): Promise<PromptVersion> => {
updatePromptNote: async (type: PromptType, versionNumber: number, manualNote: string): Promise<PromptVersion> => {
const { data } = await api.patch(`/ai/prompts/${type}/${versionNumber}/note`, { manualNote });
return extractData<PromptVersion>(data);
},
@@ -447,17 +455,46 @@ export const adminAiService = {
versionNumber: number,
contextConfig: ContextConfig
): Promise<ContextConfig> => {
const { data } = await api.put(`/ai/prompts/${type}/${versionNumber}/context-config`, contextConfig);
const { data } = await api.put(`/ai/prompts/${type}/${versionNumber}/context-config`, contextConfig, {
headers: { 'Idempotency-Key': createIdempotencyKey() },
});
return extractData<ContextConfig>(data);
},
submitSandboxRagPrep: async (
text: string,
profileId?: string | null
): Promise<{ jobId: string; status: string }> => {
const { data } = await api.post('/ai/admin/sandbox/rag-prep', { text, profileId });
submitSandboxRagPrep: async (text: string, profileId?: string | null): Promise<{ jobId: string; status: string }> => {
const { data } = await api.post(
'/ai/admin/sandbox/rag-prep',
{ text, profileId },
{ headers: { 'Idempotency-Key': createIdempotencyKey() } }
);
return extractData<{ jobId: string; status: string }>(data);
},
// --- Execution Profiles (US4 — T051) ---
getExecutionProfiles: async (): Promise<ExecutionProfile[]> => {
const { data } = await api.get('/ai/execution-profiles');
return extractData<ExecutionProfile[]>(data);
},
createExecutionProfile: async (
profile: Omit<ExecutionProfile, 'id' | 'isActive' | 'createdAt' | 'updatedAt'>
): Promise<ExecutionProfile> => {
const { data } = await api.post('/ai/execution-profiles', profile);
return extractData<ExecutionProfile>(data);
},
updateExecutionProfile: async (
id: number,
updates: Partial<Omit<ExecutionProfile, 'id' | 'isActive' | 'createdAt' | 'updatedAt'>>
): Promise<ExecutionProfile> => {
const { data } = await api.put(`/ai/execution-profiles/${id}`, updates);
return extractData<ExecutionProfile>(data);
},
deleteExecutionProfile: async (id: number): Promise<void> => {
await api.delete(`/ai/execution-profiles/${id}`);
},
};
export interface OcrEngineResponse {