/** Disposable local PostgreSQL acceptance. Never connects to the production database. */ import assert from 'node:assert/strict' import { randomUUID } from 'node:crypto' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' import bcrypt from 'bcryptjs' import sharp from 'sharp' import { readPsd } from 'ag-psd' import { prisma } from '../lib/prisma' import { assertDetailGeneratorUser } from '../lib/detail-generator/access' import { approveDetailAnalysis, createDetailAction, editDetailProject, editDetailScreen, getDetailProject, newDetailProject, uploadDetailAsset } from '../lib/detail-generator/service' import { claimDetailJob, enqueueDetailJob, findDetailAsset, findDetailJob, findDetailProject, finishDetailJob } from '../lib/detail-generator/repository' import { runDetailGeneratorWorkerOnce } from '../lib/detail-generator/jobs' import { readDetailFile } from '../lib/detail-generator/storage' import { saveDetailBrandProfile } from '../lib/detail-generator/brand-profiles' import { DETAIL_FONT_FAMILY, DETAIL_MODEL_DEFAULTS, type DetailDocument } from '../lib/detail-generator/types' import { importAiModelEnvironment } from '../lib/ai-model-service' import { parseAiModelCapabilityMetadata } from '../lib/ai-model-capabilities' import dns from 'node:dns/promises' import https from 'node:https' import { syncBuiltinESMExports } from 'node:module' import { EventEmitter } from 'node:events' import { Writable } from 'node:stream' async function main() { const db = new URL(process.env.DATABASE_URL || '') assert.equal(db.hostname, '127.0.0.1', 'Only disposable local PostgreSQL is permitted') assert.equal(db.port, '15437') assert.equal(db.pathname, '/detail_test') process.env.DETAIL_GENERATOR_ENABLED = 'true' process.env.DETAIL_GENERATOR_API_KEY = 'synthetic-test-key-no-provider-spend' process.env.DETAIL_GENERATOR_BASE_URL = 'https://detail-provider.invalid/v1' process.env.DETAIL_GENERATOR_STORAGE_DIR = path.resolve('output/detail-generator/private') const password = 'LocalDetailPreviewOnly-20261012' const hashed = await bcrypt.hash(password, 4) const owners = await Promise.all([0, 1, 2, 3].map(async index => prisma.user.upsert({ where: { email: `detail-test-${index}@example.invalid` }, update: { isSuperAdmin: index !== 3, status: 'active', isActive: true }, create: { email: `detail-test-${index}@example.invalid`, username: `详情页本地验收${index}`, password: hashed, isSuperAdmin: index !== 3, role: 'ADMIN', status: 'active', isActive: true }, }))) for (const owner of owners) await prisma.$executeRaw`DELETE FROM detail_generator_projects WHERE owner_id=${owner.id}` await prisma.$executeRaw`DELETE FROM detail_generator_brand_profiles WHERE name='本地验收品牌'` await assertDetailGeneratorUser(owners[0].id) await assert.rejects(assertDetailGeneratorUser(owners[3].id), /超级管理员/) if (!(await prisma.$queryRawUnsafe>('SELECT id FROM ai_model_providers WHERE legacy_binding=TRUE')).length) await importAiModelEnvironment(owners[0].id) // The guarded synthetic provider has explicit test evidence; it must not inherit real-provider capabilities by model name. for (const stage of ['copy', 'image'] as const) { const stages = stage === 'copy' ? ['analysis', 'copy'] : ['image'] const metadata = parseAiModelCapabilityMetadata({ input_modalities: ['text', 'image'], output_modalities: [stage === 'image' ? 'image' : 'text'], endpoints: [stage === 'image' ? '/images/edits' : '/chat/completions'] }) await prisma.$executeRawUnsafe(`UPDATE ai_model_catalog SET capability_metadata=$1::jsonb,stages=$2::text[],enabled=TRUE,compatibility='confirmed' WHERE model_id=$3 AND provider_id IN (SELECT id FROM ai_model_providers WHERE base_url='https://detail-provider.invalid/v1')`, JSON.stringify(metadata), stages, DETAIL_MODEL_DEFAULTS[stage]) } await prisma.$executeRawUnsafe(`UPDATE ai_model_configuration SET default_analysis=(SELECT c.id FROM ai_model_catalog c JOIN ai_model_providers p ON p.id=c.provider_id WHERE p.legacy_binding=TRUE AND c.model_id=$1),default_copy=(SELECT c.id FROM ai_model_catalog c JOIN ai_model_providers p ON p.id=c.provider_id WHERE p.legacy_binding=TRUE AND c.model_id=$1),default_image=(SELECT c.id FROM ai_model_catalog c JOIN ai_model_providers p ON p.id=c.provider_id WHERE p.legacy_binding=TRUE AND c.model_id=$2) WHERE id='detail-generator'`, DETAIL_MODEL_DEFAULTS.analysis, DETAIL_MODEL_DEFAULTS.image) const document = (): DetailDocument => ({ version: 1, name: '本地验收 · 产品详情页', product: { identifierType: 'sku', identifier: 'TEST-SKU-001', variant: '验收款式', name: '本地验收产品', facts: '仅用于测试:木色外观、圆角设计。没有提供尺寸、材质、承重和认证。', assetIds: [], referenceAssetIds: [], documentAssetIds: [] }, brand: { name: '本地验收品牌', tone: '自然、简洁,真实产品摄影', primaryColor: '#415947', backgroundColor: '#ffffff', textColor: '#101828', fontFamily: DETAIL_FONT_FAMILY, status: 'draft' }, models: { ...DETAIL_MODEL_DEFAULTS }, analysis: null, approvedAt: null, screens: [], }) const image = await sharp(Buffer.from('')).png().toBuffer() async function inputProject(ownerId: string) { let project = await newDetailProject(ownerId, document()) for (const kind of ['product', 'reference', 'document'] as const) { const form = new FormData() form.set('kind', kind); form.set('revision', String(project.revision)) const bytes = kind === 'document' ? Buffer.from('测试产品资料:木色外观、圆角设计。禁止编造尺寸、认证或材质。') : image form.set('file', new Blob([new Uint8Array(bytes)], { type: kind === 'document' ? 'text/plain' : 'image/png' }), kind === 'document' ? '产品资料.txt' : `${kind}.png`) const response = await uploadDetailAsset(ownerId, project.id, new Request('http://localhost/upload', { method: 'POST', body: form })) project = response.project } return project } const project = await inputProject(owners[0].id) await assert.rejects(findDetailProject(project.id, owners[1].id), /项目不存在/) let providerCalls = 0 let imageCalls = 0 let returnUnknown = false let revokeDuringAnalysis: string | null = null async function fakeProvider(input: string, init: { headers: Record; body: Buffer }) { assert.ok(String(input).startsWith('https://detail-provider.invalid/'), 'No live provider calls') assert.ok(new Headers(init?.headers).get('Idempotency-Key')) providerCalls++ if (returnUnknown) return Response.json({ error: { code: 'request_outcome_unknown' } }, { status: 503, headers: { 'x-request-id': 'test-unknown-request' } }) if (String(input).endsWith('/images/edits')) { imageCalls++ assert.ok(init.body.includes(Buffer.from('Content-Disposition: form-data;')), 'Actual streamed multipart reaches the fake HTTPS socket') return Response.json({ data: [{ b64_json: image.toString('base64') }] }) } const body = JSON.parse(String(init?.body)) const isAnalysis = Array.isArray(body.messages[0].content) if (isAnalysis) { assert.ok(JSON.stringify(body).includes('测试产品资料'), 'Documents included in analysis') if (revokeDuringAnalysis) await prisma.user.update({ where: { id: revokeDuringAnalysis }, data: { isSuperAdmin: false } }) } return Response.json({ choices: [{ message: { content: JSON.stringify(isAnalysis ? { facts: ['木色外观', '圆角设计'], warnings: ['未提供材质和尺寸'] } : { sellingPoints: ['木色外观', '圆角设计'], warnings: ['未提供材质和尺寸'], screens: [ { title: '木色,自然融入空间', body: '本地验收示例。展示真实外观与圆角细节。', imagePrompt: '简洁自然背景,保持产品款式' }, { title: '圆角细节', body: '本地验收示例。呈现可见细节。', imagePrompt: '近距离展示实际圆角细节' }, ], }) } }] }) } // Test harness replaces the Node HTTPS socket and DNS only after the disposable DB guard. // Production transport remains unchanged, including its public-address and dispatch checks. const realLookup = dns.lookup const realRequest = https.request dns.lookup = (async hostname => { assert.equal(hostname, 'detail-provider.invalid'); return [{ address: '93.184.216.34', family: 4 }] }) as typeof dns.lookup https.request = ((target: URL, options: any, onResponse: (response: any) => void) => { assert.equal(target.hostname, 'detail-provider.invalid', 'No live provider sockets') const chunks: Buffer[] = [] return new Writable({ write(chunk, _encoding, callback) { chunks.push(Buffer.from(chunk)); callback() }, final(callback) { fakeProvider(target.href, { headers: options.headers, body: Buffer.concat(chunks) }).then(async response => { const incoming = new EventEmitter() as EventEmitter & { statusCode: number; headers: Record; destroy: () => void } incoming.statusCode = response.status; incoming.headers = Object.fromEntries(response.headers.entries()); incoming.destroy = () => undefined onResponse(incoming) incoming.emit('data', Buffer.from(await response.arrayBuffer())); incoming.emit('end'); callback() }).catch(callback) }, }) }) as typeof https.request syncBuiltinESMExports() try { const key = randomUUID() const job = await createDetailAction(owners[0].id, project.id, 'analyze', { idempotencyKey: key }) assert.equal((await createDetailAction(owners[0].id, project.id, 'analyze', { idempotencyKey: key })).id, job.id) await assert.rejects(createDetailAction(owners[0].id, project.id, 'analyze', { idempotencyKey: randomUUID() }), /已有任务/) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(job.id, owners[0].id)).status, 'succeeded') let current = (await getDetailProject(owners[0].id, project.id)).project const humanDraft = structuredClone(current.document) assert.ok(humanDraft.analysis) humanDraft.analysis.screens[0].title = '人工核对:木色自然融入空间' humanDraft.analysis.sellingPoints[0] = '人工核对后的木色外观' const beforeDraftSave = providerCalls current = await editDetailProject(owners[0].id, current.id, { document: humanDraft, revision: current.revision }) current = (await getDetailProject(owners[0].id, current.id)).project assert.deepEqual(current.document.analysis, humanDraft.analysis) assert.equal(current.document.approvedAt, null) assert.equal(providerCalls, beforeDraftSave, 'Saving an unapproved human draft must not call a model') current = await approveDetailAnalysis(owners[0].id, project.id, { analysis: current.document.analysis, revision: current.revision }) const generation = await createDetailAction(owners[0].id, project.id, 'generate', { idempotencyKey: randomUUID() }) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(generation.id, owners[0].id)).status, 'succeeded') assert.equal(imageCalls, 2) current = (await getDetailProject(owners[0].id, project.id)).project const previousCalls = providerCalls const screen = current.document.screens[0] const edited = { ...screen, body: '修改后的中文文案\n本地验收,不调用生图。', texts: screen.texts.map(layer => layer.id.endsWith('-body') ? { ...layer, text: '修改后的中文文案\n本地验收,不调用生图。' } : layer) } current = await editDetailScreen(owners[0].id, current.id, edited.id, { screen: edited, revision: current.revision }) assert.equal(providerCalls, previousCalls) const style = await saveDetailBrandProfile(current.document.brand, undefined) await saveDetailBrandProfile({ ...style.profile, tone: '修改后的品牌草案' }, style.revision) await assert.rejects(saveDetailBrandProfile(style.profile, style.revision), /已更新/) assert.equal(current.document.brand.tone, '自然、简洁,真实产品摄影') await mkdir('output/detail-generator', { recursive: true }) for (const format of ['png', 'jpg', 'long-png', 'psd'] as const) { const exportJob = await createDetailAction(owners[0].id, current.id, 'export', { format, idempotencyKey: randomUUID() }) await runDetailGeneratorWorkerOnce() const result = await findDetailJob(exportJob.id, owners[0].id) assert.equal(result.status, 'succeeded', result.error || undefined) const asset = await findDetailAsset(String(result.checkpoint.exportAssetId), owners[0].id) await assert.rejects(findDetailAsset(asset.id, owners[1].id), /文件不存在/) const bytes = await readDetailFile(asset.storageKey) await writeFile(path.join('output/detail-generator', asset.name), bytes) if (format === 'psd') { const psd = readPsd(bytes, { skipLayerImageData: true, skipCompositeImageData: true, skipThumbnail: true }) assert.equal(psd.children?.length, 2) assert.ok(JSON.stringify(psd).includes('修改后的中文文案')) } } const untouchedImage = current.document.screens[1].image?.assetId const regenerate = await createDetailAction(owners[0].id, current.id, 'regenerate', { idempotencyKey: randomUUID() }, screen.id) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(regenerate.id, owners[0].id)).status, 'succeeded') current = (await getDetailProject(owners[0].id, current.id)).project assert.equal(current.document.screens[1].image?.assetId, untouchedImage) assert.equal(imageCalls, 3) // Separate projects exercise draft reset without altering the generated browser preview. const blankProject = await newDetailProject(owners[2].id, document()) const forged = { ...blankProject.document, analysis: structuredClone(current.document.analysis), approvedAt: new Date().toISOString() } const beforeForged = providerCalls await assert.rejects(editDetailProject(owners[2].id, blankProject.id, { document: forged, revision: blankProject.revision }), /请先分析产品资料/) assert.equal(providerCalls, beforeForged) assert.equal((await getDetailProject(owners[2].id, blankProject.id)).project.document.analysis, null) let workflow = await inputProject(owners[2].id) const workflowAnalysis = await createDetailAction(owners[2].id, workflow.id, 'analyze', { idempotencyKey: randomUUID() }) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(workflowAnalysis.id, owners[2].id)).status, 'succeeded') workflow = (await getDetailProject(owners[2].id, workflow.id)).project workflow = await approveDetailAnalysis(owners[2].id, workflow.id, { analysis: workflow.document.analysis, revision: workflow.revision }) assert.ok(workflow.document.approvedAt) assert.ok(workflow.document.screens.length) const resetDraft = structuredClone(workflow.document) resetDraft.analysis = null resetDraft.product.facts = '人工修正后的测试产品资料:木色外观、圆角设计;其余参数未提供。' workflow = await editDetailProject(owners[2].id, workflow.id, { document: resetDraft, revision: workflow.revision }) assert.equal(workflow.document.analysis, null) assert.equal(workflow.document.approvedAt, null) assert.deepEqual(workflow.document.screens, []) const reanalysis = await createDetailAction(owners[2].id, workflow.id, 'analyze', { idempotencyKey: randomUUID() }) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(reanalysis.id, owners[2].id)).status, 'succeeded') workflow = (await getDetailProject(owners[2].id, workflow.id)).project assert.ok(workflow.document.analysis) assert.equal(workflow.document.approvedAt, null) workflow = await approveDetailAnalysis(owners[2].id, workflow.id, { analysis: workflow.document.analysis, revision: workflow.revision }) const changedSource = structuredClone(workflow.document) changedSource.brand.tone = '修改后的测试品牌风格草案' const beforeSourceChange = providerCalls workflow = await editDetailProject(owners[2].id, workflow.id, { document: changedSource, revision: workflow.revision }) assert.equal(workflow.document.analysis, null) assert.equal(workflow.document.approvedAt, null) assert.deepEqual(workflow.document.screens, []) assert.equal(providerCalls, beforeSourceChange) const outlinePreviewJob = await createDetailAction(owners[2].id, workflow.id, 'analyze', { idempotencyKey: randomUUID() }) await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(outlinePreviewJob.id, owners[2].id)).status, 'succeeded') workflow = (await getDetailProject(owners[2].id, workflow.id)).project assert.ok(workflow.document.analysis) assert.equal(workflow.document.approvedAt, null) assert.deepEqual(workflow.document.screens, []) const unknownProject = await inputProject(owners[1].id) returnUnknown = true const unknownJob = await createDetailAction(owners[1].id, unknownProject.id, 'analyze', { idempotencyKey: randomUUID() }) const beforeUnknown = providerCalls await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(unknownJob.id, owners[1].id)).status, 'unknown') await runDetailGeneratorWorkerOnce() assert.equal(providerCalls, beforeUnknown + 1) await assert.rejects(createDetailAction(owners[1].id, unknownProject.id, 'analyze', { idempotencyKey: randomUUID() }), /结果未知/) returnUnknown = false const revokedProject = await inputProject(owners[2].id) revokeDuringAnalysis = owners[2].id const revokedJob = await createDetailAction(owners[2].id, revokedProject.id, 'analyze', { idempotencyKey: randomUUID() }) const beforeRevoked = providerCalls await runDetailGeneratorWorkerOnce() assert.equal((await findDetailJob(revokedJob.id, owners[2].id)).status, 'failed') assert.equal(providerCalls, beforeRevoked + 1, 'No copy/image call after revocation') await assert.rejects(assertDetailGeneratorUser(owners[2].id), /超级管理员/) await prisma.user.update({ where: { id: owners[2].id }, data: { isSuperAdmin: true } }) revokeDuringAnalysis = null // Concurrent claims prove the limit under real PostgreSQL locks. const queueProjects = await Promise.all(owners.slice(0, 3).map(owner => newDetailProject(owner.id, document()))) const queue = await Promise.all(queueProjects.map((item, i) => enqueueDetailJob({ projectId: item.id, ownerId: owners[i].id, kind: 'export', revision: item.revision, idempotencyKey: randomUUID(), payload: { format: 'png' } }))) const claimed = (await Promise.all([claimDetailJob(), claimDetailJob(), claimDetailJob()])).filter(item => item !== null) assert.equal(claimed.length, 2) assert.equal(new Set(claimed.map(item => item.ownerId)).size, 2) for (const item of claimed) await finishDetailJob(item.id, item.leaseToken!, 'failed', {}, 'Synthetic capacity test') const last = await claimDetailJob() assert.ok(last) await prisma.$executeRaw`UPDATE detail_generator_jobs SET checkpoint='{"dispatchPending":true}'::jsonb,lease_expires_at=NOW()-INTERVAL '1 second' WHERE id=${last.id}` assert.equal(await claimDetailJob(), null) assert.equal((await findDetailJob(last.id, last.ownerId)).status, 'unknown') assert.equal(queue.length, 3) console.log(JSON.stringify({ passed: ['DB super-admin gate', 'creator isolation', 'uploads/documents', 'analyze+approve+generate', 'human outline draft save+restore', 'blank analysis forgery denied', 'reset facts+reanalyze', 'approved source invalidation', 'idempotency', 'text edit without image call', 'brand draft snapshot+conflict', '4 export formats', 'single-screen regeneration', 'unknown no replay', 'revocation before next call', 'concurrent max2', 'expired dispatch no replay'], providerCalls, realProviderCalls: 0, previewEmail: owners[0].email, projectId: current.id, outlineProjectId: workflow.id, outlineEmail: owners[2].email })) await writeFile('output/detail-generator/preview.json', JSON.stringify({ email: owners[0].email, password, projectId: current.id, outlineProjectId: workflow.id, outlineEmail: owners[2].email })) } finally { dns.lookup = realLookup; https.request = realRequest; syncBuiltinESMExports(); await prisma.$disconnect() } } main().catch(error => { console.error(error); process.exitCode = 1 })