This commit is contained in:
2026-08-08 20:36:23 +08:00
parent 57fcabe740
commit 0558f7ae59
8 changed files with 70 additions and 6 deletions
@@ -40,7 +40,19 @@ export async function run(context) {
const cancelled = context.apiSuccess(MODULE, '取消跨服务链路拖车预约', action, '', OPTIONS)
context.assert(MODULE, '跨服务链路安全终态为 CANCELLED', action, cancelled?.status === 'CANCELLED', `status=${cancelled?.status || 'missing'}`, OPTIONS)
const detail = await context.request('GET', `${TRUCK}/${appointmentId}`)
const final = context.apiSuccess(MODULE, '回查跨服务链路审计历史', detail, '', OPTIONS)
context.addCoverageGap(MODULE, 'service-center-cross-flow', '拖车预约状态历史独立查询',
'当前拖车预约 Controller 仅提供详情实体,没有公开 histories 查询字段/端点;已验证详情中的关联 ID、版本和审计时间字段', 'no-public-history-api')
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
final?.status === 'CANCELLED' && id(final?.oceanBookingId) === id(ocean.id),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}`, OPTIONS)
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
const history = context.apiSuccess(MODULE, '查询跨服务链路拖车预约状态历史', historyResponse, '', OPTIONS)
const cancel = Array.isArray(history) ? history.find((item) => item?.action === 'CANCEL') : null
context.assert(MODULE, '跨服务链路历史包含归属、状态、操作者、时间和备注', historyResponse,
Boolean(cancel) && id(cancel?.appointmentId) === appointmentId
&& cancel?.fromStatus === 'DRAFT' && cancel?.toStatus === 'CANCELLED'
&& Boolean(cancel?.operatorId) && Boolean(cancel?.operatorName) && Boolean(cancel?.createdAt)
&& cancel?.reason === context.marker,
`history=${history?.length ?? 'n/a'}, cancelFrom=${cancel?.fromStatus || 'missing'}, cancelTo=${cancel?.toStatus || 'missing'}`,
OPTIONS)
}
@@ -9,4 +9,20 @@ async function req(context, method, path, body, headers = {}) { try { const resp
function data(r) { return String(r?.body?.code) === '200' ? r.body.data : null }
function payload(context, suffix, version = 0) { return { outboundOrderId: null, oceanBookingId: null, containerBookingId: null, loadingPlanId: null, warehouseId: 193000000000000001, customerId: 10001, customerName: `拖车验收客户-${suffix}`, carrierId: null, carrierName: `承运商-${suffix}`, fleetContact: '车队联系人', fleetPhone: '13800000004', driverName: '司机验收', driverPhone: '13800000005', driverIdCard: null, vehicleNo: `粤B-${suffix.slice(-6)}`, storeName: `门店-${suffix}`, storeContact: '门店联系人', storePhone: '13800000006', storeAddress: '深圳市南山区验收地址', storeCity: '深圳市', storeZipcode: '518000', packageCount: 10, grossWeightKg: 100, volumeCbm: 2.5, palletCount: 1, poNo: `PO-${suffix}`, plannedLoadingAt: '2026-09-02T08:00:00', transitMinutes: 120, payableAmount: 100, receivableAmount: 150, currency: 'RMB', remark: context.marker, expectedVersion: version } }
export async function run(context) { const suffix = context.marker.replace(/\D/g, '').slice(-10); const list = await req(context, 'GET', `${BASE}?page=1&size=5&keyword=${encodeURIComponent(context.marker)}`); if (missing(list) || list.http === 0) { context.addStep(MODULE, '拖车预约服务契约已部署', list, false, `入口 ${BASE} 不可用:HTTP=${list.http}, code=${codeOf(list)}`, OPTIONS); return } const page = context.apiSuccess(MODULE, '拖车预约分页 keyword 查询', list, '', OPTIONS); context.assert(MODULE, '拖车预约分页结构正确', list, Array.isArray(page?.records), `records=${page?.records?.length ?? 'n/a'}`, OPTIONS); for (const status of ['DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED']) { const r = await req(context, 'GET', `${BASE}?page=1&size=5&status=${status}`); const d = data(r); context.addStep(MODULE, `拖车预约 ${status} 筛选`, r, r.http === 200 && codeOf(r) === '200' && Array.isArray(d?.records) && d.records.every((x) => x.status === status), `HTTP=${r.http}, code=${codeOf(r)}, records=${d?.records?.length ?? 'n/a'}`, OPTIONS) }
const createResponse = await req(context, 'POST', BASE, payload(context, suffix)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS); const appointmentId = id(appointment?.id); context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS); if (!appointmentId) return; context.createdResources.truckAppointment = { id: appointmentId, marker: context.marker, finalStatus: 'CANCELLED' }; const path = `${BASE}/${appointmentId}`; let version = Number(appointment.version); const edit = await req(context, 'PUT', path, { ...payload(context, suffix, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version); const stale = await req(context, 'PUT', path, { ...payload(context, suffix, 0), vehicleNo: `STALE-${suffix}` }); context.addStep(MODULE, '拖车预约旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409','409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`, OPTIONS); const after = await req(context, 'GET', path); const ad = data(after); context.apiSuccess(MODULE, '冲突后回查拖车预约', after, '', OPTIONS); context.assert(MODULE, '拖车预约冲突未部分更新', after, ad?.vehicleNo === edited?.vehicleNo, `vehicleNo=${ad?.vehicleNo || 'missing'}`, OPTIONS); version = Number(ad?.version); const action = async (name, endpoint, expected) => { const r = await req(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-${endpoint}` }); const d = context.apiSuccess(MODULE, name, r, '', OPTIONS); context.assert(MODULE, `${name} 状态返回`, r, Boolean(d?.status), `status=${d?.status || 'missing'}`, OPTIONS); version = Number(d?.version); return d }; for (const [name, endpoint] of [['提交预约','submit'],['派车','dispatch'],['开始提货','pickup'],['到仓','arrive'],['开始装货','load'],['离场','depart'],['完成','complete']]) await action(name, endpoint, version); const final = await req(context, 'GET', path); const fd = data(final); context.apiSuccess(MODULE, '回查 COMPLETED 拖车预约', final, '', OPTIONS); context.assert(MODULE, '拖车预约状态终态和费用快照正确', final, fd?.status === 'COMPLETED' && fd?.payableAmount != null && fd?.receivableAmount != null, `status=${fd?.status || 'missing'}`, OPTIONS); context.addCoverageGap(MODULE, 'truck-appointment-lifecycle', '附件引用安全边界', '当前 Controller 无附件上传/引用 API,未伪造附件成功', 'no-public-fixture-api') }
const createResponse = await req(context, 'POST', BASE, payload(context, suffix)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS); const appointmentId = id(appointment?.id); context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS); if (!appointmentId) return; context.createdResources.truckAppointment = { id: appointmentId, marker: context.marker, finalStatus: 'CANCELLED' }; const path = `${BASE}/${appointmentId}`; let version = Number(appointment.version); const edit = await req(context, 'PUT', path, { ...payload(context, suffix, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version); const stale = await req(context, 'PUT', path, { ...payload(context, suffix, 0), vehicleNo: `STALE-${suffix}` }); context.addStep(MODULE, '拖车预约旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409','409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`, OPTIONS); const after = await req(context, 'GET', path); const ad = data(after); context.apiSuccess(MODULE, '冲突后回查拖车预约', after, '', OPTIONS); context.assert(MODULE, '拖车预约冲突未部分更新', after, ad?.vehicleNo === edited?.vehicleNo, `vehicleNo=${ad?.vehicleNo || 'missing'}`, OPTIONS); version = Number(ad?.version); const action = async (name, endpoint, expected) => { const r = await req(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-${endpoint}` }); const d = context.apiSuccess(MODULE, name, r, '', OPTIONS); context.assert(MODULE, `${name} 状态返回`, r, Boolean(d?.status), `status=${d?.status || 'missing'}`, OPTIONS); version = Number(d?.version); return d }; for (const [name, endpoint] of [['提交预约','submit'],['派车','dispatch'],['开始提货','pickup'],['到仓','arrive'],['开始装货','load'],['离场','depart'],['完成','complete']]) await action(name, endpoint, version); const final = await req(context, 'GET', path); const fd = data(final); context.apiSuccess(MODULE, '回查 COMPLETED 拖车预约', final, '', OPTIONS); context.assert(MODULE, '拖车预约状态终态和费用快照正确', final, fd?.status === 'COMPLETED' && fd?.payableAmount != null && fd?.receivableAmount != null, `status=${fd?.status || 'missing'}`, OPTIONS)
const historyResponse = await req(context, 'GET', `${path}/history`)
const history = context.apiSuccess(MODULE, '查询拖车预约状态历史', historyResponse, '', OPTIONS)
const expectedActions = ['CREATE', 'EDIT', 'SUBMIT', 'DISPATCH', 'PICKUP', 'ARRIVE', 'LOAD', 'DEPART', 'COMPLETE']
const actualActions = Array.isArray(history) ? history.map((item) => item?.action) : []
context.assert(MODULE, '拖车预约历史动作和状态链完整', historyResponse,
Array.isArray(history) && expectedActions.every((value) => actualActions.includes(value))
&& history.every((item) => id(item?.appointmentId) === appointmentId
&& Boolean(item?.action) && Object.hasOwn(item, 'fromStatus') && Boolean(item?.toStatus)
&& Boolean(item?.operatorId) && Boolean(item?.operatorName) && Boolean(item?.createdAt)),
`history=${history?.length ?? 'n/a'}, actions=${actualActions.join(',')}`, OPTIONS)
const missingHistory = await req(context, 'GET', `${BASE}/999999999999999999/history`)
context.addStep(MODULE, '不存在预约不能读取状态历史', missingHistory,
failure(missingHistory), `HTTP=${missingHistory.http}, code=${codeOf(missingHistory)}`, OPTIONS)
}