2026-10-20 14:22:55 +08:00
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
2026-10-21 15:53:33 +08:00
import { run , readAfterSynchronization } from './appointment-work.mjs'
test ( '同步就绪只重试明确协调状态,超限及其他错误原样保留' , async () => {
const coordinating = { http : 200 , body : { code : 'A0409' , message : '预约批次或运输关联已变化,服务端正在协调完整工作清单' }}
const success = { http : 200 , body : { code : '200' , data : {}}}
let calls = 0
assert . equal ( await readAfterSynchronization ({ request : async ()=> ++ calls === 1 ? coordinating : success }, '/summary' , { attempts : 3 , delay : async ()=>{}}), success )
assert . equal ( calls , 2 )
const scopeChanged = { http : 200 , body : { code : 'A0409' , message : '已开船工作范围变化,请先完成服务端同步' }}
calls = 0
assert . equal ( await readAfterSynchronization ({ request : async ()=> ++ calls === 1 ? scopeChanged : success }, '/summary' , { attempts : 3 , delay : async ()=>{}}), success )
assert . equal ( calls , 2 )
calls = 0
assert . equal ( await readAfterSynchronization ({ request : async ()=>{ calls ++ ; return coordinating }}, '/summary' , { attempts : 3 , delay : async ()=>{}}), coordinating )
assert . equal ( calls , 3 )
const other = { http : 500 , body : { code : 'S0500' }}
assert . equal ( await readAfterSynchronization ({ request : async ()=> other }, '/summary' , { delay : async ()=> assert . fail ( 'must not retry' )}), other )
const conflict = { http : 200 , body : { code : 'A0409' , message : '预约版本冲突' }}
assert . equal ( await readAfterSynchronization ({ request : async ()=> conflict }, '/summary' , { delay : async ()=> assert . fail ( 'must not retry unrelated conflict' )}), conflict )
})
2026-10-20 14:22:55 +08:00
const entry = readFileSync ( new URL ( '../admin-test-env-acceptance.mjs' , import . meta . url ), 'utf8' )
const moduleSource = readFileSync ( new URL ( './appointment-work.mjs' , import . meta . url ), 'utf8' )
function fakeContext ( overrides = {}) {
const calls = []
const gaps = []
const steps = []
const context = {
marker : 'ADMIN-API-OFFLINE' ,
calls ,
gaps ,
steps ,
async request ( method , path , body ) {
calls . push ({ method , path , body })
const url = new URL ( path , 'https://offline.invalid' )
if ( url . pathname . endsWith ( '/calendar' )) {
const from = url . searchParams . get ( 'from' )
const to = url . searchParams . get ( 'to' )
return { http : 200 , body : { code : '200' , data : { from , to , total : 2 , days : [
{ date : from , total : 1 , typeCounts : { DELIVERY : 1 } },
{ date : to , total : 1 , typeCounts : { PICKUP : 1 } },
] } } }
}
if ( url . pathname . endsWith ( '/summary' )) {
return { http : 200 , body : { code : '200' , data : { all : 2 , followToday : 1 , overdue : 0 , undated : 1 , review : 0 } } }
}
if ( url . pathname . endsWith ( '/items' )) {
return { http : 200 , body : { code : '200' , data : overrides . emptyPage
? { records : [], page : 1 , size : 20 , total : 0 , pages : 0 }
: { records : [{ workItemKey : 'WORK-1' , type : 'DELIVERY' , flow : 'TRANSFER' }], page : 1 , size : 20 , total : 1 , pages : 1 } } }
}
if ( url . pathname . includes ( '/items/WORK-1' )) {
return { http : 200 , body : { code : '200' , data : {
item : { workItemKey : 'WORK-1' , type : 'DELIVERY' , flow : 'TRANSFER' , containers : [{ containerBookingId : 'C-1' }], allowedActions : [] },
references : [], history : [],
} } }
}
if ( url . pathname . endsWith ( '/commands' )) {
return { http : 200 , body : { code : 'A0404' , message : '工作事项不存在' } }
}
throw new Error ( `unexpected offline request ${ method } ${ path } ` )
},
apiSuccess ( module , name , response ) {
assert . equal ( response . http , 200 )
assert . equal ( String ( response . body ? . code ), '200' )
steps . push ({ module , name , passed : true })
return response . body . data
},
assert ( module , name , response , condition , evidence ) {
assert . equal ( Boolean ( condition ), true , ` ${ name } : ${ evidence } ` )
steps . push ({ module , name , passed : true })
return true
},
addCoverageGap ( module , scenario , name , reason , category , metadata ) {
gaps . push ({ module , scenario , name , reason , category , metadata })
},
}
return context
}
test ( '预约工作模块在默认全量中注册且只读查询官方工作事项接口' , () => {
assert . match ( entry , /const CONFIGURED_MODULES = \[[^\n]*'appointment-work'/ )
assert . match ( moduleSource , /\/api\/platform\/v1\/last-leg\/appointment-work/ )
for ( const path of [ '/calendar' , '/summary' , '/items' , '/commands' ]) assert . match ( moduleSource , new RegExp ( path . replace ( '/' , '\\/' )))
assert . match ( moduleSource , /command: 'SAVE_FOLLOWUP'/ )
assert . doesNotMatch ( moduleSource , /this\.addStep|writeFileSync|execSync/ )
})
test ( '预约工作只读链按日历、汇总、分页、详情顺序调用,唯一写入是不存在事项的拒绝命令' , async () => {
const context = fakeContext ()
await run ( context )
assert . deepEqual ( context . calls . map (( row ) => row . method ), [ 'GET' , 'GET' , 'GET' , 'GET' , 'POST' ])
assert . ok ( context . calls [ 0 ]. path . includes ( '/calendar?' ))
assert . ok ( context . calls [ 1 ]. path . endsWith ( '/summary' ))
assert . ok ( context . calls [ 2 ]. path . includes ( '/items?view=ALL&page=1&size=20' ))
assert . ok ( context . calls [ 3 ]. path . includes ( '/items/WORK-1' ))
assert . ok ( context . calls [ 4 ]. path . includes ( '/items/FULL-ACCEPTANCE-MISSING-WORK-ITEM/commands' ))
assert . equal ( context . calls [ 4 ]. body . command , 'SAVE_FOLLOWUP' )
assert . ok ( context . gaps . some (( gap ) => gap . name === '预约写命令正向链路' && gap . metadata . blocking === false ))
})
test ( '无本轮工作事项时记录非阻断覆盖缺口而不是虚构详情或命令结果' , async () => {
const context = fakeContext ({ emptyPage : true })
await run ( context )
assert . equal ( context . calls . length , 3 )
assert . ok ( context . gaps . some (( gap ) => gap . name === '预约工作项详情与命令' && gap . metadata . blocking === false ))
})