115 lines
12 KiB
TypeScript
115 lines
12 KiB
TypeScript
/** Real upstream acceptance, restricted to the named local QA database. No production configuration writes. */
|
||
import assert from 'node:assert/strict'
|
||
import { randomUUID } from 'node:crypto'
|
||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||
import { PrismaClient } from '@prisma/client'
|
||
import type { AiModelAdminState } from '../lib/ai-model-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 output = 'D:/AI Code/gongxiangp2/output/ai-image-adapter-20261015', base = 'http://127.0.0.1:3125'
|
||
const targets = ['gpt-image-2', 'gpt-image-2-vip', 'gpt-image-2.5', 'gpt-image-2.5-flare', 'gpt-image-2.5-sunburst']
|
||
const prisma = new PrismaClient({ log: [] })
|
||
async function save(name: string, data: unknown) { await mkdir(output, { recursive: true }); await writeFile(`${output}/${name}.json`, JSON.stringify(data, null, 2)) }
|
||
async function main() {
|
||
const jar = new Map<string, string>()
|
||
function store(r: Response) { for (const entry of r.headers.getSetCookie()) { const raw = entry.split(';')[0], at = raw.indexOf('='); jar.set(raw.slice(0, at), raw.slice(at + 1)) } }
|
||
const cookie = () => Array.from(jar).map(([key, value]) => `${key}=${value}`).join('; ')
|
||
const csrf = await fetch(base + '/api/auth/csrf'); store(csrf)
|
||
const signed = await fetch(base + '/api/auth/callback/credentials', { method: 'POST', redirect: 'manual', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: cookie() }, body: new URLSearchParams({ csrfToken: (await csrf.json()).csrfToken, email: 'ai-admin-1@example.invalid', password: 'AiAdminLocalTest-20261012', callbackUrl: base + '/admin', json: 'true' }) }); store(signed); assert.equal(signed.status, 200)
|
||
async function request(path = '', method = 'GET', body?: unknown) {
|
||
const r = await fetch(base + '/api/admin/ai-models' + path, { method, headers: { Cookie: cookie(), 'Content-Type': 'application/json' }, ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(195000) })
|
||
const text = await r.text(); assert.ok(!process.env.DETAIL_GENERATOR_API_KEY || !text.includes(process.env.DETAIL_GENERATOR_API_KEY), 'Credential leaked')
|
||
const data = JSON.parse(text); assert.equal(r.status, 200, JSON.stringify({ status: r.status, code: data.code, error: data.error })); return data as AiModelAdminState
|
||
}
|
||
let state = await request(), provider = state.providers.find(p => p.credentialSource === 'environment')!
|
||
assert.ok(provider)
|
||
const phase = process.argv[2] || 'inspect'
|
||
if (phase === 'check') {
|
||
const flags = (s: AiModelAdminState) => s.providers.map(p => ({ id: p.id, models: p.models.map(m => ({ key: m.key, enabled: m.enabled, stages: m.stages })) }))
|
||
const beforeFlags = flags(state), beforeDefaults = state.defaults
|
||
const count = async () => (await prisma.$queryRawUnsafe<Array<{ count: number }>>('SELECT COUNT(*)::int AS count FROM ai_model_verifications'))[0].count
|
||
const beforeCount = await count()
|
||
async function reject(path: string, body: unknown, status: number, token = cookie()) {
|
||
const r = await fetch(base + '/api/admin/ai-models' + path, { method: 'POST', headers: { Cookie: token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||
const data = await r.json(); assert.equal(r.status, status, JSON.stringify({ status: r.status, code: data.code })); return data
|
||
}
|
||
for (const modelId of ['nano-banana-2-2k-cl', 'nano-banana-2-4k-cl']) {
|
||
const m = provider.models.find(m => m.id === modelId)!
|
||
assert.ok(m); assert.equal(m.capabilities.reason, '仅支持文生图,不能接收产品参考图')
|
||
const result = await reject(`/providers/${provider.id}/models/${m.key}/verify`, { stage: 'image', revision: provider.revision, idempotencyKey: randomUUID() }, 400)
|
||
assert.equal(result.code, 'model_capability_unsupported')
|
||
}
|
||
for (const modelId of targets) {
|
||
const receipt = JSON.parse(await readFile(`${output}/test-${modelId}.json`, 'utf8'))
|
||
await request(`/providers/${provider.id}/models/${receipt.modelKey}/verify`, 'POST', { stage: 'image', revision: provider.revision, idempotencyKey: receipt.idempotencyKey })
|
||
}
|
||
assert.equal(await count(), beforeCount, 'Rejected or replayed submissions must not create any new model execution')
|
||
const configuration = (s: AiModelAdminState, key: string, enabled: boolean, stages: Array<'image'>) => ({ revision: s.revision, defaults: s.defaults, providers: s.providers.map(p => ({ id: p.id, revision: p.revision, models: p.models.map(m => ({ key: m.key, enabled: m.key === key ? enabled : m.enabled, stages: m.key === key ? stages : m.stages })) })) })
|
||
for (const modelId of targets.filter(id => id.includes('2.5'))) {
|
||
const m = provider.models.find(m => m.id === modelId)!
|
||
assert.equal(m.verification.image.status, 'failed'); assert.equal(m.enabled, false)
|
||
const r = await fetch(base + '/api/admin/ai-models/configuration', { method: 'PUT', headers: { Cookie: cookie(), 'Content-Type': 'application/json' }, body: JSON.stringify(configuration(state, m.key, true, ['image'])) })
|
||
assert.equal(r.status, 400); assert.equal((await r.json()).code, 'model_verification_required')
|
||
}
|
||
const passed = provider.models.find(m => m.id === 'gpt-image-2-vip')!
|
||
assert.equal(passed.verification.image.status, 'passed'); assert.equal(passed.enabled, false); assert.deepEqual(passed.stages, [])
|
||
state = await request('/configuration', 'PUT', configuration(state, passed.key, true, ['image']))
|
||
assert.ok(state.providers.find(p => p.id === provider.id)!.models.find(m => m.key === passed.key)!.enabled)
|
||
state = await request('/configuration', 'PUT', configuration(state, passed.key, false, []))
|
||
assert.deepEqual(flags(state), beforeFlags); assert.deepEqual(state.defaults, beforeDefaults)
|
||
const anonymous = await fetch(base + '/api/admin/ai-models'); assert.equal(anonymous.status, 401)
|
||
const otherJar = new Map<string, string>()
|
||
const absorb = (r: Response) => { for (const item of r.headers.getSetCookie()) { const raw = item.split(';')[0], at = raw.indexOf('='); otherJar.set(raw.slice(0, at), raw.slice(at + 1)) } }
|
||
const otherCookie = () => Array.from(otherJar).map(([key, value]) => `${key}=${value}`).join('; ')
|
||
const otherCsrf = await fetch(base + '/api/auth/csrf'); absorb(otherCsrf)
|
||
const otherSigned = await fetch(base + '/api/auth/callback/credentials', { method: 'POST', redirect: 'manual', headers: { Cookie: otherCookie(), 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ csrfToken: (await otherCsrf.json()).csrfToken, email: 'ai-admin-2@example.invalid', password: 'AiAdminLocalTest-20261012', callbackUrl: base + '/admin', json: 'true' }) }); absorb(otherSigned); assert.equal(otherSigned.status, 200)
|
||
provider = state.providers.find(p => p.id === provider.id)!
|
||
await reject(`/providers/${provider.id}/models/${passed.key}/verify`, { stage: 'image', revision: provider.revision, idempotencyKey: randomUUID() }, 403, otherCookie())
|
||
assert.equal(await count(), beforeCount)
|
||
await save('native-check', { textOnlyRejectedBeforeCall: true, idempotentReplays: targets.length, rejectedEnable: 3, passedEnableSaveAndRestore: true, unrelatedSettingsPreserved: true, anonymous: 401, ordinaryAdmin: 403, verificationCountUnchanged: true, productionTouched: false })
|
||
console.log('Native API acceptance passed: reference-image eligibility, replay safety, enable gating, settings restoration and authorization.'); return
|
||
}
|
||
if (phase === 'reconcile') {
|
||
const modelId = process.argv[3]; assert.ok(targets.includes(modelId))
|
||
const receipt = JSON.parse(await readFile(`${output}/test-${modelId}.json`, 'utf8'))
|
||
assert.equal(receipt.status, 'unknown')
|
||
const script = await readFile(`${output}/upstream-trace-request.sh`, 'utf8')
|
||
assert.ok(script.includes(`client:${receipt.idempotencyKey}`))
|
||
const log = await readFile(`${output}/upstream-trace-request.sh.log`, 'utf8')
|
||
const proof = JSON.parse(log.split(/\r?\n/).find(line => line.startsWith('{'))!)
|
||
assert.equal(proof.status, 'failed_final'); assert.equal(proof.httpStatus, 502); assert.equal(proof.errorCode, 'service_rejected')
|
||
assert.match(proof.requestId, /^[a-f0-9-]{36}$/)
|
||
state = await request(`/providers/${provider.id}/verifications/${receipt.verification.id}/reconcile`, 'POST', { outcome: 'failed', reference: proof.requestId, note: `已按本次请求编号只读核查上游记录,最终状态 failed_final,HTTP 502,service_rejected,请求已结束。`, confirmed: true })
|
||
const verification = state.providers.find(p => p.id === provider.id)!.models.find(m => m.key === receipt.modelKey)!.verification.image
|
||
assert.equal(verification.status, 'failed'); assert.ok(verification.reconciled)
|
||
await save(`reconciled-${modelId}`, { proof, verification, productionTouched: false }); console.log(JSON.stringify({ modelId, proof, verification })); return
|
||
}
|
||
if (phase === 'sync') {
|
||
const models = provider.models.filter(m => targets.includes(m.id)); assert.equal(models.length, targets.length)
|
||
const preserved = provider.models.filter(m => !targets.includes(m.id)).map(m => ({ key: m.key, enabled: m.enabled, stages: m.stages, verification: m.verification }))
|
||
state = await request(`/providers/${provider.id}/sync`, 'POST', { revision: provider.revision, modelKeys: models.map(m => m.key) })
|
||
provider = state.providers.find(p => p.id === provider.id)!
|
||
assert.deepEqual(provider.models.filter(m => !targets.includes(m.id)).map(m => ({ key: m.key, enabled: m.enabled, stages: m.stages, verification: m.verification })), preserved)
|
||
const result = provider.models.filter(m => targets.includes(m.id)).map(m => ({ id: m.id, capabilities: m.capabilities, verification: m.verification.image.status }))
|
||
assert.ok(result.every(m => m.capabilities.allowedStages.includes('image')))
|
||
await save('sync', { productionTouched: false, unrelatedModelsPreserved: true, models: result }); console.log(JSON.stringify(result)); return
|
||
}
|
||
if (phase === 'test') {
|
||
const modelId = process.argv[3]; assert.ok(targets.includes(modelId))
|
||
try { await readFile(`${output}/test-${modelId}.json`); throw new Error('This model already has a test receipt; inspect it instead of repeating a paid call') } catch (error: any) { if (error.code !== 'ENOENT') throw error }
|
||
const model = provider.models.find(m => m.id === modelId)!; assert.ok(model.capabilities.allowedStages.includes('image'))
|
||
const actor = await prisma.user.findUniqueOrThrow({ where: { email: 'ai-admin-1@example.invalid' } }); assert.ok(actor.isSuperAdmin)
|
||
const idempotencyKey = randomUUID(), startedAt = new Date().toISOString()
|
||
await save(`test-${modelId}`, { modelId, modelKey: model.key, providerId: provider.id, idempotencyKey, startedAt, status: 'submitting' })
|
||
state = await request(`/providers/${provider.id}/models/${model.key}/verify`, 'POST', { stage: 'image', revision: provider.revision, idempotencyKey })
|
||
const fresh = state.providers.find(p => p.id === provider.id)!.models.find(m => m.key === model.key)!
|
||
const rows = await prisma.$queryRawUnsafe<any[]>('SELECT id,stage,status,request_id,task_id,evidence,message FROM ai_model_verifications WHERE model_key=$1 AND actor_id=$2 AND idempotency_key=$3', model.key, actor.id, idempotencyKey)
|
||
assert.equal(rows.length, 1)
|
||
await save(`test-${modelId}`, { modelId, modelKey: model.key, providerId: provider.id, idempotencyKey, startedAt, status: rows[0].status, verification: fresh.verification.image, record: rows[0], productionTouched: false })
|
||
console.log(JSON.stringify({ modelId, status: rows[0].status, verification: fresh.verification.image, evidence: rows[0].evidence }))
|
||
assert.equal(rows[0].status, 'passed', 'Real reference-image test must pass before release'); return
|
||
}
|
||
console.log(JSON.stringify(provider.models.filter(m => targets.includes(m.id) || m.id.endsWith('-cl')).map(m => ({ id: m.id, enabled: m.enabled, reason: m.capabilities.reason, stages: m.capabilities.allowedStages, verification: m.verification.image }))))
|
||
}
|
||
main().catch(error => { console.error(error instanceof Error ? error.message : 'Image acceptance failed'); process.exitCode = 1 }).finally(() => prisma.$disconnect())
|