103 lines
6.1 KiB
TypeScript
103 lines
6.1 KiB
TypeScript
/** Isolated local HTTP acceptance only. Never submits a generation request. */
|
|
import assert from 'node:assert/strict'
|
|
import { writeFile } from 'node:fs/promises'
|
|
import { PrismaClient } from '@prisma/client'
|
|
import type { AiModelAdminState } from '../lib/ai-model-types'
|
|
|
|
const database = new URL(process.env.DATABASE_URL || '')
|
|
assert.equal(database.hostname, '127.0.0.1')
|
|
assert.equal(database.port, '55441')
|
|
assert.equal(database.pathname, '/ai_model_test')
|
|
const base = 'http://localhost:3120'
|
|
const prisma = new PrismaClient({ log: [] })
|
|
const evidence: Record<string, unknown> = { generationCalls: 0 }
|
|
|
|
async function signIn(index: number) {
|
|
const jar = new Map<string, string>()
|
|
function store(response: Response) {
|
|
for (const cookie of response.headers.getSetCookie()) {
|
|
const first = cookie.split(';')[0], position = first.indexOf('=')
|
|
jar.set(first.slice(0, position), first.slice(position + 1))
|
|
}
|
|
}
|
|
const csrf = await fetch(base + '/api/auth/csrf'); store(csrf)
|
|
const { csrfToken } = await csrf.json()
|
|
const login = await fetch(base + '/api/auth/callback/credentials', {
|
|
method: 'POST', redirect: 'manual',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: base, Cookie: [...jar].map(([key, value]) => `${key}=${value}`).join('; ') },
|
|
body: new URLSearchParams({ csrfToken, email: `ai-admin-${index}@example.invalid`, password: 'AiAdminLocalTest-20261012', callbackUrl: base + '/admin', json: 'true' }),
|
|
})
|
|
store(login); assert.equal(login.status, 200)
|
|
return [...jar].map(([key, value]) => `${key}=${value}`).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, '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))
|
|
return { status: response.status, data: JSON.parse(text) }
|
|
}
|
|
|
|
function input(state: AiModelAdminState) {
|
|
return { revision: state.revision, defaults: state.defaults, providers: state.providers.map(provider => ({
|
|
id: provider.id, revision: provider.revision,
|
|
models: provider.models.map(({ key, enabled, stages, compatibility }) => ({ key, enabled, stages, compatibility })),
|
|
})) }
|
|
}
|
|
|
|
async function main() {
|
|
const beforeJobs = await prisma.detailGeneratorJob.count()
|
|
const admin = await signIn(0), ordinary = await signIn(2)
|
|
assert.equal((await request('')).status, 401)
|
|
assert.equal((await request(ordinary)).status, 403)
|
|
let state = (await request(admin)).data as AiModelAdminState
|
|
const imported = state.providers.find(provider => provider.credentialSource === 'environment')
|
|
assert.ok(imported)
|
|
const sync = await request(admin, `/providers/${imported.id}/sync`, 'POST', { revision: imported.revision })
|
|
assert.equal(sync.status, 200, JSON.stringify(sync.data))
|
|
state = sync.data
|
|
const provider = state.providers.find(value => value.id === imported.id)!
|
|
const present = provider.models.filter(model => model.present)
|
|
const repeat = await request(admin, `/providers/${provider.id}/sync`, 'POST', { revision: provider.revision })
|
|
assert.equal(repeat.status, 200, JSON.stringify(repeat.data))
|
|
assert.equal(repeat.data.providers.find((value: { id: string }) => value.id === provider.id).revision, provider.revision)
|
|
evidence.unchangedCatalogKeepsBindingRevision = true
|
|
evidence.catalogCount = present.length
|
|
evidence.classification = Object.fromEntries(['text', 'vision', 'image', 'other', 'unknown'].map(kind => [kind, present.filter(model => model.capabilities.kind === kind).length]))
|
|
assert.ok(present.some(model => model.capabilities.kind === 'image'))
|
|
assert.ok(present.some(model => model.capabilities.kind === 'text' || model.capabilities.kind === 'vision'))
|
|
const conversation = present.find(model => model.capabilities.allowedStages.includes('copy') && !model.capabilities.allowedStages.includes('image'))!
|
|
assert.ok(conversation)
|
|
const bad = input(state)
|
|
const badModel = bad.providers.find(value => value.id === provider.id)!.models.find(model => model.key === conversation.key)!
|
|
badModel.stages = ['image']; badModel.enabled = true; badModel.compatibility = 'confirmed'
|
|
assert.equal((await request(admin, '/configuration', 'PUT', bad)).status, 400)
|
|
assert.equal((await request(ordinary, '/configuration', 'PUT', bad)).status, 403)
|
|
evidence.conversationImageAssignmentDenied = true
|
|
const forged = input(state)
|
|
Object.assign(forged.providers[0].models[0], { capabilities: { allowedStages: ['image'], kind: 'image', source: 'verified', reason: '' } })
|
|
assert.equal((await request(admin, '/configuration', 'PUT', forged)).status, 400)
|
|
evidence.clientCapabilityForgeryDenied = true
|
|
const unchanged = (await request(admin)).data as AiModelAdminState
|
|
assert.equal(unchanged.revision, state.revision)
|
|
evidence.rejectedConfigurationDoesNotWrite = true
|
|
// Only existing administrator-open models are exposed to the detail generator.
|
|
const response = await fetch(base + '/api/detail-generator', { headers: { Cookie: admin } })
|
|
assert.equal(response.status, 200)
|
|
const { config } = await response.json()
|
|
for (const stage of ['analysis', 'copy', 'image'] as const) {
|
|
const expected = state.providers.flatMap(source => source.models.filter(model => source.enabled && source.hasApiKey && model.present && model.enabled && model.compatibility === 'confirmed' && model.stages.includes(stage) && model.capabilities.allowedStages.includes(stage)).map(model => model.key)).sort()
|
|
assert.deepEqual(config.models[stage].slice().sort(), expected)
|
|
}
|
|
evidence.userChoicesEqualAdministratorOpenCapabilityIntersection = true
|
|
const after = await prisma.detailGeneratorJob.count()
|
|
assert.equal(after, beforeJobs)
|
|
evidence.jobCountUnchanged = true
|
|
await writeFile('output/detail-generator/ai-capabilities-http-evidence.json', JSON.stringify(evidence, null, 2))
|
|
console.log(JSON.stringify(evidence))
|
|
}
|
|
main().catch(error => { console.error(error.message); process.exitCode = 1 }).finally(() => prisma.$disconnect())
|