import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' 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) }) 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)) })