fix(ai): separate completed executions from unknown model results
This commit is contained in:
parent
b4c2d30898
commit
2c29b9bd78
@ -0,0 +1,11 @@
|
||||
import { aiModelAdminRoute, AiModelError } from '@/lib/ai-model-access'
|
||||
import { releaseAiModelVerificationExecution } from '@/lib/ai-model-execution-release'
|
||||
import { parseDetailJson } from '@/lib/detail-generator/validation'
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
export async function POST(request: Request, { params }: { params: { id: string; verificationId: string } }) {
|
||||
return aiModelAdminRoute(async userId => {
|
||||
if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') throw new AiModelError('须使用 JSON 请求', 415)
|
||||
return releaseAiModelVerificationExecution(userId, params.id, params.verificationId, await parseDetailJson(request))
|
||||
})
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
|
||||
## 测试状态与查询
|
||||
|
||||
执行占用与测试结果分开处理:若原同步请求的最终结果仍未知,但已有服务商核查凭证确认执行结束、计费已结算,超级管理员可通过 `POST /api/admin/ai-models/providers/{providerId}/verifications/{verificationId}/release-execution` 登记执行结束。JSON 必须包含原 `requestId`、`executionEnded: true`、`billingFinalized: true`、`reference`、详细 `note` 与 `confirmed: true`。此操作仅新增审计,不修改原验证记录、请求号或幂等键,不开放模型,也不解除该模型的禁重发限制。其它模型可继续测试;进行中记录、异步任务或凭证不匹配时拒绝。
|
||||
|
||||
| 界面状态 | 含义与操作 |
|
||||
| --- | --- |
|
||||
| 待测试 | 已有可执行的用途适配,尚无该用途的实际测试结果。 |
|
||||
|
||||
25
lib/__tests__/ai-model-execution-release.test.ts
Normal file
25
lib/__tests__/ai-model-execution-release.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/** @jest-environment node */
|
||||
import { randomUUID } from 'crypto'
|
||||
import { releaseAiModelVerificationExecution } from '@/lib/ai-model-execution-release'
|
||||
import { aiModelExecutionOccupiesSlot } from '@/lib/ai-model-execution-slot'
|
||||
import { aiModelAudit } from '@/lib/ai-model-repository'
|
||||
import { assertAiModelAdmin, assertAiModelAdminForWrite } from '@/lib/ai-model-access'
|
||||
jest.mock('next-auth',()=>({getServerSession:jest.fn()}))
|
||||
jest.mock('@/lib/auth',()=>({authOptions:{}}))
|
||||
jest.mock('@/lib/prisma',()=>({prisma:{}}))
|
||||
jest.mock('@/lib/department-permission',()=>({isSuperAdmin:jest.fn()}))
|
||||
jest.mock('@/lib/ai-model-access',()=>({...jest.requireActual('@/lib/ai-model-access'),assertAiModelAdmin:jest.fn(),assertAiModelAdminForWrite:jest.fn()}))
|
||||
jest.mock('@/lib/ai-model-service',()=>({assertAiModelRecoveryTextSafe:jest.fn(),getAiModelAdminState:jest.fn(async()=>({providers:[]}))}))
|
||||
jest.mock('@/lib/detail-generator/repository',()=>({detailTransactionLock:jest.fn()}))
|
||||
const mockDb={$queryRawUnsafe:jest.fn(),$executeRawUnsafe:jest.fn()}
|
||||
jest.mock('@/lib/ai-model-repository',()=>({aiModelTransaction:jest.fn(async cb=>cb(mockDb)),aiModelAudit:jest.fn()}))
|
||||
let row:any,duplicate=false
|
||||
const provider=randomUUID(),requestId=randomUUID(),proof={requestId,executionEnded:true,billingFinalized:true,reference:requestId,note:'服务商已核查:只派发一次,执行已结束,扣费已结算为零,原结果无法恢复。',confirmed:true}
|
||||
beforeEach(()=>{jest.clearAllMocks();duplicate=false;row={id:randomUUID(),modelKey:randomUUID(),stage:'analysis',status:'unknown',requestId,taskId:null,evidence:{},idempotencyKey:randomUUID()};mockDb.$queryRawUnsafe.mockImplementation(async sql=>sql.includes('FROM ai_model_audit')?duplicate?[{id:'audit'}]:[]:[row])})
|
||||
test('ending execution writes only audit; original result, IDs, keys and retry fence are preserved',async()=>{const before=JSON.stringify(row);await releaseAiModelVerificationExecution('admin',provider,row.id,proof);expect(JSON.stringify(row)).toBe(before);expect(mockDb.$executeRawUnsafe).not.toHaveBeenCalled();expect(aiModelAudit).toHaveBeenCalledWith(mockDb,'admin','model.verification-execution-ended',provider,expect.objectContaining({resultStillUnknown:true,retryAllowed:false,originalRequestId:requestId,verificationIds:[row.id]}))})
|
||||
test.each([{requestId:randomUUID()},{executionEnded:false},{billingFinalized:false},{confirmed:false},{status:'passed'},{note:'已结束'},{model:'override'}])('insufficient or altered proof cannot release execution: %j',async change=>{await expect(releaseAiModelVerificationExecution('admin',provider,row.id,{...proof,...change})).rejects.toThrow();expect(aiModelAudit).not.toHaveBeenCalled()})
|
||||
test.each(['running','passed','failed'])('a %s result cannot be released as unknown',async status=>{row.status=status;await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toMatchObject({code:'execution_proof_conflict'});expect(aiModelAudit).not.toHaveBeenCalled()})
|
||||
test('asynchronous tasks cannot skip polling',async()=>{row.taskId='task-1';await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toMatchObject({code:'execution_proof_conflict'})})
|
||||
test('duplicate evidence cannot overwrite the first audit',async()=>{duplicate=true;await releaseAiModelVerificationExecution('admin',provider,row.id,proof);expect(aiModelAudit).not.toHaveBeenCalled()})
|
||||
test('fresh database revocation blocks audit writes',async()=>{(assertAiModelAdminForWrite as jest.Mock).mockRejectedValueOnce(new Error('revoked'));await expect(releaseAiModelVerificationExecution('admin',provider,row.id,proof)).rejects.toThrow('revoked');expect(aiModelAudit).not.toHaveBeenCalled()})
|
||||
test('slot predicate requires the exact original execution, request and verification membership',()=>{const sql=aiModelExecutionOccupiesSlot('v');expect(sql).toContain("v.status='running'");expect(sql).toContain("ended.details->>'originalRequestId'=v.request_id");expect(sql).toContain("ended.details->'verificationIds' ? v.id");expect(()=>aiModelExecutionOccupiesSlot('x;DROP')).toThrow()})
|
||||
@ -168,7 +168,8 @@ test('verification respects active generation capacity', async () => {
|
||||
expect(callAiModelProtocol).not.toHaveBeenCalled()
|
||||
const capacity = mockDb.$queryRawUnsafe.mock.calls.find(([sql]) => sql.includes('active_executions'))!
|
||||
expect(capacity[0]).toContain("status='queued' AND checkpoint->>'dispatchPending'='true'")
|
||||
expect(capacity[0]).toContain("status IN ('running','unknown')")
|
||||
expect(capacity[0]).toContain("v.status='unknown'")
|
||||
expect(capacity[0]).toContain("model.verification-execution-ended")
|
||||
})
|
||||
test('revocation before admission performs no call or insert', async () => {
|
||||
;(assertAiModelAdmin as jest.Mock).mockRejectedValue(new AiModelError('forbidden', 403))
|
||||
@ -324,7 +325,7 @@ test('one multimodal request creates independent passed records and consumes one
|
||||
expect(executions.every(value => value.evidence.purpose === 'analysis-copy' && value.idempotencyKey === submission.idempotencyKey)).toBe(true)
|
||||
expect(mockDb.$executeRawUnsafe.mock.calls.filter(([sql]) => sql.includes("status='passed'")).map(([, id]) => id)).toEqual(executions.map(value => value.id))
|
||||
expect((aiModelAudit as jest.Mock).mock.calls.filter(([, , action]) => action === 'model.verification-start')).toHaveLength(1)
|
||||
expect(mockDb.$queryRawUnsafe.mock.calls.find(([sql]) => sql.includes('active_executions'))?.[0]).toContain("SELECT DISTINCT COALESCE(evidence->>'executionGroupId',id)")
|
||||
expect(mockDb.$queryRawUnsafe.mock.calls.find(([sql]) => sql.includes('active_executions'))?.[0]).toContain("SELECT DISTINCT COALESCE(v.evidence->>'executionGroupId',v.id)")
|
||||
duplicate = true
|
||||
await verifyAiModel('admin', providerId, modelKey, submission)
|
||||
expect(callAiModelProtocol).toHaveBeenCalledTimes(1)
|
||||
|
||||
@ -19,7 +19,7 @@ beforeEach(() => {
|
||||
db.$queryRawUnsafe.mockImplementation(async (sql, ...args) => {
|
||||
if (sql.includes('FROM detail_generator_jobs WHERE id=$1')) return args[1] === current.ownerId ? [{ ...current }] : []
|
||||
if (sql.includes('FROM detail_generator_projects')) return [{ id: 'project-1', ownerId: 'owner-1', revision, createdAt: new Date(), updatedAt: new Date() }]
|
||||
if (sql.includes('UNION ALL SELECT id FROM ai_model_verifications')) return busy ? [{ id: 'another-unknown' }] : []
|
||||
if (sql.includes('UNION ALL SELECT v.id FROM ai_model_verifications')) return busy ? [{ id: 'another-unknown' }] : []
|
||||
if (sql.startsWith('UPDATE detail_generator_jobs')) { current = { ...current, status: args[2], checkpoint: JSON.parse(args[3]), requestId: args[4], leaseToken: args[5] }; return [{ ...current }] }
|
||||
return []
|
||||
})
|
||||
|
||||
28
lib/ai-model-execution-release.ts
Normal file
28
lib/ai-model-execution-release.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { z } from 'zod'
|
||||
import { AiModelError, assertAiModelAdmin, assertAiModelAdminForWrite } from './ai-model-access'
|
||||
import { aiModelAudit, aiModelTransaction } from './ai-model-repository'
|
||||
import { assertAiModelRecoveryTextSafe, getAiModelAdminState } from './ai-model-service'
|
||||
import { AI_MODEL_VERIFICATION_COLUMNS, lockAiModelVerificationExecution, verificationExecutionId, type AiModelVerificationRow } from './ai-model-verification-state'
|
||||
import { detailTransactionLock } from './detail-generator/repository'
|
||||
|
||||
const proofSchema = z.object({ requestId: z.string().uuid(), executionEnded: z.literal(true), billingFinalized: z.literal(true), reference: z.string().trim().min(3).max(160), note: z.string().trim().min(20).max(500), confirmed: z.literal(true) }).strict()
|
||||
/** Records execution completion only. Original unknown rows and their same-model retry fence remain untouched. */
|
||||
export async function releaseAiModelVerificationExecution(userId: string, providerId: string, verificationId: string, body: unknown) {
|
||||
const parsed = proofSchema.safeParse(body)
|
||||
if (!z.string().uuid().safeParse(verificationId).success || !parsed.success) throw new AiModelError('须提供原请求号及执行已结束、计费已结算的核查凭证')
|
||||
await assertAiModelAdmin(userId)
|
||||
await assertAiModelRecoveryTextSafe(providerId, `${parsed.data.reference}\n${parsed.data.note}`)
|
||||
await aiModelTransaction(async db => {
|
||||
await assertAiModelAdminForWrite(userId, db)
|
||||
const rows = await db.$queryRawUnsafe<AiModelVerificationRow[]>(`SELECT ${AI_MODEL_VERIFICATION_COLUMNS} FROM ai_model_verifications WHERE id=$1 AND model_key IN (SELECT id FROM ai_model_catalog WHERE provider_id=$2) FOR UPDATE`, verificationId, providerId)
|
||||
if (!rows[0]) throw new AiModelError('测试记录不存在', 404)
|
||||
const execution = await lockAiModelVerificationExecution(db, rows[0])
|
||||
if (execution.some(row => row.status !== 'unknown' || row.taskId || row.requestId !== parsed.data.requestId)) throw new AiModelError('仅可登记同一原请求的未知同步结果;不能替换请求或关闭异步任务', 409, 'execution_proof_conflict')
|
||||
await detailTransactionLock(db, 'detail-generator-execution')
|
||||
const executionGroupId = verificationExecutionId(rows[0])
|
||||
const prior = await db.$queryRawUnsafe<Array<{ id: string }>>(`SELECT id FROM ai_model_audit WHERE action='model.verification-execution-ended' AND details->>'executionGroupId'=$1 AND details->>'originalRequestId'=$2`, executionGroupId, parsed.data.requestId)
|
||||
if (prior[0]) return
|
||||
await aiModelAudit(db, userId, 'model.verification-execution-ended', providerId, { ...parsed.data, originalRequestId: parsed.data.requestId, executionGroupId, verificationIds: execution.map(row => row.id), modelKey: rows[0].modelKey, resultStillUnknown: true, retryAllowed: false })
|
||||
})
|
||||
return getAiModelAdminState()
|
||||
}
|
||||
10
lib/ai-model-execution-slot.ts
Normal file
10
lib/ai-model-execution-slot.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/** Unknown results retain their retry fence, but a separately audited ended execution need not occupy a worker forever. */
|
||||
export function aiModelExecutionOccupiesSlot(alias = 'v'): string {
|
||||
if (!/^[a-z][a-z0-9_]*$/i.test(alias)) throw new Error('Invalid verification table alias')
|
||||
return `(${alias}.status='running' OR (${alias}.status='unknown' AND NOT EXISTS (
|
||||
SELECT 1 FROM ai_model_audit ended WHERE ended.action='model.verification-execution-ended'
|
||||
AND ended.details->>'executionGroupId'=COALESCE(${alias}.evidence->>'executionGroupId',${alias}.id)
|
||||
AND ended.details->>'originalRequestId'=${alias}.request_id
|
||||
AND ended.details->'verificationIds' ? ${alias}.id
|
||||
)))`
|
||||
}
|
||||
@ -11,6 +11,7 @@ import { AI_MODEL_VERIFICATION_RULE_VERSION, createVerificationChallenge, readVe
|
||||
export { validateAiModelVerificationOutput } from './ai-model-verification-output'
|
||||
import { detailTransactionLock } from './detail-generator/repository'
|
||||
import type { AiModelDatabase } from './ai-model-repository'
|
||||
import { aiModelExecutionOccupiesSlot } from './ai-model-execution-slot'
|
||||
import { createMultiReferenceChallenge, multiReferenceImages, multiReferencePrompt, readMultiReferenceChallenge, validateMultiReferenceOutput } from './ai-model-multireference'
|
||||
|
||||
const requestSchema = z.object({ stage: z.enum(AI_MODEL_TEST_PURPOSES), revision: z.number().int().positive(), idempotencyKey: z.string().uuid(), referenceImageCount: z.number().int().min(1).max(8).optional() }).strict()
|
||||
@ -40,7 +41,7 @@ function imageTransportEvidence(value: unknown, protocol: AiModelProtocol, secre
|
||||
async function assertExecutionSlot(db: AiModelDatabase, actorId: string, resumeId: string | null = null): Promise<void> {
|
||||
await detailTransactionLock(db, 'detail-generator-execution')
|
||||
const rows = await db.$queryRawUnsafe<Array<{ count: number; own: number }>>(`SELECT COUNT(*)::int AS count,COUNT(*) FILTER (WHERE actor_id=$1)::int AS own FROM (
|
||||
SELECT DISTINCT COALESCE(evidence->>'executionGroupId',id) AS execution_id,actor_id FROM ai_model_verifications WHERE ($2::text IS NULL OR COALESCE(evidence->>'executionGroupId',id)<>$2) AND status IN ('running','unknown')
|
||||
SELECT DISTINCT COALESCE(v.evidence->>'executionGroupId',v.id) AS execution_id,v.actor_id FROM ai_model_verifications v WHERE ($2::text IS NULL OR COALESCE(v.evidence->>'executionGroupId',v.id)<>$2) AND ${aiModelExecutionOccupiesSlot('v')}
|
||||
UNION ALL SELECT id AS execution_id,owner_id AS actor_id FROM detail_generator_jobs WHERE status='running' AND lease_expires_at>NOW()
|
||||
OR status='queued' AND checkpoint->>'dispatchPending'='true' AND COALESCE(checkpoint #>> '{upstreamTask,id}', '') ~ '^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,199}$'
|
||||
) AS active_executions`, actorId, resumeId)
|
||||
|
||||
@ -3,6 +3,7 @@ import type { Prisma } from '@prisma/client'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { DetailGeneratorError } from './access'
|
||||
import { assertAiModelAdminForWrite } from '@/lib/ai-model-access'
|
||||
import { aiModelExecutionOccupiesSlot } from '@/lib/ai-model-execution-slot'
|
||||
import { detailTextRecoveryOptions, detailTextRequestId } from './text-recovery'
|
||||
import type { DetailAsset, DetailAssetKind, DetailDocument, DetailJob, DetailJobKind, DetailJobStatus, DetailProject } from './types'
|
||||
|
||||
@ -150,7 +151,7 @@ export async function prepareDetailTextRecovery(id: string, ownerId: string, act
|
||||
...(action === 'query' ? { textRecovery: { mode: 'query-only', stage: current.checkpoint.stage, requestId } } : { textRecovery: undefined,
|
||||
...(current.status === 'failed' ? { copyAttempt: { id: randomUUID(), source: 'known-failed-copy' } } : {}) }) }
|
||||
if (action === 'continue') {
|
||||
const busy = await tx.$queryRawUnsafe<Row[]>(`SELECT id FROM detail_generator_jobs WHERE owner_id=$1 AND status IN ('queued','running') UNION ALL SELECT id FROM ai_model_verifications WHERE actor_id=$1 AND status IN ('running','unknown') LIMIT 1`, ownerId)
|
||||
const busy = await tx.$queryRawUnsafe<Row[]>(`SELECT id FROM detail_generator_jobs WHERE owner_id=$1 AND status IN ('queued','running') UNION ALL SELECT v.id FROM ai_model_verifications v WHERE v.actor_id=$1 AND ${aiModelExecutionOccupiesSlot('v')} LIMIT 1`, ownerId)
|
||||
if (busy.length) throw new DetailGeneratorError('另有生成或模型测试尚未完成,请先完成或核查该请求', 409, 'user_capacity')
|
||||
}
|
||||
const updated = await tx.$queryRawUnsafe<Row[]>(`UPDATE detail_generator_jobs SET status=$3,checkpoint=$4::jsonb,request_id=$5,error=NULL,lease_token=$6,lease_expires_at=CASE WHEN $3='running' THEN NOW()+INTERVAL '10 minutes' ELSE NULL END,completed_at=NULL,updated_at=NOW() WHERE id=$1 AND owner_id=$2 AND status=$7 RETURNING ${jobColumns}`, id, ownerId, action === 'query' ? 'running' : 'queued', JSON.stringify(checkpoint), action === 'query' ? requestId : null, action === 'query' ? randomUUID() : null, current.status)
|
||||
@ -195,8 +196,8 @@ export async function claimDetailJob(): Promise<StoredDetailJob | null> {
|
||||
AND checkpoint->>'stage' LIKE 'screen-%'))`
|
||||
await tx.$executeRawUnsafe(`UPDATE detail_generator_jobs SET status = CASE WHEN ${unresolved} THEN 'unknown' ELSE 'queued' END, error = CASE WHEN ${unresolved} THEN '执行中断,模型请求结果未知;请核查服务商记录,勿重复提交' ELSE NULL END, lease_token = NULL, lease_expires_at = NULL, updated_at = NOW() WHERE status = 'running' AND lease_expires_at < NOW()`)
|
||||
const pendingTask = `checkpoint->>'dispatchPending' = 'true' AND COALESCE(checkpoint #>> '{upstreamTask,id}', '') ~ '^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,199}$'`
|
||||
const activeVerification = `(status IN ('running','unknown'))`
|
||||
const count = await tx.$queryRawUnsafe<Row[]>(`SELECT ((SELECT COUNT(*) FROM detail_generator_jobs WHERE (status='running' AND lease_expires_at>=NOW()) OR (status='queued' AND ${pendingTask})) + (SELECT COUNT(DISTINCT COALESCE(evidence->>'executionGroupId',id)) FROM ai_model_verifications WHERE ${activeVerification}))::int AS count`)
|
||||
const activeVerification = aiModelExecutionOccupiesSlot('v')
|
||||
const count = await tx.$queryRawUnsafe<Row[]>(`SELECT ((SELECT COUNT(*) FROM detail_generator_jobs WHERE (status='running' AND lease_expires_at>=NOW()) OR (status='queued' AND ${pendingTask})) + (SELECT COUNT(DISTINCT COALESCE(v.evidence->>'executionGroupId',v.id)) FROM ai_model_verifications v WHERE ${activeVerification}))::int AS count`)
|
||||
// A queued known upstream task already owns its slot. Let it poll even when both slots are occupied.
|
||||
const candidates = await tx.$queryRawUnsafe<Row[]>(`SELECT ${jobColumns} FROM detail_generator_jobs j WHERE status = 'queued' AND ($1::boolean OR (${pendingTask})) AND (CASE WHEN jsonb_typeof(checkpoint->'pollNotBefore') = 'number' THEN (checkpoint->>'pollNotBefore')::numeric ELSE 0 END) <= EXTRACT(EPOCH FROM NOW()) * 1000 AND EXISTS (SELECT 1 FROM detail_generator_projects p WHERE p.id = j.project_id AND p.deleted_at IS NULL) AND NOT EXISTS (SELECT 1 FROM detail_generator_jobs other WHERE other.owner_id = j.owner_id AND other.status = 'running' AND other.lease_expires_at >= NOW()) AND NOT EXISTS (SELECT 1 FROM ai_model_verifications v WHERE v.actor_id=j.owner_id AND ${activeVerification}) ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED`, Number(count[0]?.count) < 2)
|
||||
if (!candidates[0]) return null
|
||||
|
||||
@ -14,7 +14,8 @@ async function main() {
|
||||
'CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY,"isSuperAdmin" BOOLEAN NOT NULL,"isActive" BOOLEAN NOT NULL,status TEXT,"archivedAt" TIMESTAMPTZ)',
|
||||
'CREATE TABLE IF NOT EXISTS detail_generator_projects (id TEXT PRIMARY KEY,owner_id TEXT NOT NULL,document JSONB NOT NULL,revision INTEGER NOT NULL,created_at TIMESTAMPTZ DEFAULT NOW(),updated_at TIMESTAMPTZ DEFAULT NOW(),deleted_at TIMESTAMPTZ)',
|
||||
"CREATE TABLE IF NOT EXISTS detail_generator_jobs (id TEXT PRIMARY KEY,project_id TEXT NOT NULL,owner_id TEXT NOT NULL,kind TEXT NOT NULL,status TEXT NOT NULL,payload JSONB DEFAULT '{}',checkpoint JSONB DEFAULT '{}',fingerprint TEXT,project_revision INTEGER NOT NULL,idempotency_key TEXT,lease_token TEXT,lease_expires_at TIMESTAMPTZ,progress INTEGER DEFAULT 0,error TEXT,request_id TEXT,created_at TIMESTAMPTZ DEFAULT NOW(),updated_at TIMESTAMPTZ DEFAULT NOW(),started_at TIMESTAMPTZ,completed_at TIMESTAMPTZ)",
|
||||
'CREATE TABLE IF NOT EXISTS ai_model_verifications (id TEXT PRIMARY KEY,actor_id TEXT NOT NULL,status TEXT NOT NULL)',
|
||||
'CREATE TABLE IF NOT EXISTS ai_model_verifications (id TEXT PRIMARY KEY,actor_id TEXT NOT NULL,status TEXT NOT NULL,request_id TEXT,evidence JSONB DEFAULT \'{}\')',
|
||||
"CREATE TABLE IF NOT EXISTS ai_model_audit (id TEXT PRIMARY KEY,action TEXT NOT NULL,details JSONB NOT NULL)",
|
||||
]) await prisma.$executeRawUnsafe(sql)
|
||||
const { prepareDetailTextRecovery } = await import('../lib/detail-generator/repository')
|
||||
const { detailTextDispatchKey } = await import('../lib/detail-generator/text-recovery')
|
||||
@ -45,6 +46,22 @@ async function main() {
|
||||
const blocker = randomUUID()
|
||||
await prisma.$executeRawUnsafe("INSERT INTO ai_model_verifications (id,actor_id,status) VALUES ($1,$2,'unknown')", blocker, owner)
|
||||
await assert.rejects(prepareDetailTextRecovery(id, owner, 'continue'), (error: any) => error.code === 'user_capacity'); await unchanged(); assertions.push('unknown-model-test-blocks-new-copy')
|
||||
const { aiModelExecutionOccupiesSlot } = await import('../lib/ai-model-execution-slot')
|
||||
const occupied = async () => Number((await prisma.$queryRawUnsafe<any[]>(`SELECT COUNT(*)::int AS count FROM ai_model_verifications v WHERE ${aiModelExecutionOccupiesSlot('v')}`))[0].count)
|
||||
await prisma.$executeRawUnsafe('UPDATE ai_model_verifications SET request_id=$2 WHERE id=$1', blocker, requestId)
|
||||
assert.equal(await occupied(), 1); assertions.push('unknown-without-completion-proof-keeps-slot')
|
||||
const audit = randomUUID(), proof = { executionGroupId: blocker, originalRequestId: requestId, verificationIds: [blocker] }
|
||||
for (const incorrect of [{ ...proof, executionGroupId: randomUUID() }, { ...proof, originalRequestId: randomUUID() }, { ...proof, verificationIds: [randomUUID()] }]) {
|
||||
await prisma.$executeRawUnsafe("INSERT INTO ai_model_audit(id,action,details) VALUES ($1,'model.verification-execution-ended',$2::jsonb)", audit, JSON.stringify(incorrect))
|
||||
assert.equal(await occupied(), 1)
|
||||
await prisma.$executeRawUnsafe('DELETE FROM ai_model_audit WHERE id=$1', audit)
|
||||
}
|
||||
assertions.push('other-execution-request-or-row-proof-cannot-release-slot')
|
||||
await prisma.$executeRawUnsafe("INSERT INTO ai_model_audit(id,action,details) VALUES ($1,'model.verification-execution-ended',$2::jsonb)", audit, JSON.stringify(proof))
|
||||
assert.equal(await occupied(), 0); assertions.push('audited-ended-unknown-releases-only-execution-slot')
|
||||
await prisma.$executeRawUnsafe("UPDATE ai_model_verifications SET status='running' WHERE id=$1", blocker)
|
||||
assert.equal(await occupied(), 1); assertions.push('running-execution-cannot-use-old-completion-proof')
|
||||
await prisma.$executeRawUnsafe('DELETE FROM ai_model_audit WHERE id=$1', audit)
|
||||
await prisma.$executeRawUnsafe('DELETE FROM ai_model_verifications WHERE id=$1 AND actor_id=$2', blocker, owner)
|
||||
const continued = await prepareDetailTextRecovery(id, owner, 'continue')
|
||||
assert.equal(continued.status, 'queued'); assert.equal(continued.requestId, null); assert.equal(continued.checkpoint.facts, facts); assertions.push('explicit-continuation-keeps-facts-clears-old-request-id')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user