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
@@ -0,0 +1,296 @@
// File: backend/tests/integration/ai/sandbox-runtime-params.spec.ts
// Change Log:
// - 2026-06-15: Created integration test for runtime parameters application to sandbox (T043)
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Queue } from 'bullmq';
import { AiBatchProcessor } from '../../../src/modules/ai/processors/ai-batch.processor';
import { AiPolicyService } from '../../../src/modules/ai/services/ai-policy.service';
import { AiPromptsService } from '../../../src/modules/ai/prompts/ai-prompts.service';
import { AiExecutionProfile } from '../../../src/modules/ai/entities/ai-execution-profile.entity';
import { AiSandboxProfile } from '../../../src/modules/ai/entities/ai-sandbox-profile.entity';
import { AiPrompt } from '../../../src/modules/ai/prompts/ai-prompts.entity';
import { DataSource } from 'typeorm';
import IORedis from 'ioredis';
describe('Sandbox Runtime Parameters Integration Tests (T043)', () => {
let _processor: AiBatchProcessor;
let aiPolicyService: AiPolicyService;
let aiPromptsService: AiPromptsService;
let aiBatchQueue: Queue;
let dataSource: DataSource;
let redis: IORedis;
beforeAll(async () => {
redis = new IORedis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT || '6379'),
});
aiBatchQueue = new Queue('ai-batch', {
connection: redis,
});
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mariadb',
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT || '3306'),
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'lcbp3_test',
entities: [AiExecutionProfile, AiSandboxProfile, AiPrompt],
synchronize: false,
}),
TypeOrmModule.forFeature([
AiExecutionProfile,
AiSandboxProfile,
AiPrompt,
]),
],
providers: [AiBatchProcessor, AiPolicyService, AiPromptsService],
}).compile();
_processor = module.get<AiBatchProcessor>(AiBatchProcessor);
aiPolicyService = module.get<AiPolicyService>(AiPolicyService);
aiPromptsService = module.get<AiPromptsService>(AiPromptsService);
dataSource = module.get<DataSource>(DataSource);
});
afterAll(async () => {
await aiBatchQueue.close();
await redis.quit();
await dataSource.destroy();
});
describe('Runtime Parameters Application', () => {
it('ควรใช้ custom profile parameters เมื่อระบุ profileId ใน sandbox-rag-prep job', async () => {
// สร้าง custom execution profile
const profileRepo = dataSource.getRepository(AiExecutionProfile);
const customProfile = profileRepo.create({
profileName: 'custom-rag-profile',
canonicalModel: 'np-dms-ai',
temperature: 0.2,
topP: 0.7,
maxTokens: 2048,
numCtx: 4096,
repeatPenalty: 1.2,
keepAliveSeconds: 30,
isActive: true,
createdBy: 1,
});
await profileRepo.save(customProfile);
// สร้าง active prompt สำหรับ rag_prep_prompt
const prompt = await aiPromptsService.create(
'rag_prep_prompt',
{ template: 'Chunk this text: {{text}}' },
1
);
await aiPromptsService.activate(
'rag_prep_prompt',
prompt.versionNumber,
1
);
const idempotencyKey = 'test-runtime-params-001';
await aiBatchQueue.add('sandbox-rag-prep', {
jobType: 'sandbox-rag-prep',
documentPublicId: 'test-doc-001',
projectPublicId: 'default',
payload: {
text: 'Test text for runtime parameters',
profileId: 'custom-rag-profile',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
// ลบข้อมูลทดสอบ
await aiPromptsService.delete('rag_prep_prompt', prompt.versionNumber, 1);
await profileRepo.delete(customProfile.id);
}, 60000);
it('ควร fallback ไป standard profile เมื่อ profileId ไม่มีอยู่', async () => {
const prompt = await aiPromptsService.create(
'rag_prep_prompt',
{ template: 'Chunk this text: {{text}}' },
1
);
await aiPromptsService.activate(
'rag_prep_prompt',
prompt.versionNumber,
1
);
const idempotencyKey = 'test-runtime-params-fallback';
await aiBatchQueue.add('sandbox-rag-prep', {
jobType: 'sandbox-rag-prep',
documentPublicId: 'test-doc-002',
projectPublicId: 'default',
payload: {
text: 'Test text for fallback',
profileId: 'non-existent-profile',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
// ลบข้อมูลทดสอบ
await aiPromptsService.delete('rag_prep_prompt', prompt.versionNumber, 1);
}, 60000);
it('ควรใช้ sandbox draft parameters เมื่อระบุใน sandbox-ai-extract job', async () => {
const sandboxRepo = dataSource.getRepository(AiSandboxProfile);
const sandboxDraft = sandboxRepo.create({
profileName: 'standard',
canonicalModel: 'np-dms-ai',
temperature: 0.3,
topP: 0.6,
maxTokens: 2048,
numCtx: 4096,
repeatPenalty: 1.1,
keepAliveSeconds: 30,
updatedBy: 1,
});
await sandboxRepo.save(sandboxDraft);
const prompt = await aiPromptsService.create(
'ocr_extraction',
{ template: 'Extract from {{ocr_text}}' },
1
);
await aiPromptsService.activate(
'ocr_extraction',
prompt.versionNumber,
1
);
const idempotencyKey = 'test-sandbox-draft-params';
await aiBatchQueue.add('sandbox-ai-extract', {
jobType: 'sandbox-ai-extract',
documentPublicId: 'test-doc-003',
projectPublicId: 'default',
payload: {
promptVersion: prompt.versionNumber,
projectPublicId: 'default',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
// ลบข้อมูลทดสอบ
await aiPromptsService.delete('ocr_extraction', prompt.versionNumber, 1);
await sandboxRepo.delete(sandboxDraft.id);
}, 60000);
it('ควร apply runtime parameters จาก AiPolicyService.getSandboxParameters', async () => {
const profileRepo = dataSource.getRepository(AiExecutionProfile);
const testProfile = profileRepo.create({
profileName: 'runtime-test-profile',
canonicalModel: 'np-dms-ai',
temperature: 0.15,
topP: 0.65,
maxTokens: 1024,
numCtx: 2048,
repeatPenalty: 1.05,
keepAliveSeconds: 15,
isActive: true,
createdBy: 1,
});
await profileRepo.save(testProfile);
// ทดสอบ getSandboxParameters
const params = await aiPolicyService.getSandboxParameters(
'runtime-test-profile'
);
expect(params).toBeDefined();
expect(params.temperature).toBe(0.15);
expect(params.topP).toBe(0.65);
expect(params.maxTokens).toBe(1024);
expect(params.numCtx).toBe(2048);
expect(params.repeatPenalty).toBe(1.05);
expect(params.keepAliveSeconds).toBe(15);
// ลบข้อมูลทดสอบ
await profileRepo.delete(testProfile.id);
});
it('ควร cache sandbox parameters ใน Redis เพื่อ performance', async () => {
const profileRepo = dataSource.getRepository(AiExecutionProfile);
const cacheTestProfile = profileRepo.create({
profileName: 'cache-test-profile',
canonicalModel: 'np-dms-ai',
temperature: 0.25,
topP: 0.75,
maxTokens: 3072,
numCtx: 6144,
repeatPenalty: 1.15,
keepAliveSeconds: 45,
isActive: true,
createdBy: 1,
});
await profileRepo.save(cacheTestProfile);
// First call - should fetch from DB and cache
const params1 =
await aiPolicyService.getSandboxParameters('cache-test-profile');
expect(params1.temperature).toBe(0.25);
// Second call - should fetch from Redis cache
const params2 =
await aiPolicyService.getSandboxParameters('cache-test-profile');
expect(params2.temperature).toBe(0.25);
// Verify cache exists in Redis
const cached = await redis.get('ai:policy:cache-test-profile');
expect(cached).toBeDefined();
// ลบข้อมูลทดสอบ
await profileRepo.delete(cacheTestProfile.id);
await redis.del('ai:policy:cache-test-profile');
});
});
});
@@ -0,0 +1,332 @@
// File: backend/tests/integration/ai/sandbox-workflow.spec.ts
// Change Log:
// - 2026-06-15: Created integration test for 3-step sandbox workflow (T032)
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Queue } from 'bullmq';
import { AiBatchProcessor } from '../../../src/modules/ai/processors/ai-batch.processor';
import { AiPromptsService } from '../../../src/modules/ai/prompts/ai-prompts.service';
import { AiPolicyService } from '../../../src/modules/ai/services/ai-policy.service';
import { OcrService } from '../../../src/modules/ai/services/ocr.service';
import { OllamaService } from '../../../src/modules/ai/services/ollama.service';
import { SandboxOcrEngineService } from '../../../src/modules/ai/services/sandbox-ocr-engine.service';
import { EmbeddingService } from '../../../src/modules/ai/services/embedding.service';
import { AiRagService } from '../../../src/modules/ai/ai-rag.service';
import { Attachment } from '../../../src/common/file-storage/entities/attachment.entity';
import { Project } from '../../../src/modules/project/entities/project.entity';
import { AiPrompt } from '../../../src/modules/ai/prompts/ai-prompts.entity';
import { DataSource } from 'typeorm';
import IORedis from 'ioredis';
describe('3-Step Sandbox Workflow Integration Tests (T032)', () => {
let _processor: AiBatchProcessor;
let aiBatchQueue: Queue;
let aiPromptsService: AiPromptsService;
let dataSource: DataSource;
let redis: IORedis;
beforeAll(async () => {
redis = new IORedis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT || '6379'),
});
aiBatchQueue = new Queue('ai-batch', {
connection: redis,
});
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mariadb',
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT || '3306'),
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'lcbp3_test',
entities: [Attachment, Project, AiPrompt],
synchronize: false,
}),
TypeOrmModule.forFeature([Attachment, Project, AiPrompt]),
],
providers: [
AiBatchProcessor,
AiPromptsService,
AiPolicyService,
OcrService,
OllamaService,
SandboxOcrEngineService,
EmbeddingService,
AiRagService,
],
}).compile();
processor = module.get<AiBatchProcessor>(AiBatchProcessor);
aiPromptsService = module.get<AiPromptsService>(AiPromptsService);
dataSource = module.get<DataSource>(DataSource);
});
afterAll(async () => {
await aiBatchQueue.close();
await redis.quit();
await dataSource.destroy();
});
describe('Step 1: OCR Extraction', () => {
it('ควรส่งงาน sandbox-ocr และรับผลลัพธ์ OCR text จาก Redis', async () => {
const idempotencyKey = 'test-sandbox-ocr-001';
await aiBatchQueue.add('sandbox-ocr', {
jobType: 'sandbox-ocr',
documentPublicId: 'test-doc-001',
projectPublicId: 'default',
payload: {
pdfPath: '/test/sample.pdf',
engine: 'auto',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:ocr:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
expect((result as { ocrText: string }).ocrText).toBeDefined();
expect(typeof (result as { ocrText: string }).ocrText).toBe('string');
}, 60000);
});
describe('Step 2: AI Metadata Extraction', () => {
it('ควรส่งงาน sandbox-ai-extract และรับผลลัพธ์ JSON metadata จาก Redis', async () => {
// สร้าง active prompt สำหรับ ocr_extraction
const prompt = await aiPromptsService.create(
'ocr_extraction',
{ template: 'Extract metadata from {{ocr_text}}' },
1
);
await aiPromptsService.activate(
'ocr_extraction',
prompt.versionNumber,
1
);
const idempotencyKey = 'test-sandbox-extract-001';
await aiBatchQueue.add('sandbox-ai-extract', {
jobType: 'sandbox-ai-extract',
documentPublicId: 'test-doc-002',
projectPublicId: 'default',
payload: {
promptVersion: prompt.versionNumber,
projectPublicId: 'default',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
expect((result as { answer: unknown }).answer).toBeDefined();
// ลบข้อมูลทดสอบ
await aiPromptsService.delete('ocr_extraction', prompt.versionNumber, 1);
}, 60000);
});
describe('Step 3: RAG Prep', () => {
it('ควรส่งงาน sandbox-rag-prep และรับผลลัพธ์ chunks และ vectors จาก Redis', async () => {
// สร้าง active prompt สำหรับ rag_prep_prompt
const prompt = await aiPromptsService.create(
'rag_prep_prompt',
{ template: 'Chunk this text: {{text}}' },
1
);
await aiPromptsService.activate(
'rag_prep_prompt',
prompt.versionNumber,
1
);
const idempotencyKey = 'test-sandbox-rag-prep-001';
await aiBatchQueue.add('sandbox-rag-prep', {
jobType: 'sandbox-rag-prep',
documentPublicId: 'test-doc-003',
projectPublicId: 'default',
payload: {
text: 'This is a test document for RAG preparation. It contains multiple sections that should be chunked semantically.',
profileId: 'standard',
},
idempotencyKey,
});
// Poll Redis for result
let result = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${idempotencyKey}`);
if (cached) {
result = JSON.parse(cached);
break;
}
}
expect(result).toBeDefined();
expect((result as { status: string }).status).toBe('completed');
expect((result as { ragChunks: unknown[] }).ragChunks).toBeDefined();
expect(
Array.isArray((result as { ragChunks: unknown[] }).ragChunks)
).toBe(true);
expect(
(result as { ragChunks: unknown[] }).ragChunks.length
).toBeGreaterThan(0);
expect((result as { ragVectors: unknown[] }).ragVectors).toBeDefined();
expect(
Array.isArray((result as { ragVectors: unknown[] }).ragVectors)
).toBe(true);
// ลบข้อมูลทดสอบ
await aiPromptsService.delete('rag_prep_prompt', prompt.versionNumber, 1);
}, 60000);
});
describe('Full 3-Step Workflow Integration', () => {
it('ควรรัน 3 steps ต่อเนื่องกัน OCR → AI Extract → RAG Prep', async () => {
// สร้าง prompts ที่จำเป็น
const ocrPrompt = await aiPromptsService.create(
'ocr_extraction',
{ template: 'Extract metadata from {{ocr_text}}' },
1
);
await aiPromptsService.activate(
'ocr_extraction',
ocrPrompt.versionNumber,
1
);
const ragPrompt = await aiPromptsService.create(
'rag_prep_prompt',
{ template: 'Chunk this text: {{text}}' },
1
);
await aiPromptsService.activate(
'rag_prep_prompt',
ragPrompt.versionNumber,
1
);
const workflowId = 'test-full-workflow-001';
// Step 1: OCR
await aiBatchQueue.add('sandbox-ocr', {
jobType: 'sandbox-ocr',
documentPublicId: workflowId,
projectPublicId: 'default',
payload: {
pdfPath: '/test/sample.pdf',
engine: 'auto',
},
idempotencyKey: `${workflowId}-ocr`,
});
let ocrResult = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:ocr:result:${workflowId}-ocr`);
if (cached) {
ocrResult = JSON.parse(cached);
break;
}
}
expect(ocrResult).toBeDefined();
expect((ocrResult as { status: string }).status).toBe('completed');
const ocrText = (ocrResult as { ocrText: string }).ocrText;
// Step 2: AI Extract
await aiBatchQueue.add('sandbox-ai-extract', {
jobType: 'sandbox-ai-extract',
documentPublicId: workflowId,
projectPublicId: 'default',
payload: {
promptVersion: ocrPrompt.versionNumber,
projectPublicId: 'default',
},
idempotencyKey: `${workflowId}-extract`,
});
let extractResult = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${workflowId}-extract`);
if (cached) {
extractResult = JSON.parse(cached);
break;
}
}
expect(extractResult).toBeDefined();
expect((extractResult as { status: string }).status).toBe('completed');
expect((extractResult as { answer: unknown }).answer).toBeDefined();
// Step 3: RAG Prep
await aiBatchQueue.add('sandbox-rag-prep', {
jobType: 'sandbox-rag-prep',
documentPublicId: workflowId,
projectPublicId: 'default',
payload: {
text: ocrText || 'Fallback text for RAG prep',
profileId: 'standard',
},
idempotencyKey: `${workflowId}-rag-prep`,
});
let ragResult = null;
for (let i = 0; i < 30; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const cached = await redis.get(`ai:rag:result:${workflowId}-rag-prep`);
if (cached) {
ragResult = JSON.parse(cached);
break;
}
}
expect(ragResult).toBeDefined();
expect((ragResult as { status: string }).status).toBe('completed');
expect((ragResult as { ragChunks: unknown[] }).ragChunks).toBeDefined();
expect((ragResult as { ragVectors: unknown[] }).ragVectors).toBeDefined();
// ลบข้อมูลทดสอบ
await aiPromptsService.delete(
'ocr_extraction',
ocrPrompt.versionNumber,
1
);
await aiPromptsService.delete(
'rag_prep_prompt',
ragPrompt.versionNumber,
1
);
}, 180000);
});
});