feat(ai): add ADR-036 unified OCR architecture and frontend test coverage
CI / CD Pipeline / build (push) Failing after 6m24s
CI / CD Pipeline / deploy (push) Has been skipped

- Add ADR-036 unified OCR architecture (typhoon-ocr via Ollama)
- Extend AI execution profiles for OCR sandbox configuration
- Add comprehensive frontend test coverage (components, hooks, services)
- Add backend test coverage for document-numbering services
- Update OCR sidecar with typhoon-ocr integration
- Add AI policy service and execution profile management
- Update AGENTS.md and architecture documentation
This commit is contained in:
2026-06-14 06:34:07 +07:00
parent e3503b6a77
commit 7e8f4859cd
108 changed files with 33914 additions and 339 deletions
@@ -0,0 +1,125 @@
// File: frontend/components/transmittal/__tests__/transmittal-form.test.tsx
// Change Log
// - 2026-06-13: Add coverage for transmittal form render, cancel, validation, and submit flows.
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { toast } from 'sonner';
import { createTestQueryClient } from '@/lib/test-utils';
import { TransmittalForm } from '../transmittal-form';
import { transmittalService } from '@/lib/services/transmittal.service';
import { correspondenceService } from '@/lib/services/correspondence.service';
import { projectService } from '@/lib/services/project.service';
import { organizationService } from '@/lib/services/organization.service';
const push = vi.fn();
const back = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({ push, back }),
}));
vi.mock('@/lib/services/transmittal.service', () => ({
transmittalService: {
create: vi.fn(),
},
}));
vi.mock('@/lib/services/correspondence.service', () => ({
correspondenceService: {
getAll: vi.fn(),
},
}));
vi.mock('@/lib/services/project.service', () => ({
projectService: {
getAll: vi.fn(),
},
}));
vi.mock('@/lib/services/organization.service', () => ({
organizationService: {
getAll: vi.fn(),
},
}));
function renderForm() {
const { wrapper } = createTestQueryClient();
return render(<TransmittalForm />, { wrapper });
}
async function chooseCombobox(label: string | RegExp, option: string): Promise<void> {
const user = userEvent.setup();
await user.click(screen.getByRole('combobox', { name: label }));
const matches = await screen.findAllByText(option);
await user.click(matches[matches.length - 1]);
}
describe('TransmittalForm', () => {
beforeEach(() => {
vi.clearAllMocks();
Element.prototype.scrollIntoView = vi.fn();
vi.mocked(projectService.getAll).mockResolvedValue({
data: [{ publicId: '019505a1-7c3e-7000-8000-abc123defc01', projectName: 'LCBP3' }],
});
vi.mocked(organizationService.getAll).mockResolvedValue({
data: [{ uuid: '019505a1-7c3e-7000-8000-abc123defc02', organizationName: 'TEAM Consulting' }],
});
vi.mocked(correspondenceService.getAll).mockResolvedValue({
data: [{ uuid: '019505a1-7c3e-7000-8000-abc123defc03', correspondenceNumber: 'COR-001' }],
});
vi.mocked(transmittalService.create).mockResolvedValue({
uuid: '019505a1-7c3e-7000-8000-abc123defc04',
correspondence: { uuid: '019505a1-7c3e-7000-8000-abc123defc05' },
});
});
it('renders main sections and supports cancel navigation', async () => {
const user = userEvent.setup();
renderForm();
expect(await screen.findByText('Transmittal Details')).toBeInTheDocument();
expect(screen.getByText('Transmittal Items')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(back).toHaveBeenCalled();
});
it('shows validation errors when required fields are missing', async () => {
const user = userEvent.setup();
renderForm();
await user.click(await screen.findByRole('button', { name: 'Create Transmittal' }));
expect(await screen.findByText('Project is required')).toBeInTheDocument();
expect(screen.getByText('Recipient is required')).toBeInTheDocument();
expect(screen.getByText('Correspondence is required')).toBeInTheDocument();
expect(screen.getByText('Subject is required')).toBeInTheDocument();
});
it('submits cleaned transmittal payload and navigates to created record', async () => {
const user = userEvent.setup();
renderForm();
await screen.findByText('Transmittal Details');
await chooseCombobox(/project/i, 'LCBP3');
await chooseCombobox(/recipient organization/i, 'TEAM Consulting');
await user.click(screen.getByRole('combobox', { name: /reference document/i }));
await user.click(await screen.findByText('COR-001'));
await user.type(screen.getByPlaceholderText('Enter transmittal subject'), 'Weekly package');
await user.clear(screen.getByPlaceholderText('ID'));
await user.type(screen.getByPlaceholderText('ID'), '12');
await user.type(screen.getByPlaceholderText('Copies/Notes'), 'For record');
await user.type(screen.getByPlaceholderText('Additional notes...'), 'Submitted by test');
await user.click(screen.getByRole('button', { name: 'Create Transmittal' }));
await waitFor(() => {
expect(transmittalService.create).toHaveBeenCalledWith({
projectId: '019505a1-7c3e-7000-8000-abc123defc01',
recipientOrganizationId: '019505a1-7c3e-7000-8000-abc123defc02',
correspondenceId: '019505a1-7c3e-7000-8000-abc123defc03',
subject: 'Weekly package',
purpose: 'FOR_APPROVAL',
remarks: 'Submitted by test',
items: [{ itemType: 'DRAWING', itemId: 12, description: 'For record' }],
});
});
expect(toast.success).toHaveBeenCalledWith('Transmittal created successfully');
expect(push).toHaveBeenCalledWith('/transmittals/019505a1-7c3e-7000-8000-abc123defc05');
});
});
@@ -0,0 +1,65 @@
// File: frontend/components/transmittal/__tests__/transmittal-list.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for TransmittalList component
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { TransmittalList } from '../transmittal-list';
import { Transmittal } from '@/types/transmittal';
// Mock DataTable เนื่องจากเป็น complex component
vi.mock('@/components/common/data-table', () => ({
DataTable: ({ data, columns }: { data: unknown[]; columns: unknown[] }) => (
<div data-testid="data-table">
<span data-testid="row-count">{data.length} rows</span>
<span data-testid="col-count">{columns.length} columns</span>
</div>
),
}));
vi.mock('next/link', () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href}>{children}</a>
),
}));
// Mock Transmittal data ตาม ADR-019 (UUIDv7)
const mockTransmittal: Transmittal = {
publicId: '019505a1-7c3e-7000-8000-abc123def001',
transmittalNo: 'TRS-2026-001',
subject: 'Test Transmittal Subject',
purpose: 'FOR_APPROVAL',
items: [
{ publicId: '019505a1-7c3e-7000-8000-abc123def002', description: 'Item 1' } as any,
{ publicId: '019505a1-7c3e-7000-8000-abc123def003', description: 'Item 2' } as any,
],
createdAt: '2026-06-01T00:00:00Z',
} as any;
describe('TransmittalList', () => {
it('ควรเรนเดอร์ DataTable ได้ถูกต้อง', () => {
render(<TransmittalList data={[mockTransmittal]} />);
expect(screen.getByTestId('data-table')).toBeInTheDocument();
});
it('ควร pass data ถูกต้องให้ DataTable', () => {
render(<TransmittalList data={[mockTransmittal]} />);
expect(screen.getByTestId('row-count')).toHaveTextContent('1 rows');
});
it('ควร pass columns ถูกต้องให้ DataTable (6 columns)', () => {
render(<TransmittalList data={[mockTransmittal]} />);
expect(screen.getByTestId('col-count')).toHaveTextContent('6 columns');
});
it('ควร return null เมื่อ data เป็น null/undefined', () => {
const { container } = render(<TransmittalList data={null as any} />);
expect(container).toBeEmptyDOMElement();
});
it('ควรเรนเดอร์ empty state เมื่อ data เป็น array ว่าง', () => {
render(<TransmittalList data={[]} />);
expect(screen.getByTestId('data-table')).toBeInTheDocument();
expect(screen.getByTestId('row-count')).toHaveTextContent('0 rows');
});
});