2026-07-21 15:28:55 +08:00
process . env . NODE_ENV = 'test' ;
process . env . DATABASE_URL = 'pg-mem://' ;
process . env . INITIAL_ADMIN_PASSWORD = 'AdminTest123!' ;
process . env . COS_CONFIG_ENCRYPTION_KEY = 'test-only-encryption-key-at-least-32-characters' ;
const { default : app } = await import ( '../api/app.js' ) ;
2026-07-22 16:41:03 +08:00
const { database , closeDatabase } = await import ( '../api/database.js' ) ;
2026-07-22 18:12:23 +08:00
const { encryptSecret } = await import ( '../api/configCrypto.js' ) ;
2026-07-21 15:28:55 +08:00
const server = app . listen ( 0 , '127.0.0.1' ) ;
await new Promise < void > ( ( resolve ) = > server . once ( 'listening' , resolve ) ) ;
const address = server . address ( ) ;
if ( ! address || typeof address === 'string' ) throw new Error ( '测试服务启动失败' ) ;
const base = ` http://127.0.0.1: ${ address . port } ` ;
async function request ( path : string , init : RequestInit = { } , cookie? : string ) {
const response = await fetch ( ` ${ base } ${ path } ` , { . . . init , headers : { . . . ( init . headers || { } ) , . . . ( cookie ? { Cookie : cookie } : { } ) } } ) ;
const body = response . status === 204 ? null : await response . json ( ) ;
return { response , body } ;
}
function expectStatus ( actual : number , expected : number , label : string , body? : unknown ) {
if ( actual !== expected ) throw new Error ( ` ${ label } : 期望 ${ expected } ,实际 ${ actual } ,响应 ${ JSON . stringify ( body ) } ` ) ;
}
try {
const health = await request ( '/api/health' ) ; expectStatus ( health . response . status , 200 , '健康检查' ) ;
if ( ( health . body as { database? : string } ) . database !== 'postgres' ) throw new Error ( '测试未运行在 PostgreSQL 查询层' ) ;
const login = await request ( '/api/auth/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { username : 'admin' , password : 'AdminTest123!' } ) } ) ;
expectStatus ( login . response . status , 200 , '平台管理员登录' ) ;
const adminCookie = login . response . headers . get ( 'set-cookie' ) ? . split ( ';' ) [ 0 ] ;
if ( ! adminCookie ) throw new Error ( '登录未返回会话 Cookie' ) ;
const group = await request ( '/api/management/groups' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { name : '测试运营组' , username : 'test_manager' , display_name : '测试管理员' , password : 'Manager123!' } ) } , adminCookie ) ;
expectStatus ( group . response . status , 201 , '创建运营组' ) ;
const groupId = Number ( ( group . body as { id : number } ) . id ) ;
const duplicateGroupAdmin = await request ( '/api/management/users' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { group_id : groupId , username : 'second_manager' , display_name : '林溪' , password : 'Manager123!' , role : 'group_admin' } ) } , adminCookie ) ;
expectStatus ( duplicateGroupAdmin . response . status , 409 , '拒绝第二位组管理员' , duplicateGroupAdmin . body ) ;
const firstOperator = await request ( '/api/management/users' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { group_id : groupId , username : 'first_operator' , display_name : '林溪' , password : 'Operator123!' , role : 'operator' } ) } , adminCookie ) ;
expectStatus ( firstOperator . response . status , 201 , '创建第一位光影叙事' , firstOperator . body ) ;
const secondOperator = await request ( '/api/management/users' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { group_id : groupId , username : 'second_operator' , display_name : '周宁' , password : 'Operator123!' , role : 'operator' } ) } , adminCookie ) ;
expectStatus ( secondOperator . response . status , 201 , '创建第二位光影叙事' , secondOperator . body ) ;
const secondPlatformAdmin = await request ( '/api/management/users' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { username : 'second_admin' , display_name : '平台管理员乙' , password : 'AdminTest123!' , role : 'platform_admin' } ) } , adminCookie ) ;
expectStatus ( secondPlatformAdmin . response . status , 201 , '创建第二位平台管理员' , secondPlatformAdmin . body ) ;
const renameOperator = await request ( ` /api/management/users/ ${ Number ( ( firstOperator . body as { id : number } ).id)}/name ` , { method : 'PATCH' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { display_name : '林清溪' } ) } , adminCookie ) ;
expectStatus ( renameOperator . response . status , 200 , '修改光影叙事用户名' , renameOperator . body ) ;
const groupAccounts = await request ( ` /api/management/users?groupId= ${ groupId } ` , { } , adminCookie ) ;
expectStatus ( groupAccounts . response . status , 200 , '读取组内账号' , groupAccounts . body ) ;
const groupUsers = groupAccounts . body as Array < { display_name : string ; role : string } > ;
if ( groupUsers . filter ( ( item ) = > item . role === 'group_admin' ) . length !== 1 || groupUsers . filter ( ( item ) = > item . role === 'operator' ) . length !== 2 ) throw new Error ( '运营组账号数量规则未正确执行' ) ;
const allAccounts = await request ( '/api/management/users' , { } , adminCookie ) ;
expectStatus ( allAccounts . response . status , 200 , '读取平台账号' , allAccounts . body ) ;
if ( ( allAccounts . body as Array < { role : string } > ) . filter ( ( item ) = > item . role === 'platform_admin' ) . length !== 2 ) throw new Error ( '多个平台管理员未正确创建' ) ;
const replaceAdmin = await request ( ` /api/management/groups/ ${ groupId } /replace-admin ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { user_id : Number ( ( firstOperator . body as { id : number } ) . id ) , previous_action : 'demote' } ) } , adminCookie ) ;
expectStatus ( replaceAdmin . response . status , 200 , '更换组管理员' , replaceAdmin . body ) ;
const replacedAccounts = await request ( ` /api/management/users?groupId= ${ groupId } ` , { } , adminCookie ) ;
const replacedUsers = replacedAccounts . body as Array < { id :number ; role :string ; last_login_at :string | null } > ;
if ( replacedUsers . filter ( ( item ) = > item . role === 'group_admin' ) . length !== 1 || replacedUsers . find ( ( item ) = > item . role === 'group_admin' ) ? . id !== Number ( ( firstOperator . body as { id :number } ) . id ) ) throw new Error ( '组管理员更换未保持唯一身份' ) ;
const groupsAfterReplace = await request ( '/api/management/groups' , { } , adminCookie ) ;
expectStatus ( groupsAfterReplace . response . status , 200 , '读取运营组统计' , groupsAfterReplace . body ) ;
const groupStats = ( groupsAfterReplace . body as Array < { id :number ; user_count :number ; operator_count :number ; group_admin_name :string | null } > ) . find ( ( item ) = > item . id === groupId ) ;
if ( ! groupStats || groupStats . user_count !== 3 || groupStats . operator_count !== 2 || ! groupStats . group_admin_name ) throw new Error ( '运营组统计不正确' ) ;
const renameGroup = await request ( ` /api/management/groups/ ${ groupId } ` , { method : 'PATCH' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { name : '已更名运营组' } ) } , adminCookie ) ;
expectStatus ( renameGroup . response . status , 200 , '修改运营组名称' , renameGroup . body ) ;
if ( ( renameGroup . body as { name :string } ) . name !== '已更名运营组' ) throw new Error ( '运营组名称未正确更新' ) ;
const newAdminLogin = await request ( '/api/auth/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { username : 'first_operator' , password : 'Operator123!' } ) } ) ;
expectStatus ( newAdminLogin . response . status , 200 , '新组管理员登录' , newAdminLogin . body ) ;
2026-07-21 18:28:21 +08:00
const newAdminCookie = newAdminLogin . response . headers . get ( 'set-cookie' ) ? . split ( ';' ) [ 0 ] ;
if ( ! newAdminCookie ) throw new Error ( '新组管理员登录未返回会话 Cookie' ) ;
2026-07-21 15:28:55 +08:00
const accountsAfterLogin = await request ( ` /api/management/users?groupId= ${ groupId } ` , { } , adminCookie ) ;
if ( ! ( accountsAfterLogin . body as Array < { username :string ; last_login_at :string | null } > ) . find ( ( item ) = > item . username === 'first_operator' ) ? . last_login_at ) throw new Error ( '最近登录时间未记录' ) ;
const adminLogs = await request ( '/api/management/audit-logs?userId=1' , { } , adminCookie ) ;
expectStatus ( adminLogs . response . status , 200 , '按账号筛选审计日志' , adminLogs . body ) ;
if ( ! ( adminLogs . body as Array < { action :string } > ) . some ( ( item ) = > item . action === 'group.admin_replace' ) ) throw new Error ( '管理员更换审计日志缺失' ) ;
const project = await request ( '/api/projects' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { name : 'PostgreSQL 联调项目' , slug : 'postgres-runtime-test' , client_description : '运行时验证' , groupId } ) } , adminCookie ) ;
expectStatus ( project . response . status , 201 , '创建项目' , project . body ) ;
if ( ( project . body as { group_name? :string } ) . group_name !== '已更名运营组' ) throw new Error ( '项目接口未返回所属运营组名称' ) ;
const projectId = Number ( ( project . body as { id : number } ) . id ) ;
2026-07-22 18:12:23 +08:00
const missingStorage = await request ( ` /api/projects/ ${ projectId } /works ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '缺少 COS 配置' , images : [ 'https://cdn.example.com/missing.jpg' ] } ) } , adminCookie ) ;
expectStatus ( missingStorage . response . status , 409 , '没有活动 COS 时拒绝 URL 导入' , missingStorage . body ) ;
const worksWithoutStorage = await request ( ` /api/projects/ ${ projectId } /works ` , { } , adminCookie ) ;
if ( ( worksWithoutStorage . body as unknown [ ] ) . length !== 0 ) throw new Error ( 'URL 导入失败后仍创建了作品记录' ) ;
await database . insertId ( "INSERT INTO storage_configs (region,bucket,public_base_url,cdn_domain,path_prefix,secret_id_encrypted,secret_key_encrypted,status,test_status,created_by) VALUES (?,?,?,?,?,?,?,?,?,?)" , [ 'ap-guangzhou' , 'runtime-test-1234567890' , 'https://runtime-test-1234567890.cos.ap-guangzhou.myqcloud.com' , 'https://cdn.example.com' , 'delivery-desk' , encryptSecret ( 'test-secret-id' ) , encryptSecret ( 'test-secret-key' ) , 'active' , 'passed' , 1 ] ) ;
const blockedPrivateImage = await request ( ` /api/projects/ ${ projectId } /works ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '禁止内网图片' , images : [ 'http://127.0.0.1/private.jpg' ] } ) } , adminCookie ) ;
expectStatus ( blockedPrivateImage . response . status , 400 , '拒绝内网图片转存' , blockedPrivateImage . body ) ;
const blockedPrivateIpv6 = await request ( ` /api/projects/ ${ projectId } /works ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '禁止 IPv6 本机图片' , images : [ 'http://[::1]/private.jpg' ] } ) } , adminCookie ) ;
expectStatus ( blockedPrivateIpv6 . response . status , 400 , '拒绝 IPv6 本机图片转存' , blockedPrivateIpv6 . body ) ;
2026-07-21 18:28:21 +08:00
const otherProject = await request ( '/api/projects' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { name : '同组隔离项目' , slug : 'isolated-project' , client_description : '不应被项目 Key 看见' , groupId } ) } , adminCookie ) ;
expectStatus ( otherProject . response . status , 201 , '创建同组隔离项目' , otherProject . body ) ;
const otherProjectId = Number ( ( otherProject . body as { id :number } ) . id ) ;
2026-07-22 16:41:03 +08:00
const otherWork = await request ( ` /api/projects/ ${ otherProjectId } /works ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '其他项目作品' , images : [ 'https://cdn.example.com/isolated.jpg' ] } ) } , adminCookie ) ;
expectStatus ( otherWork . response . status , 201 , '创建其他项目作品' , otherWork . body ) ;
const otherWorkId = Number ( ( otherWork . body as { id :number } ) . id ) ;
2026-07-22 18:12:23 +08:00
const nativeFetch = globalThis . fetch ;
const { default : COS } = await import ( 'cos-nodejs-sdk-v5' ) ;
const cosPrototype = COS . prototype as unknown as { putObject : ( . . . args :unknown [ ] ) = > unknown } ;
const nativePutObject = cosPrototype . putObject ;
let importedStorageKey = '' ;
try {
globalThis . fetch = ( async ( input :RequestInfo | URL , init? :RequestInit ) = > {
const target = input instanceof Request?input.url :String ( input ) ;
if ( target === 'https://93.184.216.34/source.png' ) return new Response ( Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' , 'base64' ) , { status :200 , headers : { 'Content-Type' : 'image/png' } } ) ;
return nativeFetch ( input , init ) ;
} ) as typeof fetch ;
cosPrototype . putObject = ( async ( options :unknown ) = > { importedStorageKey = String ( ( options as { Key :string } ) . Key ) ; return { statusCode :200 } } ) as typeof cosPrototype . putObject ;
const importedWork = await request ( ` /api/projects/ ${ otherProjectId } /works ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '异源转存作品' , images : [ 'https://93.184.216.34/source.png' ] } ) } , adminCookie ) ;
expectStatus ( importedWork . response . status , 201 , '异源图片转存 COS' , importedWork . body ) ;
const importedDetail = await request ( ` /api/works/ ${ Number ( ( importedWork . body as { id :number } ).id)} ` , { } , adminCookie ) ;
const importedImage = ( importedDetail . body as { images :Array < { url :string ; storage_provider :string ; storage_key :string } > } ) . images [ 0 ] ;
if ( ! importedStorageKey . startsWith ( 'delivery-desk/imports/' ) || importedImage . url !== ` https://cdn.example.com/ ${ importedStorageKey } ` || importedImage . storage_provider !== 'tencent_cos' ) throw new Error ( '异源图片没有归一到当前 COS' ) ;
} finally {
globalThis . fetch = nativeFetch ;
cosPrototype . putObject = nativePutObject ;
}
2026-07-21 15:28:55 +08:00
const access = await request ( ` /api/projects/ ${ projectId } /customer-access ` , { method : 'PATCH' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { enabled : true , password : 'Review123!' } ) } , adminCookie ) ;
expectStatus ( access . response . status , 200 , '配置客户访问' ) ;
const collection = await request ( ` /api/projects/ ${ projectId } /collections ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { name : '第一阶段' , client_description : '验收阶段' } ) } , adminCookie ) ;
expectStatus ( collection . response . status , 201 , '创建作品交付集' ) ;
2026-07-21 18:28:21 +08:00
const collectionId = Number ( ( collection . body as { id :number } ) . id ) ;
2026-07-21 19:23:58 +08:00
if ( ( collection . body as { status :string } ) . status !== 'draft' ) throw new Error ( '空作品交付集未初始化为待提交' ) ;
2026-07-21 18:28:21 +08:00
const projectKey = await request ( '/api/management/api-keys' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { name : '项目接入测试 Key' , project_id :projectId } ) } , newAdminCookie ) ;
expectStatus ( projectKey . response . status , 201 , '创建项目级 API Key' , projectKey . body ) ;
const projectKeyId = Number ( ( projectKey . body as { item : { id :number } } ) . item . id ) ;
const projectToken = ( projectKey . body as { token :string } ) . token ;
const bearerHeaders = { Authorization : ` Bearer ${ projectToken } ` } ;
const visibleProjects = await request ( '/api/projects' , { headers :bearerHeaders } ) ;
expectStatus ( visibleProjects . response . status , 200 , '项目级 Key 查询项目' , visibleProjects . body ) ;
if ( ( visibleProjects . body as Array < { id :number } > ) . length !== 1 || Number ( ( visibleProjects . body as Array < { id :number } > ) [ 0 ] . id ) !== projectId ) throw new Error ( '项目级 Key 未严格隔离到绑定项目' ) ;
const forbiddenProject = await request ( ` /api/projects/ ${ otherProjectId } ` , { headers :bearerHeaders } ) ;
expectStatus ( forbiddenProject . response . status , 403 , '项目级 Key 拒绝访问其他项目' , forbiddenProject . body ) ;
2026-07-22 16:41:03 +08:00
const forbiddenFeedback = await request ( ` /api/works/ ${ otherWorkId } /annotations ` , { headers :bearerHeaders } ) ;
expectStatus ( forbiddenFeedback . response . status , 403 , '项目级 Key 拒绝读取其他项目作品反馈' , forbiddenFeedback . body ) ;
2026-07-21 18:28:21 +08:00
const visibleCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
expectStatus ( visibleCollections . response . status , 200 , '项目级 Key 查询作品交付集' , visibleCollections . body ) ;
if ( ! ( visibleCollections . body as Array < { id :number } > ) . some ( ( item ) = > Number ( item . id ) === collectionId ) ) throw new Error ( '项目级 Key 未返回目标作品交付集' ) ;
const createWorkBody = { collectionId , externalId : 'runtime-client-work-001' , title : '接口作品 V1' , description : '公开 URL 图片' , tags : [ 'API 测试' ] , images : [ 'https://cdn.example.com/runtime-v1.jpg' ] } ;
const createdWork = await request ( '/api/notes' , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( createWorkBody ) } ) ;
expectStatus ( createdWork . response . status , 201 , 'JSON URL 创建作品' , createdWork . body ) ;
const workId = Number ( ( createdWork . body as { id :number } ) . id ) ;
2026-07-21 19:23:58 +08:00
const reviewingCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const reviewingCollection = ( reviewingCollections . body as Array < { id :number ; status :string ; work_count :number } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( reviewingCollection ? . status !== 'reviewing' || Number ( reviewingCollection . work_count ) !== 1 ) throw new Error ( '新增待验收作品后,作品交付集未进入验收中' ) ;
2026-07-22 16:41:03 +08:00
const reviewingProject = await request ( ` /api/projects/ ${ projectId } ` , { headers :bearerHeaders } ) ;
if ( ( reviewingProject . body as { review_status :string } ) . review_status !== 'reviewing' ) throw new Error ( '新增待验收作品后,项目未进入验收中' ) ;
2026-07-21 18:28:21 +08:00
const repeatedWork = await request ( '/api/notes' , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( createWorkBody ) } ) ;
expectStatus ( repeatedWork . response . status , 200 , 'externalId 幂等创建' , repeatedWork . body ) ;
if ( Number ( ( repeatedWork . body as { id :number } ) . id ) !== workId || ! ( repeatedWork . body as { idempotent? :boolean } ) . idempotent ) throw new Error ( 'externalId 重复请求创建了不同作品' ) ;
const foundWork = await request ( ` /api/notes?collectionId= ${ collectionId } &externalId=runtime-client-work-001 ` , { headers :bearerHeaders } ) ;
expectStatus ( foundWork . response . status , 200 , 'externalId 查询作品' , foundWork . body ) ;
if ( ( foundWork . body as Array < { id :number } > ) . length !== 1 || Number ( ( foundWork . body as Array < { id :number } > ) [ 0 ] . id ) !== workId ) throw new Error ( '未能通过 externalId 找回作品' ) ;
const newVersion = await request ( ` /api/notes/ ${ workId } /versions ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '接口作品 V2' , description : '第二版' , tags : [ 'API 测试' ] , images : [ 'https://cdn.example.com/runtime-v2.jpg' ] } ) } ) ;
expectStatus ( newVersion . response . status , 201 , 'JSON URL 创建新版本' , newVersion . body ) ;
if ( Number ( ( newVersion . body as { version_number :number } ) . version_number ) !== 2 ) throw new Error ( '作品版本号未递增' ) ;
const workDetail = await request ( ` /api/notes/ ${ workId } ` , { headers :bearerHeaders } ) ;
expectStatus ( workDetail . response . status , 200 , '读取新版本作品' , workDetail . body ) ;
2026-07-22 16:41:03 +08:00
const firstRound = ( workDetail . body as { rounds :Array < { version_number :number ; round_status :string ; completion_reason :string } > } ) . rounds . find ( ( item ) = > item . version_number === 1 ) ;
if ( firstRound ? . round_status !== 'completed' || firstRound . completion_reason !== 'superseded' ) throw new Error ( '新验收轮次未自动收口旧轮次' ) ;
2026-07-21 18:28:21 +08:00
const currentImages = ( workDetail . body as { images :Array < { url :string ; storage_provider :string } > } ) . images ;
2026-07-22 18:12:23 +08:00
if ( currentImages . length !== 1 || currentImages [ 0 ] . url !== 'https://cdn.example.com/runtime-v2.jpg' || currentImages [ 0 ] . storage_provider !== 'tencent_cos' ) throw new Error ( '同源 COS 图片没有直接复用' ) ;
2026-07-21 15:28:55 +08:00
const reviewLogin = await request ( '/api/review/postgres-runtime-test/login' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { reviewer_name : '客户测试' , password : 'Review123!' } ) } ) ;
expectStatus ( reviewLogin . response . status , 200 , '客户登录' ) ;
const reviewCookie = reviewLogin . response . headers . get ( 'set-cookie' ) ? . split ( ';' ) [ 0 ] ;
const reviewProject = await request ( '/api/review/postgres-runtime-test/project' , { } , reviewCookie ) ;
expectStatus ( reviewProject . response . status , 200 , '客户项目读取' ) ;
2026-07-21 20:25:52 +08:00
const approveV2 = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { version_number :2 , decision : 'approved' } ) } , reviewCookie ) ;
2026-07-21 19:23:58 +08:00
expectStatus ( approveV2 . response . status , 200 , '客户通过作品' , approveV2 . body ) ;
const completedCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const completedCollection = ( completedCollections . body as Array < { id :number ; status :string ; completed_at :string | null ; approved_count :number } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( completedCollection ? . status !== 'completed' || ! completedCollection . completed_at || Number ( completedCollection . approved_count ) !== 1 ) throw new Error ( '全部作品通过后,作品交付集未自动完成' ) ;
const readonlyComment = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /comments ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { content : '完成后不应写入' } ) } , reviewCookie ) ;
expectStatus ( readonlyComment . response . status , 409 , '验收完毕后客户只读' , readonlyComment . body ) ;
const completedWorkDetail = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } ` , { } , reviewCookie ) ;
expectStatus ( completedWorkDetail . response . status , 200 , '完成后读取作品' , completedWorkDetail . body ) ;
2026-07-22 16:41:03 +08:00
if ( ( completedWorkDetail . body as { project : { review_status :string } } ) . project . review_status !== 'completed' ) throw new Error ( '作品详情未返回项目验收完成状态' ) ;
const completedProject = await request ( ` /api/projects/ ${ projectId } ` , { headers :bearerHeaders } ) ;
if ( ( completedProject . body as { review_status :string } ) . review_status !== 'completed' ) throw new Error ( '全部作品通过后,项目接口未返回验收完成' ) ;
2026-07-21 19:23:58 +08:00
const reopenApproved = await request ( ` /api/notes/ ${ workId } /reopen ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { reason : '补充复核' } ) } , newAdminCookie ) ;
expectStatus ( reopenApproved . response . status , 200 , '组管理员重新打开已通过作品' , reopenApproved . body ) ;
const reopenedByAdmin = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
if ( ( reopenedByAdmin . body as Array < { id :number ; status :string } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ? . status !== 'reviewing' ) throw new Error ( '管理员重新打开作品后,作品交付集未回到验收中' ) ;
2026-07-21 20:25:52 +08:00
const approveReopened = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { version_number :2 , decision : 'approved' } ) } , reviewCookie ) ;
2026-07-21 19:23:58 +08:00
expectStatus ( approveReopened . response . status , 200 , '客户通过重新打开的作品' , approveReopened . body ) ;
const versionThree = await request ( ` /api/notes/ ${ workId } /versions ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '接口作品 V3' , description : '完成后追加版本' , tags : [ 'API 测试' ] , images : [ 'https://cdn.example.com/runtime-v3.jpg' ] } ) } ) ;
expectStatus ( versionThree . response . status , 201 , '完成后创建新版本' , versionThree . body ) ;
const reopenedCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const reopenedCollection = ( reopenedCollections . body as Array < { id :number ; status :string ; completed_at :string | null } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( reopenedCollection ? . status !== 'reviewing' || reopenedCollection . completed_at !== null ) throw new Error ( '新版本未将作品交付集重新打开为验收中' ) ;
2026-07-22 16:41:03 +08:00
const requestChanges = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :3 , decision : 'changes_requested' , reason : '请调整第三轮' } ) } , reviewCookie ) ;
2026-07-21 19:23:58 +08:00
expectStatus ( requestChanges . response . status , 200 , '客户要求修改' , requestChanges . body ) ;
2026-07-22 16:41:03 +08:00
const closedRoundComment = await request ( ` /api/works/ ${ workId } /comments ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { content : '退修轮次不应继续写入' } ) } ) ;
expectStatus ( closedRoundComment . response . status , 409 , '退修轮次禁止新增总体反馈' , closedRoundComment . body ) ;
const closedRoundDecision = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :3 , decision : 'approved' } ) } , reviewCookie ) ;
expectStatus ( closedRoundDecision . response . status , 409 , '退修轮次不可再次通过' , closedRoundDecision . body ) ;
const roundFour = await request ( ` /api/works/ ${ workId } /rounds ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '接口作品 V4' , description : '第四轮' , tags : [ 'API 测试' ] , images : [ 'https://cdn.example.com/runtime-v4.jpg' ] } ) } ) ;
expectStatus ( roundFour . response . status , 201 , '创建单方案第 4 轮' , roundFour . body ) ;
const multiRound = await request ( ` /api/notes/ ${ workId } /review-rounds ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { candidates : [ { title : '方案 A' , images : [ 'https://cdn.example.com/a.jpg' ] } , { title : '方案 B' , images : [ 'https://cdn.example.com/b.jpg' ] } ] } ) } ) ;
expectStatus ( multiRound . response . status , 400 , '拒绝一轮多个方案' , multiRound . body ) ;
const historicalClientAnnotation = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /text-annotations ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :3 , target : 'title' , start_offset :0 , end_offset :4 , selected_text : '接口作品' , content : '历史轮次不应写入' } ) } , reviewCookie ) ;
2026-07-21 20:25:52 +08:00
expectStatus ( historicalClientAnnotation . response . status , 409 , '客户不可批注历史轮次' , historicalClientAnnotation . body ) ;
2026-07-22 16:41:03 +08:00
const mismatchedSelection = await request ( ` /api/works/ ${ workId } /text-annotations ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :4 , target : 'title' , start_offset :0 , end_offset :4 , selected_text : '错误文字' , content : '不应写入' } ) } ) ;
expectStatus ( mismatchedSelection . response . status , 400 , '拒绝与内容快照不匹配的文字选区' , mismatchedSelection . body ) ;
const textAnnotation = await request ( ` /api/works/ ${ workId } /text-annotations ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :4 , target : 'title' , start_offset :0 , end_offset :4 , selected_text : '接口作品' , content : '标题选区批注' } ) } ) ;
expectStatus ( textAnnotation . response . status , 201 , '创建标题选区批注' , textAnnotation . body ) ;
const textAnnotationId = Number ( ( textAnnotation . body as { id :number } ) . id ) ;
const tagAnnotation = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /text-annotations ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :4 , target : 'tags' , start_offset :0 , end_offset :6 , selected_text : 'API 测试' , content : 'Tag 选区批注' } ) } , reviewCookie ) ;
expectStatus ( tagAnnotation . response . status , 201 , '客户创建 Tag 选区批注' , tagAnnotation . body ) ;
const feedbackReply = await request ( ` /api/works/ ${ workId } /feedback/text_annotation/ ${ textAnnotationId } /replies ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { content : '已记录这条意见' } ) } ) ;
expectStatus ( feedbackReply . response . status , 201 , '回复文字批注' , feedbackReply . body ) ;
const feedback = await request ( ` /api/works/ ${ workId } /annotations ` , { headers :bearerHeaders } ) ;
expectStatus ( feedback . response . status , 200 , '按作品 ID 获取全部反馈' , feedback . body ) ;
const roundFeedback = ( feedback . body as { rounds :Array < { round_number :number ; text_annotations :Array < unknown > ; feedback_replies :Array < unknown > } > } ) . rounds . find ( ( item ) = > item . round_number === 4 ) ;
if ( roundFeedback ? . text_annotations . length !== 2 || roundFeedback . feedback_replies . length !== 1 ) throw new Error ( '作品反馈聚合未返回标题、Tag 批注及其回复' ) ;
const optimizationContext = await request ( ` /api/works/ ${ workId } /optimization-context?round=4 ` , { headers :bearerHeaders } ) ;
expectStatus ( optimizationContext . response . status , 200 , '获取内容优化上下文' , optimizationContext . body ) ;
const optimizationBody = optimizationContext . body as { content : { title :string ; tags :string [ ] ; images :Array < { url :string } > } ; feedback : { text_annotations :Array < { target :string ; replies :Array < unknown > } > } } ;
if ( optimizationBody . content . title !== '接口作品 V4' || optimizationBody . content . tags [ 0 ] !== 'API 测试' || optimizationBody . content . images [ 0 ] ? . url !== 'https://cdn.example.com/runtime-v4.jpg' ) throw new Error ( '内容优化上下文缺少当前轮次图文快照' ) ;
if ( optimizationBody . feedback . text_annotations . length !== 2 || optimizationBody . feedback . text_annotations . find ( ( item ) = > item . target === 'title' ) ? . replies . length !== 1 ) throw new Error ( '内容优化上下文未组合有效批注与回复' ) ;
const withdrawText = await request ( ` /api/works/ ${ workId } /feedback/text_annotation/ ${ textAnnotationId } /withdraw ` , { method : 'POST' , headers :bearerHeaders } ) ;
expectStatus ( withdrawText . response . status , 200 , '本人留痕撤回文字批注' , withdrawText . body ) ;
const afterWithdraw = await request ( ` /api/works/ ${ workId } /annotations ` , { headers :bearerHeaders } ) ;
const withdrawnItem = ( afterWithdraw . body as { rounds :Array < { round_number :number ; text_annotations :Array < { id :number ; withdrawn_at :string | null } > } > } ) . rounds . find ( ( item ) = > item . round_number === 4 ) ? . text_annotations . find ( ( item ) = > item . id === textAnnotationId ) ;
if ( ! withdrawnItem ? . withdrawn_at ) throw new Error ( '撤回批注未在作品聚合接口中保留记录' ) ;
const actionableContext = await request ( ` /api/works/ ${ workId } /optimization-context?round=4 ` , { headers :bearerHeaders } ) ;
if ( ( actionableContext . body as { feedback : { text_annotations :Array < { id :number } > } } ) . feedback . text_annotations . some ( ( item ) = > item . id === textAnnotationId ) ) throw new Error ( '内容优化上下文默认返回了已撤回批注' ) ;
const historyContext = await request ( ` /api/works/ ${ workId } /optimization-context?round=4&include_history=true ` , { headers :bearerHeaders } ) ;
if ( ! ( historyContext . body as { feedback : { text_annotations :Array < { id :number } > } } ) . feedback . text_annotations . some ( ( item ) = > item . id === textAnnotationId ) ) throw new Error ( '内容优化上下文无法按需返回历史批注' ) ;
await database . execute ( "UPDATE projects SET status='closed' WHERE id=?" , [ projectId ] ) ;
const closedProjectRound = await request ( ` /api/works/ ${ workId } /rounds ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { title : '不应创建' , images : [ 'https://cdn.example.com/closed.jpg' ] } ) } , adminCookie ) ;
expectStatus ( closedProjectRound . response . status , 409 , '已关闭项目禁止创建新轮次' , closedProjectRound . body ) ;
const closedProjectDecision = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :4 , decision : 'approved' } ) } , reviewCookie ) ;
expectStatus ( closedProjectDecision . response . status , 409 , '已关闭项目禁止客户验收写入' , closedProjectDecision . body ) ;
await database . execute ( "UPDATE projects SET status='active' WHERE id=?" , [ projectId ] ) ;
const approveRoundFour = await request ( ` /api/review/postgres-runtime-test/works/ ${ workId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { round_number :4 , decision : 'approved' } ) } , reviewCookie ) ;
expectStatus ( approveRoundFour . response . status , 200 , '客户通过第 4 轮' , approveRoundFour . body ) ;
const secondWork = await request ( ` /api/projects/ ${ projectId } /works ` , { method : 'POST' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { externalId : 'runtime-client-work-002' , title : '完成后新增作品' , description : '验证部分通过' , tags : [ 'API 测试' ] , images : [ 'https://cdn.example.com/runtime-second.jpg' ] } ) } ) ;
2026-07-21 19:23:58 +08:00
expectStatus ( secondWork . response . status , 201 , '完成后新增作品' , secondWork . body ) ;
const secondWorkId = Number ( ( secondWork . body as { id :number } ) . id ) ;
const partialCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const partialCollection = ( partialCollections . body as Array < { id :number ; status :string ; work_count :number ; approved_count :number } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( partialCollection ? . status !== 'reviewing' || Number ( partialCollection . work_count ) !== 2 || Number ( partialCollection . approved_count ) !== 1 ) throw new Error ( '完成后新增作品未恢复验收中或进度统计错误' ) ;
2026-07-22 16:41:03 +08:00
const reopenedProject = await request ( ` /api/projects/ ${ projectId } ` , { headers :bearerHeaders } ) ;
if ( ( reopenedProject . body as { review_status :string } ) . review_status !== 'reviewing' ) throw new Error ( '完成后新增作品未将项目恢复为验收中' ) ;
2026-07-21 20:25:52 +08:00
const approveSecond = await request ( ` /api/review/postgres-runtime-test/works/ ${ secondWorkId } /decision ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { version_number :1 , decision : 'approved' } ) } , reviewCookie ) ;
2026-07-21 19:23:58 +08:00
expectStatus ( approveSecond . response . status , 200 , '客户通过新增作品' , approveSecond . body ) ;
const forbiddenDraft = await request ( ` /api/notes/ ${ workId } /status ` , { method : 'PATCH' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { status : 'draft' } ) } ) ;
expectStatus ( forbiddenDraft . response . status , 409 , '普通写入不能绕过重新打开规则' , forbiddenDraft . body ) ;
const reopenForDraft = await request ( ` /api/notes/ ${ workId } /reopen ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { reason : '验证退回草稿后的集合状态' } ) } , newAdminCookie ) ;
expectStatus ( reopenForDraft . response . status , 200 , '组管理员重新打开第一件作品' , reopenForDraft . body ) ;
const reopenSecondForDraft = await request ( ` /api/notes/ ${ secondWorkId } /reopen ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( { reason : '验证空集合状态' } ) } , newAdminCookie ) ;
expectStatus ( reopenSecondForDraft . response . status , 200 , '组管理员重新打开第二件作品' , reopenSecondForDraft . body ) ;
const draftWork = await request ( ` /api/notes/ ${ workId } /status ` , { method : 'PATCH' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { status : 'draft' } ) } ) ;
expectStatus ( draftWork . response . status , 200 , '作品退回草稿' , draftWork . body ) ;
const draftSecondWork = await request ( ` /api/notes/ ${ secondWorkId } /status ` , { method : 'PATCH' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { status : 'draft' } ) } ) ;
expectStatus ( draftSecondWork . response . status , 200 , '新增作品退回草稿' , draftSecondWork . body ) ;
const draftCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const draftCollection = ( draftCollections . body as Array < { id :number ; status :string ; work_count :number ; completed_at :string | null } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( draftCollection ? . status !== 'draft' || Number ( draftCollection . work_count ) !== 0 || draftCollection . completed_at !== null ) throw new Error ( '无已提交作品时未回到待提交' ) ;
const resubmitWork = await request ( ` /api/notes/ ${ workId } /status ` , { method : 'PATCH' , headers : { . . . bearerHeaders , 'Content-Type' : 'application/json' } , body :JSON.stringify ( { status : 'pending' } ) } ) ;
expectStatus ( resubmitWork . response . status , 200 , '重新提交作品' , resubmitWork . body ) ;
const deleteWork = await request ( ` /api/notes/ ${ workId } ` , { method : 'DELETE' , headers :bearerHeaders } ) ;
expectStatus ( deleteWork . response . status , 204 , '删除最后一件作品' , deleteWork . body ) ;
const deleteSecondWork = await request ( ` /api/notes/ ${ secondWorkId } ` , { method : 'DELETE' , headers :bearerHeaders } ) ;
expectStatus ( deleteSecondWork . response . status , 204 , '删除第二件作品' , deleteSecondWork . body ) ;
const emptyCollections = await request ( ` /api/projects/ ${ projectId } /collections ` , { headers :bearerHeaders } ) ;
const emptyCollection = ( emptyCollections . body as Array < { id :number ; status :string ; work_count :number } > ) . find ( ( item ) = > Number ( item . id ) === collectionId ) ;
if ( emptyCollection ? . status !== 'draft' || Number ( emptyCollection . work_count ) !== 0 ) throw new Error ( '删除最后一件作品后未回到待提交' ) ;
const revokeProjectKey = await request ( ` /api/management/api-keys/ ${ projectKeyId } ` , { method : 'DELETE' } , newAdminCookie ) ;
expectStatus ( revokeProjectKey . response . status , 204 , '吊销项目级 API Key' ) ;
2026-07-21 15:28:55 +08:00
const apiKey = await request ( '/api/management/api-keys' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { name : '运行时测试 Key' } ) } , adminCookie ) ;
expectStatus ( apiKey . response . status , 201 , '创建平台 API Key' ) ;
const keyId = Number ( ( apiKey . body as { item : { id : number } } ) . item . id ) ;
const revoke = await request ( ` /api/management/api-keys/ ${ keyId } ` , { method : 'DELETE' } , adminCookie ) ;
expectStatus ( revoke . response . status , 204 , '吊销平台 API Key' ) ;
2026-07-22 18:12:23 +08:00
process . stdout . write ( 'PostgreSQL 运行时验证通过: 账号角色、项目隔离、客户门禁、项目自动状态、完成只读、API Key 发现、externalId 幂等、COS 同源复用、异源转存、SSRF 拦截与单方案轮次\n' ) ;
2026-07-21 15:28:55 +08:00
} finally {
await new Promise < void > ( ( resolve , reject ) = > server . close ( ( error ) = > error ? reject ( error ) : resolve ( ) ) ) ;
await closeDatabase ( ) ;
}