This commit is contained in:
2026-08-08 15:25:46 +08:00
parent 7b2b208467
commit 931009a518
11 changed files with 10920 additions and 3 deletions
@@ -5,6 +5,21 @@ function xlsxOk(response) {
&& response.bytes[0] === 0x50 && response.bytes[1] === 0x4b
}
async function uploadTemplate(context, path, bytes, fileName) {
const form = new FormData()
form.append('file', new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), fileName)
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
body: form, signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let body = null
try { body = raw ? JSON.parse(raw) : null } catch { /* bounded evidence only */ }
return { method: 'POST', path, http: response.status, body, raw: body === null ? raw.slice(0, 500) : null,
requestId: body?.requestId || response.headers.get('x-request-id'), traceId: body?.traceId || response.headers.get('x-trace-id') }
} catch (error) { return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
}
export async function run(context) {
const module = 'master-data'
const suffix = context.marker.replace(/\D/g, '').slice(-12)
@@ -150,8 +165,30 @@ export async function run(context) {
const response = await context.requestBinary('GET', path)
context.addStep(module, `下载${name}导入模板`, response, xlsxOk(response),
`bytes=${response.bytes?.length ?? 0}, contentType=${response.contentType || 'missing'}`)
if (xlsxOk(response)) {
const importPath = `${path.replace('/import-template', '/import')}?tenantId=0&dryRun=true`
const first = await uploadTemplate(context, importPath, response.bytes, `${context.marker}-${name}.xlsx`)
const firstData = first.body?.data
context.addStep(module, `${name}合法模板 dryRun 导入契约`, first,
first.http === 200 && String(first.body?.code) === '200',
`HTTP=${first.http}, code=${first.body?.code ?? 'missing'}, rows=${firstData?.totalRows ?? 'n/a'}, parserSuccess=${firstData?.success ?? 'n/a'}`,
{ scenario: 'valid-import', deploymentBlocker: first.http === 404 || first.http === 405 || first.http === 503 })
const second = await uploadTemplate(context, importPath, response.bytes, `${context.marker}-${name}-replay.xlsx`)
context.addStep(module, `${name}合法模板重复 dryRun 幂等结果稳定`, second,
second.http === 200 && String(second.body?.code) === '200'
&& second.body?.data?.totalRows === firstData?.totalRows && second.body?.data?.importedRows === firstData?.importedRows,
`firstRows=${firstData?.totalRows ?? 'n/a'}, replayRows=${second.body?.data?.totalRows ?? 'n/a'}`,
{ scenario: 'valid-import', deploymentBlocker: second.http === 404 || second.http === 405 || second.http === 503 })
context.addCoverageGap(module, 'valid-import', `${name}带数据行的合法 XLSX 写入`,
'当前验收运行时无 XLSX 写入依赖;仅上传真实服务模板做解析/重复结果稳定性验证,未伪造数据行或写入主数据', 'runtime-dependency')
} else {
context.addCoverageGap(module, 'valid-import', `${name}合法 XLSX 导入`, '模板下载契约不可用,无法安全取得真实字段模板;未构造伪 XLSX', 'deployment')
}
}
context.addCoverageGap(module, 'optimistic-lock', '仓库/托盘/快递/往来单位旧 version 冲突',
'真实 DTO 未声明 version/expectedVersion 字段,不能伪造并发冲突断言;库位版本已由 supplemental-boundaries 覆盖', 'contract-gap')
const disableWarehouseResponse = await context.request('PATCH', `/api/platform/v1/warehouse-admin/${warehouseId}/status?tenantId=0&status=INACTIVE`)
context.apiSuccess(module, '停用测试仓库', disableWarehouseResponse)
}
@@ -0,0 +1,45 @@
const MODULE = 'service-center-flow'
const OPTIONS = { scenario: 'service-center-cross-flow', deploymentBlocker: true }
const OCEAN = '/api/platform/v1/ocean-bookings'
const TRUCK = '/api/platform/v1/truck-appointments'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
export async function run(context) {
const ocean = context.createdResources?.oceanBooking
if (!ocean?.id || ocean.finalStatus !== 'RELEASED') {
context.addStep(MODULE, '跨服务链路前置订舱终态可用', null, false,
'本轮海运订舱模块未返回 RELEASED 隔离记录,未伪造关联拖车预约', OPTIONS)
return
}
const suffix = context.marker.replace(/\D/g, '').slice(-10)
const customerId = Number(context.context?.customerId || 10001)
const payload = {
outboundOrderId: null, oceanBookingId: Number(ocean.id), containerBookingId: null, loadingPlanId: null,
warehouseId: Number(context.createdResources?.warehouse?.id || context.context?.warehouseId || 193000000000000001),
customerId, customerName: `服务中心链路客户-${suffix}`, carrierId: null, carrierName: `链路承运商-${suffix}`,
fleetContact: '链路车队联系人', fleetPhone: '13800000007', driverName: '链路司机', driverPhone: '13800000008',
driverIdCard: null, vehicleNo: `粤D-${suffix.slice(-6)}`, storeName: `链路门店-${suffix}`, storeContact: '链路联系人',
storePhone: '13800000009', storeAddress: '深圳市南山区链路地址', storeCity: '深圳市', storeZipcode: '518000',
packageCount: 1, grossWeightKg: 1, volumeCbm: 0.1, palletCount: 0, poNo: `FLOW-PO-${suffix}`,
plannedLoadingAt: '2026-09-03T08:00:00', transitMinutes: 60, payableAmount: 1, receivableAmount: 2,
currency: 'RMB', remark: context.marker, expectedVersion: 0,
}
const response = await context.request('POST', TRUCK, payload)
const appointment = context.apiSuccess(MODULE, '创建关联海运订舱的拖车预约', response, `oceanBookingId=${ocean.id}`, OPTIONS)
const appointmentId = id(appointment?.id)
context.assert(MODULE, '拖车预约关联订舱 ID、版本和审计字段完整', response,
Boolean(appointmentId) && id(appointment?.oceanBookingId) === id(ocean.id) && Number.isFinite(Number(appointment?.version))
&& Boolean(appointment?.createdAt || appointment?.createdBy || appointment?.operatorName),
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
if (!appointmentId) return
context.createdResources.serviceCenterFlow = { appointmentId, oceanBookingId: ocean.id, marker: context.marker, finalStatus: 'CANCELLED' }
const action = await context.request('POST', `${TRUCK}/${appointmentId}/cancel`, { expectedVersion: Number(appointment.version), reason: context.marker })
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.assert(MODULE, '跨服务链路状态历史存在', detail, Array.isArray(final?.histories) && final.histories.length >= 1, `histories=${final?.histories?.length ?? 'n/a'}`, OPTIONS)
}
@@ -89,8 +89,8 @@ export async function run(context) {
// Public APIs do not expose a safe way to create a real occupied pallet/
// location association without an inbound allocation fixture. Keep this as
// an explicit automation gap instead of claiming a deletion-protection pass.
context.addStep(MODULE, '托盘/库位真实占用删除保护自动化缺口', null, false,
'当前公共验收 API 未提供可安全构造并释放占用关联的独立 fixture;未操作历史资源', OPTIONS)
context.addCoverageGap(MODULE, 'resource-occupancy', '托盘/库位真实占用删除保护',
'当前公共验收 API 未提供可安全构造并释放占用关联的独立 fixture;未操作历史资源', 'no-public-fixture-api')
// Import boundary probes use only in-memory bytes and dry-run where supported.
const bogusXlsx = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x00, 0x00])