This commit is contained in:
2026-10-21 15:53:33 +08:00
parent 94595c810b
commit 090591076f
41 changed files with 459 additions and 63 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
| 类别 | 正式入口 | 管理边界 |
|---|---|---|
| 本机重型检查限流 | `local-check.ps1` | 所有工作目录、对话共用两个用户级执行名额;PowerShell 7,状态/超时/并发与 Job Object 回收规则见外层 `docs/AI工程执行与协作约束.md`;只读状态不申请重型名额 |
| 后端分层检查 | `backend-check.ps1` | 默认指定模块回归,支持测试源码编译和精确测试类;必填模块,自动接入重型限流;全量触发条件见后端根指引,参数及副作用见 README |
| 后端分层检查 | `backend-check.ps1` | 局部任务显式选 Focused;模式缺省为 Module,不是日常必跑;必填模块并接入限流,范围见后端根指引,参数见 README |
| 测试/灰度单票验收 | `prod-release-acceptance.mjs`、`prod-release-delivery-stage.mjs`、内部 `prod-release-order-stage.mjs` | 显式 test/gray、统一 Gateway/accessToken;同一票义乌到洛杉矶、前置同时核对东莞;full-flow 串行覆盖买单FBA正常链至签收回传及车次资源释放,保留分阶段入口与独立检查点;不调用测试造数模块,不创建账号/主档或删除数据;配置和边界见 README |
| Admin 全量 HTTP 验证 | `admin-test-env-acceptance.mjs` | 通过显式环境文件运行统一 TEST Gateway/会话,拒绝旧海外网关参数;默认模块覆盖平台全链路,定向验证使用 `--modules`,模块注册和依赖以总入口及 `admin-test-env-modules/README.md` 为准 |
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 通过显式环境文件运行统一 TEST Gateway/会话,拒绝旧海外网关参数;覆盖客户工作台完整链路,`client-phase2-test-env-acceptance.mjs` 只是入库测量与计费专项,不替代全量入口 |
+3 -3
View File
@@ -458,13 +458,13 @@ Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块
.\scripts\backend-check.ps1 -Modules order -Mode Compile
# 开发迭代:精确类名,支持逗号分隔和全限定名。
.\scripts\backend-check.ps1 -Modules common-core -Mode Focused -Tests ApiResponseTest
# 单服务收尾:指定模块及上游依赖测试。
# 服务级影响且定向覆盖不足:指定模块及上游依赖测试。
.\scripts\backend-check.ps1 -Modules order
# 跨服务收尾:显式列出受影响生产者、消费者。
# 跨服务模块回归:显式列出受影响生产者、消费者。
.\scripts\backend-check.ps1 -Modules 'order,firstleg'
```
三种模式均使用根 reactor 的 `-pl <modules> -am`,不会安装制品到本地仓库,不默认 `clean`。`Compile` 运行 `test-compile`;`Focused`/`Module` 运行 `test`。`-am` 只补上游构建依赖,HTTP/事件下游消费者须由调用者显式列入。公共模块变更也须核对下游,不能只测试公共模块自身。Focused 只用于迭代,不能替代有行为变化时的模块收尾;API、Mapper、索引等相关静态合同仍按其规则单独执行。
三种模式均使用根 reactor 的 `-pl <modules> -am`,不会安装制品到本地仓库,不默认 `clean`。`Compile` 运行 `test-compile`;`Focused`/`Module` 运行 `test`。`-am` 只补上游构建依赖,HTTP/事件下游消费者须显式列入。局部逻辑使用 `Focused` 覆盖修改行为及必要边界即可交付,不因收尾改跑 `Module`;模块回归只用于定向覆盖不足的服务级影响。公共模块按实际消费者验证。模式缺省值为 `Module`,局部任务须显式选 `Focused`;API、Mapper、索引合同按各自条件执行。
Focused 不接受通配符或方法选择,每个类必须唯一命中所选模块的 `src/test/java`。为允许上游无同名测试,Maven 设置 `surefire.failIfNoSpecifiedTests=false`,随后逐类核对本轮更新的 Surefire XML;报告缺失、陈旧、全部跳过或含失败均不能通过,避免零测试假绿。
+2
View File
@@ -57,6 +57,8 @@ export function planA0LocationIsolation(locations, requiredSlots = 1,
}
const MODULE_DEPENDENCIES = new Map([
['inbound-weight-ocr', ['orders']],
['orders', ['merchant-qualification-fixture']],
['phase5-root-fulfillment', ['merchant-qualification-fixture']],
['pda-loading-link-regression', ['merchant-qualification-fixture']],
['phase4-overseas-return-followups', ['phase4-overseas-return-receipt']],
+3 -1
View File
@@ -4,7 +4,9 @@
固定托盘造数中,`inbound-measurement.mjs` 只复用 `TP-001`、`YW-TP-002` 指定池中的现有托盘;一只可复用,不要求两只同时存在,两只均缺失才阻断,不自动选择池外托盘。仅明确设置内部 `allowAnyExistingPallet` 的通用场景可读取当前仓库已有资源;两者均不创建托盘。
OCR模块必须通过`ADMIN_TEST_OCR_IMAGE`显式指定本轮图片,不依赖个人桌面路径。合成称重测试图只证明上传、识别响应及人工确认协议,不代表现场称重照片或设备验收。费用项目模块按受控编码验证112条正式基线,合法扩展项目保留并单独核对分页和筛选。散板普通状态接口仅验证取消及非法派车拒绝,原单须出现在待匹配需求池;需求池接车到签收另列覆盖缺口,不以手工跳状态冒充履约。
OCR模块必须通过`ADMIN_TEST_OCR_IMAGE`显式指定本轮图片,不依赖个人桌面路径;定向运行自动展开客户资质及订单前置。识别失败的空字段允许省略,但识别成功必须有有效重量与单位;人工修正和确认分别增加版本。合成称重测试图只证明上传、识别响应及人工确认协议,不代表现场称重照片或设备验收。费用项目模块按受控编码验证112条正式基线,合法扩展项目保留并单独核对分页和筛选。散板普通状态接口仅验证取消及非法派车拒绝,原单须出现在待匹配需求池;需求池接车到签收另列覆盖缺口,不以手工跳状态冒充履约。
预约工作查询在本轮业务写入后,仅对服务端明确返回的 A0409“预约批次或运输关联已变化,服务端正在协调完整工作清单”或“已开船工作范围变化,请先完成服务端同步”每 5 秒重读,最多等待 240 秒;超时保留失败,其他错误不重试。这是后台完整扫描的就绪等待,不是页面响应性能验收,也不触发同步写接口。
先运行 `node scripts/acceptance-overview.mjs`(后端根目录,无网络)查看当前默认与可选模块。范围来自真实注册表;本文件描述各模块用途与依赖,不代表所有模块默认执行或已通过。报告解读及跨端/专项边界见 [总索引](../README.md#先看范围再看证据),最新运行状态只查外层部署契约。
@@ -1,7 +1,26 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { run } from './appointment-work.mjs'
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')
@@ -1,3 +1,18 @@
import { setTimeout as sleep } from 'node:timers/promises'
export async function readAfterSynchronization(context, path, { attempts = 49, delay = () => sleep(5000) } = {}) {
let response
for (let attempt = 1; attempt <= attempts; attempt += 1) {
response = await context.request('GET', path)
const coordinating = response.http === 200 && String(response.body?.code) === 'A0409'
&& ['预约批次或运输关联已变化,服务端正在协调完整工作清单',
'已开船工作范围变化,请先完成服务端同步'].includes(response.body?.message)
if (!coordinating || attempt === attempts) return response
await delay()
}
return response
}
function count(value) {
const number = Number(value)
return Number.isFinite(number) ? number : NaN
@@ -15,7 +30,7 @@ export async function run(context) {
const from = shanghaiDate(-7)
const to = shanghaiDate(7)
const calendarResponse = await context.request('GET', `${base}/calendar?from=${from}&to=${to}`)
const calendarResponse = await readAfterSynchronization(context, `${base}/calendar?from=${from}&to=${to}`)
const calendar = context.apiSuccess(module, '查询预约工作日历', calendarResponse)
const days = Array.isArray(calendar?.days) ? calendar.days : []
const dayTotal = days.reduce((sum, day) => sum + Number(day?.total || 0), 0)
@@ -29,14 +44,14 @@ export async function run(context) {
`date=${day?.date || 'missing'},total=${day?.total ?? 'missing'},typeTotal=${typeTotal}`)
}
const summaryResponse = await context.request('GET', `${base}/summary`)
const summaryResponse = await readAfterSynchronization(context, `${base}/summary`)
const summary = context.apiSuccess(module, '查询预约工作汇总', summaryResponse)
const summaryFields = ['all', 'followToday', 'overdue', 'undated', 'review']
context.assert(module, '工作汇总五项计数完整', summaryResponse,
summaryFields.every((field) => Number.isFinite(Number(summary?.[field]))),
summaryFields.map((field) => `${field}=${summary?.[field] ?? 'missing'}`).join(','))
const pageResponse = await context.request('GET', `${base}/items?view=ALL&page=1&size=20`)
const pageResponse = await readAfterSynchronization(context, `${base}/items?view=ALL&page=1&size=20`)
const page = context.apiSuccess(module, '查询预约工作事项首页', pageResponse)
const records = Array.isArray(page?.records) ? page.records : []
const total = count(page?.total)
@@ -1,8 +1,10 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
import {
validRecognition, sameOptionalWeight, confirmedVersion,
confirmedNetSummary,
isTransientGatewayFailure,
normalizeWeightPalletOptionPage,
@@ -14,7 +16,22 @@ import {
workspaceWeightMatchesBaseline,
} from './inbound-weight-ocr.mjs'
test('OCR failure permits absent optional values but recognition requires a valid weight; correction and confirmation each advance version', () => {
assert.equal(validRecognition({ recognitionStatus: 'FAILED' }), true)
assert.equal(validRecognition({ recognitionStatus: 'RECOGNIZED' }), false)
assert.equal(validRecognition({ recognitionStatus: 'RECOGNIZED', weightValue: 45, weightUnit: 'KG', confidence: 0.9 }), true)
assert.equal(validRecognition({ recognitionStatus: 'RECOGNIZED', weightValue: 45, weightUnit: 'KG', confidence: 2 }), false)
assert.equal(sameOptionalWeight(undefined, null), true)
assert.equal(sameOptionalWeight(undefined, 0), false)
assert.equal(sameOptionalWeight(5, 6), false)
assert.equal(confirmedVersion(3, false), 4)
assert.equal(confirmedVersion(3, true), 5)
})
test('retries only transient gateway failures for the unauthenticated upload probe', () => {
const modules = normalizeModuleOrder(['inbound-weight-ocr'])
assert.ok(modules.indexOf('merchant-qualification-fixture') < modules.indexOf('orders'))
assert.ok(modules.indexOf('orders') < modules.indexOf('inbound-weight-ocr'))
assert.equal(isTransientGatewayFailure({ http: 0 }), true)
assert.equal(isTransientGatewayFailure({ http: 502 }), true)
assert.equal(isTransientGatewayFailure({ http: 503 }), true)
@@ -22,6 +22,22 @@ function closeNumber(actual, expected, tolerance = 0.0005) {
return Number.isFinite(Number(actual)) && Math.abs(Number(actual) - Number(expected)) < tolerance
}
export function validRecognition(result) {
if (!result || !OCR_STATUSES.has(result.recognitionStatus)) return false
if (result.confidence != null && (!Number.isFinite(Number(result.confidence)) || Number(result.confidence) < 0 || Number(result.confidence) > 1)) return false
if (result.recognitionStatus === 'FAILED') return result.weightValue == null
return result.weightValue != null && Number(result.weightValue) > 0
&& normalizedGrossKg(result.weightValue, result.weightUnit) != null
}
export function sameOptionalWeight(actual, expected) {
return actual == null || expected == null ? actual == null && expected == null : closeNumber(actual, expected)
}
export function confirmedVersion(version, hasAdjustment) {
return Number(version) + (hasAdjustment ? 2 : 1)
}
function palletSnapshotComplete(pallet) {
return Boolean(id(pallet?.palletId))
&& typeof pallet?.palletCode === 'string' && pallet.palletCode.length > 0
@@ -254,10 +270,7 @@ async function createUploadedResult(context, target, pallet, image, overrides =
Boolean(fileId) && typeof upload?.fileId === 'string',
`fileIdType=${typeof upload?.fileId}, status=${upload?.recognitionStatus ?? 'missing'}`, SCENARIO_OPTIONS)
context.assert(MODULE, 'OCR 上传响应字段完整且置信度受限', uploadResponse,
upload && OCR_STATUSES.has(upload.recognitionStatus)
&& Object.hasOwn(upload, 'weightValue') && Object.hasOwn(upload, 'weightUnit')
&& Object.hasOwn(upload, 'confidence')
&& (upload.confidence == null || (Number(upload.confidence) >= 0 && Number(upload.confidence) <= 1)),
validRecognition(upload),
`status=${upload?.recognitionStatus ?? 'missing'}, hasWeight=${upload?.weightValue != null}, confidence=${upload?.confidence ?? 'null'}`,
SCENARIO_OPTIONS)
if (!fileId) throw new Error('称重图片上传未返回 fileId,无法登记和清理测试资源')
@@ -269,8 +282,7 @@ async function createUploadedResult(context, target, pallet, image, overrides =
const retry = context.apiSuccess(MODULE, '重试当前称重图片 OCR', retryResponse,
`fileId=${fileId}`, SCENARIO_OPTIONS)
context.assert(MODULE, 'OCR 重试保持文件身份并返回识别契约', retryResponse,
id(retry?.fileId) === fileId && OCR_STATUSES.has(retry?.recognitionStatus)
&& Object.hasOwn(retry || {}, 'weightValue') && Object.hasOwn(retry || {}, 'confidence'),
id(retry?.fileId) === fileId && validRecognition(retry),
`sameFile=${id(retry?.fileId) === fileId}, status=${retry?.recognitionStatus ?? 'missing'}`,
SCENARIO_OPTIONS)
if (retry) recognized = retry
@@ -474,8 +486,8 @@ export async function run(context) {
const registeredRow = registered.workspace?.weightResults?.find((row) => id(row.id) === original.resultId)
context.assert(MODULE, '工作区保留本轮 OCR 原值且未覆盖历史结果', registered.response,
registeredRow && !baselineIds.has(original.resultId)
&& closeNumber(registeredRow.ocrWeightValue, original.result.ocrWeightValue)
&& registeredRow.ocrWeightUnit === original.result.ocrWeightUnit,
&& sameOptionalWeight(registeredRow.ocrWeightValue, original.result.ocrWeightValue)
&& (registeredRow.ocrWeightUnit ?? null) === (original.result.ocrWeightUnit ?? null),
`resultFound=${Boolean(registeredRow)}, baselineCount=${baselineIds.size}, currentCount=${registered.workspace?.weightResults?.length ?? 'n/a'}`,
SCENARIO_OPTIONS)
@@ -506,7 +518,7 @@ export async function run(context) {
const confirmedTare = Number(confirmed?.palletTareWeightKg)
const confirmedNet = Number(confirmed?.netWeightKg)
context.assert(MODULE, '确认结果返回托盘快照、毛重和去皮净重', confirmResponse,
confirmed?.recognitionStatus === 'CONFIRMED' && Number(confirmed?.version) === original.version + 1
confirmed?.recognitionStatus === 'CONFIRMED' && Number(confirmed?.version) === confirmedVersion(original.version, !canConfirmOriginal)
&& id(confirmed?.palletId) === id(pallet.palletId)
&& confirmed?.palletCode === pallet.palletCode && confirmed?.palletType === pallet.palletType
&& closeNumber(confirmed?.palletCapacityCbm, pallet.capacityCbm)
@@ -53,7 +53,7 @@ test('商户夹具通过正式 File 和 IAM 审核流程建立下单资格', ()
assert.match(fixture, /MERCHANT_TEST_PASSWORD/)
assert.match(fixture, /context\.options\?\.customerName/)
assert.match(fixture, /tenantName/)
assert.match(fixture, /expectedProcurementPrefix: customerName \? null : 'QLYSXDKH'/)
assert.doesNotMatch(fixture, /expectedProcurementPrefix/)
assert.match(acceptance, /administrator-phone-availability\?phone=/)
assert.match(acceptance, /attempt < 100/)
assert.doesNotMatch(acceptance, /phoneSequence % 10/)
@@ -120,10 +120,8 @@ export async function run(context, { publishEnvironment = true } = {}) {
}
context.context.merchantQualificationFixture = {
tenantId, tenantName, username, password, marker: context.marker,
expectedProcurementPrefix: customerName ? null : 'QLYSXDKH',
}
context.createdResources.merchantQualificationFixture = {
tenantId, tenantName, username, marker: context.marker,
expectedProcurementPrefix: customerName ? null : 'QLYSXDKH',
}
}
@@ -111,4 +111,8 @@ test('船期日历模块注册为默认模块并核对月历、渠道、模板
assert.ok(ctx.calls[3].path.includes('/sailing-assignments/exceptions'))
assert.ok(ctx.calls[4].path.endsWith('/11'))
assert.ok(ctx.calls[5].path.endsWith('/999999999999999'))
delete sailing.etaAt
await runSailingCalendar(ctx)
sailing.etaAt = 'invalid-eta'
await assert.rejects(runSailingCalendar(ctx), /月历记录、汇总与筛选选项结构完整/)
})
@@ -103,7 +103,7 @@ export async function run(context) {
const clearance = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/clearance`)
context.assert(MODULE, '国内未出仓时目的港清关不生成国外工作台数据', clearance,
clearance.http === 200 && String(clearance?.body?.code) === 'A0404'
&& String(clearance?.body?.message || '').includes('国外工作台货柜不存在'),
&& clearance?.body?.message === '货柜尚未离仓或实际开船,不能维护目的港清关资料',
`HTTP=${clearance.http}, code=${clearance?.body?.code ?? 'missing'}, containerBookingId=${containerId}`, OPTIONS)
const customs = await context.request('GET', `/api/platform/v1/first-leg/loading-lists/${encodeURIComponent(planId)}/customs?warehouseId=${encodeURIComponent(warehouseId)}`)
if (fixture?.finalStatus === 'LOADED') {
@@ -55,6 +55,7 @@ test('overview regression requires workflowStatus and distinguishes 100 percent
test('pre-departure overview rejects destination workbench documents explicitly', () => {
assert.match(overviewAcceptance, /国内未出仓时 \$\{documentType\} 不生成国外工作台数据/)
assert.match(overviewAcceptance, /国内未出仓时目的港清关不生成国外工作台数据/)
assert.match(overviewAcceptance, /货柜尚未离仓或实际开船,不能维护目的港清关资料/)
assert.match(overviewAcceptance, /国外工作台货柜不存在/)
})
@@ -207,7 +207,8 @@ function fixture(options = {}) {
test('module is context injected, blocks missing prerequisites and excludes forbidden surfaces', () => {
assert.match(source, /export async function run\(context\)/)
assert.doesNotMatch(source, /https?:\/\/|process\.env|fetch\(/)
assert.doesNotMatch(source, /addCoverageGap|coverageGap\(/)
assert.equal((source.match(/addCoverageGap\(/g) || []).length, 1)
assert.match(source, /looseboard-pool-fulfillment/)
assert.match(source, /deploymentBlocker: true/)
assert.match(source, /contactPhone: '\d+'/)
assert.doesNotMatch(source, /contactPhone: '\+[^']+'/)
@@ -5,6 +5,28 @@ import test from 'node:test'
import { run, vehicleCapacityMatches } from './phase4-overseas-master-data.mjs'
import { run as runCare } from './fleet-vehicle-care.mjs'
test('平台用户手机号已存在只换号重试,未知创建结果不得重发', async () => {
for (const collision of [true, false]) {
const { context } = fakeContext({ softAssertions: true })
const original = context.request.bind(context)
const phones = []
let sequence = 0
context.findAvailablePhone = async () => `1380000${String(++sequence).padStart(4, '0')}`
context.request = async (method, path, body, ...rest) => {
if (method === 'POST' && path === '/api/platform/v1/iam/users') {
phones.push(body.phone)
if (phones.length === 1) return collision
? {http:200,body:{code:'A0409',message:'手机号已存在'}}
: {http:500,body:{code:'S0500'}}
}
return original(method,path,body,...rest)
}
await run(context)
assert.equal(phones.length, collision ? 2 : 1)
if (collision) assert.notEqual(phones[0],phones[1])
}
})
test('运维专项接受NON_NULL真实响应但仍完成全部六类记录和两车停用', async () => {
const { context, state } = fakeContext()
const request = context.request.bind(context)
@@ -375,13 +375,19 @@ async function verifyCanonicalPermissionDenial(context, resource) {
if (!roleId) throw new Error('无主数据权限角色未返回ID')
resource.permissionProbe = { roleId, membershipId: null, roleStatus: 'ACTIVE', userStatus: null }
const phone = await context.findAvailablePhone(MODULE)
const username = `p4_md_deny_${suffix}`.slice(0, 64).toLowerCase()
const password = `P4md${suffix.slice(-8)}A1!`
const userResponse = await context.request('POST', '/api/platform/v1/iam/users', {
username, displayName: `四期主数据拒绝-${suffix}`, phone,
email: `${username}@example.com`, tenantId: '0', roleIds: [roleId], password, status: 'ACTIVE',
})
let userResponse
for (let attempt = 0; attempt < 3; attempt += 1) {
const phone = await context.findAvailablePhone(MODULE)
userResponse = await context.request('POST', '/api/platform/v1/iam/users', {
username, displayName: `四期主数据拒绝-${suffix}`, phone,
email: `${username}@example.com`, tenantId: '0', roleIds: [roleId], password, status: 'ACTIVE',
})
// 客户管理员候选检查不覆盖平台用户;仅明确未创建的手机号冲突允许换号。
if (!(userResponse.http === 200 && String(userResponse.body?.code) === 'A0409'
&& userResponse.body?.message === '手机号已存在')) break
}
const user = requireData(context, '创建无主数据权限用户', userResponse)
const membershipId = asId(user?.membershipId)
if (!membershipId) throw new Error('无主数据权限用户未返回membershipId')
@@ -10,9 +10,10 @@ const pda = readFileSync(new URL('./phase4-overseas-pda.mjs', import.meta.url),
test('采购单号按北京时间日期执行精确合同', () => {
const instant = new Date('2026-09-05T16:30:00.000Z')
assert.equal(beijingDate(instant), '20260906')
assert.equal(matchesProcurementNo('QLYSXDKH202609060001', 'QLYSXDKH', instant), true)
assert.equal(matchesProcurementNo('QLYSXDKH202609050001', 'QLYSXDKH', instant), false)
assert.equal(matchesProcurementNo('QLYSXDKH20260906001', 'QLYSXDKH', instant), false)
assert.equal(matchesProcurementNo('CG202609060001', instant), true)
assert.equal(matchesProcurementNo('CG202609050001', instant), false)
assert.equal(matchesProcurementNo('CG20260906001', instant), false)
assert.equal(matchesProcurementNo('QLYSXDKH202609060001', instant), false)
})
test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释放', () => {
@@ -28,9 +28,8 @@ export function beijingDate(now = new Date()) {
return `${parts.year}${parts.month}${parts.day}`
}
export function matchesProcurementNo(orderNo, prefix, now = new Date()) {
return typeof prefix === 'string' && prefix.length > 0
&& new RegExp(`^${prefix}${beijingDate(now)}\\d{4}$`).test(String(orderNo || ''))
export function matchesProcurementNo(orderNo, now = new Date()) {
return new RegExp(`^CG${beijingDate(now)}\\d{4}$`).test(String(orderNo || ''))
}
async function poll(action, predicate, attempts = 60, intervalMs = 1_000) {
@@ -133,8 +132,8 @@ async function procurementFlow(context, fixture, token, warehouseId) {
customerName: fixture.tenantName, customerToken: token, warehouseId, upload,
readWallet: async () => { const result = await platformAccount(context, customerId); if (!success(result.response) || !result.account) throw new Error('本轮客户钱包查询失败'); return result.account } })
assert(context, '采购正常全链路及资金完成', null, proof.status === 'PASSED', `orderNo=${proof.orderNo}`)
assert(context, '采购单号日期和客户前缀正确', null,
fixture.expectedProcurementPrefix ? matchesProcurementNo(proof.orderNo, fixture.expectedProcurementPrefix) : /^[A-Z0-9]{1,8}\d{8}\d{4}$/.test(proof.orderNo), `orderNo=${proof.orderNo}`)
assert(context, '采购单号为CG加北京时间日期和流水', null,
matchesProcurementNo(proof.orderNo), `orderNo=${proof.orderNo}`)
return { orderNo: proof.orderNo, walletBusinessNos: proof.walletBusinessNos, settledAmountCny: proof.settledAmountCny }
} finally {
for (const check of context.createdResources.procurementSample?.checks || []) assert(context, check.label, null, check.passed)