/** Local PostgreSQL + real HTTP acceptance. Catalog sync is read-only; never submits generation. */ import assert from 'node:assert/strict' import { writeFile } from 'node:fs/promises' import { PrismaClient } from '@prisma/client' import { DETAIL_FONT_FAMILY } from '../lib/detail-generator/types' const db = new URL(process.env.DATABASE_URL || '') assert.equal(db.hostname, '127.0.0.1'); assert.equal(db.port, '55441'); assert.equal(db.pathname, '/ai_model_test') const base = 'http://localhost:3120' const p = new PrismaClient({ log: [] }) const evidence: Record = { generationCalls: 0 } async function session(index: number) { const jar = new Map() const store = (response: Response) => { for (const cookie of response.headers.getSetCookie()) { const first = cookie.split(';')[0]; const split = first.indexOf('='); jar.set(first.slice(0, split), first.slice(split + 1)) } } const csrf = await fetch(base + '/api/auth/csrf'); store(csrf) const { csrfToken } = await csrf.json() const response = await fetch(base + '/api/auth/callback/credentials', { method: 'POST', redirect: 'manual', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: base, Cookie: [...jar].map(([k, v]) => `${k}=${v}`).join('; ') }, body: new URLSearchParams({ csrfToken, email: `ai-admin-${index}@example.invalid`, password: 'AiAdminLocalTest-20261012', callbackUrl: base + '/admin', json: 'true' }) }) store(response); assert.equal(response.status, 200) return [...jar].map(([k, v]) => `${k}=${v}`).join('; ') } async function request(cookie: string, path = '', method = 'GET', body?: unknown) { const response = await fetch(base + '/api/admin/ai-models' + path, { method, redirect: 'manual', headers: { ...(cookie ? { Cookie: cookie } : {}), 'Content-Type': 'application/json' }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }) const text = await response.text() if (process.env.DETAIL_GENERATOR_API_KEY) assert.ok(!text.includes(process.env.DETAIL_GENERATOR_API_KEY), 'No key in API response') assert.ok(!text.includes('ai-admin-test-new-provider-key'), 'No replacement key in response') return { status: response.status, data: text ? JSON.parse(text) : null } } function assertStatus(result: { status: number; data: any }, expected = 200) { assert.equal(result.status, expected, JSON.stringify(result.data)); return result.data } async function main() { const initialJobs = await p.detailGeneratorJob.count() assert.equal((await request('')).status, 401) const ordinary = await session(2) for (const [path, method, body] of [['', 'GET', undefined], ['/import-environment', 'POST', undefined], ['/providers', 'POST', {}], ['/configuration', 'PUT', {}]] as const) assert.equal((await request(ordinary, path, method, body)).status, 403) evidence.anonymous = 401; evidence.ordinaryAdmin = 403 const admin = await session(0) let state = assertStatus(await request(admin)) const existing = state.providers.find((provider: any) => provider.credentialSource === 'environment') if (!existing) state = assertStatus(await request(admin, '/import-environment', 'POST')) const imported = state.providers.find((provider: any) => provider.credentialSource === 'environment') assert.ok(imported?.hasApiKey) assert.equal((await request(admin, '/import-environment', 'POST')).status, 409) const again = assertStatus(await request(admin)) assert.equal(again.providers.filter((provider: any) => provider.credentialSource === 'environment').length, 1) evidence.environmentImportIdempotent = true const sync = await request(admin, `/providers/${imported.id}/sync`, 'POST', { revision: imported.revision }) state = assertStatus(sync) const synced = state.providers.find((provider: any) => provider.id === imported.id) assert.ok(synced.models.length >= 2) assert.equal(new Set(synced.models.map((model: any) => model.id)).size, synced.models.length) const known = new Set(['gemini-2.5-flash', 'nano-banana-2']) for (const model of synced.models) if (!known.has(model.id)) { assert.equal(model.enabled, false); assert.equal(model.compatibility, 'unconfirmed') } evidence.catalogModelCount = synced.models.filter((model: any) => model.present).length evidence.newModelsInitiallyDisabled = true assert.equal((await request(admin, `/providers/${imported.id}/sync`, 'POST', { revision: imported.revision })).status, 409) evidence.staleRevisionRejected = true assert.equal((await request(admin, '/providers', 'POST', { name: 'Unsafe fixture', baseUrl: 'https://127.0.0.1', apiKey: 'ai-admin-test-new-provider-key' })).status, 400) evidence.privateAddressRejected = true const created = await request(admin, '/providers', 'POST', { name: '本地验收上游', baseUrl: 'https://api.example.com/v1', apiKey: 'ai-admin-test-new-provider-key' }) state = assertStatus(created, created.status === 201 ? 201 : 200) const newProvider = state.providers.find((provider: any) => provider.name === '本地验收上游') assert.ok(newProvider.hasApiKey); assert.equal(newProvider.credentialSource, 'encrypted'); assert.equal(newProvider.enabled, false) const renamed = assertStatus(await request(admin, `/providers/${newProvider.id}`, 'PATCH', { revision: newProvider.revision, name: '本地验收上游(已保存)' })) assert.ok(renamed.providers.find((provider: any) => provider.id === newProvider.id).hasApiKey) state = renamed evidence.newKeyMaskedAndBlankPreserved = true const defaultProvider = state.providers.find((provider: any) => provider.id === imported.id) const selectable = defaultProvider.models.find((model: any) => model.id === 'gemini-2.5-pro') assert.ok(selectable, 'Live catalog must include selected test model') // Public protocol metadata allows copy only; confirmation cannot grant image understanding. assert.ok(selectable.capabilities.allowedStages.includes('copy')) selectable.enabled = true; selectable.stages = ['copy']; selectable.compatibility = 'confirmed' const analyzer = defaultProvider.models.find((model: any) => model.id === 'gemini-2.5-flash') assert.ok(analyzer.capabilities.allowedStages.includes('analysis')) state.defaults.analysis = analyzer.key; state.defaults.copy = selectable.key const input = { revision: state.revision, defaults: state.defaults, providers: state.providers.map((provider: any) => ({ id: provider.id, revision: provider.revision, models: provider.models.map(({ key, enabled, stages, compatibility }: any) => ({ key, enabled, stages, compatibility })) })) } state = assertStatus(await request(admin, '/configuration', 'PUT', input)) assert.equal(state.defaults.copy, selectable.key) assert.equal((await request(admin, '/configuration', 'PUT', input)).status, 409) const detail = await fetch(base + '/api/detail-generator', { headers: { Cookie: admin } }).then(response => response.json()) assert.equal(detail.config.defaults.copy, selectable.key) assert.ok(detail.config.modelOptions.copy.some((option: any) => option.value === selectable.key && option.label.includes('gemini-2.5-pro'))) assert.ok(!detail.config.modelOptions.analysis.some((option: any) => option.value === selectable.key)) evidence.dynamicDetailDefaults = true const document = { version: 1, name: '本地配置联动验收', product: { identifierType: 'sku', identifier: 'LOCAL-AI-TEST', variant: '', name: '本地验收产品', facts: '', assetIds: [], referenceAssetIds: [], documentAssetIds: [] }, brand: { name: '本地验收', tone: '', primaryColor: '#1677ff', backgroundColor: '#ffffff', textColor: '#101828', fontFamily: DETAIL_FONT_FAMILY, status: 'draft' }, models: detail.config.defaults, analysis: null, approvedAt: null, screens: [] } const projectResponse = await fetch(base + '/api/detail-generator', { method: 'POST', headers: { Cookie: admin, 'Content-Type': 'application/json' }, body: JSON.stringify({ document }) }) assert.equal(projectResponse.status, 200) const project = (await projectResponse.json()).project assert.equal(project.document.models.copy, selectable.key) const active = state.providers.find((provider: any) => provider.id === imported.id) assertStatus(await request(admin, `/providers/${active.id}`, 'PATCH', { revision: active.revision, enabled: false })) const editResponse = await fetch(base + `/api/detail-generator/projects/${project.id}`, { method: 'PATCH', headers: { Cookie: admin, 'Content-Type': 'application/json' }, body: JSON.stringify({ revision: project.revision, document: { ...project.document, name: '停用上游后仍可保存文字' } }) }) assert.equal(editResponse.status, 200) evidence.disabledProviderDoesNotBlockEditing = true state = assertStatus(await request(admin)) const disabled = state.providers.find((provider: any) => provider.id === imported.id) state = assertStatus(await request(admin, `/providers/${disabled.id}`, 'PATCH', { revision: disabled.revision, enabled: true })) const revokedSession = await session(1) const revoked = await p.user.findUniqueOrThrow({ where: { email: 'ai-admin-1@example.invalid' } }) try { await p.user.update({ where: { id: revoked.id }, data: { isSuperAdmin: false } }) assert.equal((await request(revokedSession)).status, 403) assert.equal((await request(revokedSession, `/providers/${imported.id}/sync`, 'POST', { revision: 1 })).status, 403) } finally { await p.user.update({ where: { id: revoked.id }, data: { isSuperAdmin: true } }) } evidence.oldSessionRevoked = true assert.equal(await p.detailGeneratorJob.count(), initialJobs) evidence.jobCountUnchanged = true await writeFile('output/detail-generator/ai-admin-http-evidence.json', JSON.stringify(evidence, null, 2)) console.log(JSON.stringify(evidence)) } main().catch(error => { console.error(error.message); process.exitCode = 1 }).finally(() => p.$disconnect())