test: stabilize full development acceptance
This commit is contained in:
+11
-6
@@ -10,34 +10,39 @@
|
||||
|
||||
| 类别 | 正式入口 | 管理边界 |
|
||||
|---|---|---|
|
||||
| Admin 全量 HTTP 验证 | `admin-test-env-acceptance.mjs` | 默认模块覆盖平台全链路;定向验证使用 `--modules`,模块注册和依赖以总入口及 `admin-test-env-modules/README.md` 为准 |
|
||||
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 客户工作台完整链路;`client-phase2-test-env-acceptance.mjs` 只是入库测量与计费专项,不替代全量入口 |
|
||||
| Admin 全量 HTTP 验证 | `admin-test-env-acceptance.mjs` | 通过显式环境文件运行开发或测试环境;默认模块覆盖平台全链路,定向验证使用 `--modules`,模块注册和依赖以总入口及 `admin-test-env-modules/README.md` 为准 |
|
||||
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 通过显式环境文件运行开发或测试环境;覆盖客户工作台完整链路,`client-phase2-test-env-acceptance.mjs` 只是入库测量与计费专项,不替代全量入口 |
|
||||
| Client 快速下单定向验证 | `client-quick-order-test-env-acceptance.mjs` | 使用既有测试客户通过批量草稿接口验证 FBA、海外仓、私人地址、逐行失败和幂等重放;只创建草稿,不提交或审核,不替代 Client 全量入口 |
|
||||
| 待审核订单造数 | `create-pending-review-orders.mjs` | 使用已配置客户创建并提交订单,停在待审核;不审核、不推进后续履约 |
|
||||
| 批量待分柜造数 | `it-test-wait-split-bulk.mjs` | 固定“IT测试”和“义乌仓”,随机客户可选真实线路并尽力推进到 `WAIT_SPLIT`;失败记录后继续,不自动删除成功数据;只复用 `TP-001`/`YW-TP-002`,不新增托盘或库位,普通位不足落 `A0` |
|
||||
| Admin HTTP 内部模块 | `admin-test-env-modules/*.mjs` | 由 Admin 总入口动态加载,除文档明确的独立编排器外不得当作独立全量入口;`*contract.test.mjs` 只做离线合同 |
|
||||
| 入仓测量与整票库位块专项 | `admin-test-env-modules/inbound-measurement.mjs`、`admin-test-env-modules/README.md` | 仍从 Admin 总入口按 `orders,inbound-measurement` 依赖链执行;开启 `--verify-pallet-location-guide true` 后逐张确认回查块扩容只能追加,既有槽身份不得漂移,并验证删除图片后空槽保留与替代图片复用;会写测试环境,必须有当前轮授权 |
|
||||
| 入仓测量与整票库位块专项 | `admin-test-env-modules/inbound-measurement.mjs`、`admin-test-env-modules/README.md` | 仍从 Admin 总入口按 `orders,inbound-measurement` 依赖链执行;开启 `--verify-pallet-location-guide true` 后逐张确认回查块扩容只能追加,既有槽身份不得漂移,并验证删除图片后空槽保留与替代图片复用;会写目标环境,必须有对应环境授权 |
|
||||
| Finance 扩展 HTTP 模块 | `admin-test-env-modules/firstleg-container-fee-allocation.mjs`、`finance-direct-paid-isolation.mjs`、`phase4-overseas-finance-cross-region.mjs` | 分别验证柜费跨客户分摊、海外仓服务直接已付隔离、五类海外费用跨区入池;必须由 Admin 总入口按已登记依赖运行,使用本轮资源且禁止借历史数据,真实 HTTP 仍须当前轮授权 |
|
||||
| 订舱与拖车服务中心链路 | `admin-test-env-modules/service-center-flow.mjs` | 复用本轮 `RELEASED` 订舱,创建订舱级拖车并验证附件、完整状态链、历史和稳定关联;由 Admin 总入口按依赖运行,会写测试环境 |
|
||||
| 订舱与拖车服务中心链路 | `admin-test-env-modules/service-center-flow.mjs` | 复用本轮 `RELEASED` 订舱,创建订舱级拖车并验证附件、完整状态链、历史和稳定关联;由 Admin 总入口按依赖运行,会写目标环境 |
|
||||
| 定向 HTTP/诊断 | 以 `README.md` 的“仓库级入口清单”为准 | 必须明确是只读诊断还是会补证据、创建资源或推进状态,不能用诊断成功冒充全量通过 |
|
||||
| 本地质量与静态合同 | `backend-full-regression.ps1`、`frontend-full-regression.ps1`、`mapper-scan-contract.mjs`、`script-documentation-contract.test.mjs` 等 | 默认不访问测试环境;数据库非 dry-run、Nacos 发布等有写入能力的入口必须按 README 单独识别 |
|
||||
| 本地质量与静态合同 | `backend-full-regression.ps1`、`frontend-full-regression.ps1`、`mapper-scan-contract.mjs`、`script-documentation-contract.test.mjs` 等 | 默认不访问开发或测试环境;数据库非 dry-run、Nacos 发布等有写入能力的入口必须按 README 单独识别 |
|
||||
| 部署与基础设施 | `../deploy/scripts/`、`../deploy/rocketmq/` | 靠近部署组件管理,不移动到根脚本目录;部署、初始化数据库或发布配置必须获得当前轮明确授权 |
|
||||
| 服务局部合同 | `../firstleg/scripts/`、`../lastleg-service/scripts/`、`../warehouse/scripts/` | 只服务所属模块,入口与命令登记在服务 `AGENTS.md` 或局部 README;不得散落到 `src/test/scripts/` |
|
||||
|
||||
## 新增与修改规则
|
||||
|
||||
- 跨服务编排、全仓质量检查、测试环境验收和通用诊断放在根 `scripts/`;只理解单一服务源码或数据的脚本放在 `<service>/scripts/`。
|
||||
- 跨服务编排、全仓质量检查、开发/测试环境验收和通用诊断放在根 `scripts/`;只理解单一服务源码或数据的脚本放在 `<service>/scripts/`。
|
||||
- 可直接运行的脚本必须在 `README.md` 登记:用途、目标环境、是否写数据、凭据来源、报告位置、失败/重跑语义和清理边界。内部模块必须登记到 `admin-test-env-modules/README.md`,并按需同步总入口模块注册及依赖展开。
|
||||
- 文件命名使用稳定业务语义:离线 Node 合同以 `.test.mjs` 结尾;环境写入入口显式包含 `test-env` 或在 README 中标记目标环境;fixture/verify 必须区分准备写入和只读核验。
|
||||
- 不在源码、AGENTS、README 或报告中写入密码、Token、私钥、真实客户数据和固定环境密钥;环境地址与账号从入口已支持的参数或本机环境配置读取。
|
||||
- 开发与测试验收配置分别只维护在 `$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env`、`$env:USERPROFILE\.codex\chongchong-global-test-acceptance.env`。脚本不得自动猜测环境或读取另一份文件;运行者必须通过位于脚本路径之前的 `node --env-file="<明确文件>"` 选择环境。
|
||||
- 开发目录只使用开发环境文件和开发域名,测试目录只使用测试环境文件和测试域名;不得跨目录复制报告、fixture 或未提交脚本来替代正常分支合并。
|
||||
- 不直接写业务数据库伪造 HTTP 验收成功,不吞掉 `deploymentBlocker`、业务错误码、`requestId` 或 `traceId`。造数脚本的部分成功数据默认保留,清理、重置或删除必须另行授权。
|
||||
- 不默认并发运行共享环境造数。只有用户明确要求并确认序号、报告目录和服务容量边界时,才允许拆分 worker;每个 worker 必须使用不重叠序号和独立报告目录。
|
||||
- 修改公共参数、上下文或模块签名时,定向检查总入口、生产模块、fixture、合同测试、README 和调用命令,禁止保留旧参数兼容分支。
|
||||
- 模块依赖必须由 `normalizeModuleOrder()` 和依赖注册统一展开;定向执行也必须获得与默认全量相同的生产者 fixture,缺少前置不得借历史数据或把 coverage gap 当成功。
|
||||
- Finance 默认链固定为 `finance-full-fixtures → finance-reconciliation-full → finance-settlement-full → finance-direct-paid-isolation → finance-downloads`。直接已付隔离必须在结算模块产出本轮收付款成功结果后执行;后续动作必须回查实时详情,不得按 fixture 创建时的陈旧状态判断。
|
||||
- 静态合同应锁定业务语义和稳定接口,不依赖无关排版或已改名文档路径;正式代码、文档目录、SQL 入口、Feign/HTTP 客户端或 Maven 拓扑变化时,同步更新相应枚举基线并核对新增项的访问范围、幂等和失败语义,不得仅放宽断言消除失败。
|
||||
|
||||
## 最低验证
|
||||
|
||||
- 修改单个 Node 脚本先运行 `node --check <script>`;PowerShell 使用解析检查或该脚本声明的 `-DryRun`,Shell/Groovy 使用现有离线合同,不为验证触发部署。
|
||||
- 修改 Admin 模块时运行直接对应的 `*contract.test.mjs`;修改造数链时同时运行 `admin-test-env-modules/it-test-wait-split-bulk-contract.test.mjs` 等受影响合同。
|
||||
- 宣称脚本“全量补齐”前,必须运行 `scripts/` 根级离线合同与 `admin-test-env-modules/*contract.test.mjs`,并分别报告测试文件数、断言通过/失败数和精确失败文件;历史真实 HTTP 报告全绿不能覆盖当前离线合同失败。
|
||||
- 新增、移动或删除脚本后运行 `node --test scripts/script-documentation-contract.test.mjs`,确认目录已登记且所有直接入口都有说明。
|
||||
- 修改任一 `AGENTS.md` 后从后端根运行 `./scripts/agents-index-contract.ps1`;真实 HTTP、数据库、Nacos、Jenkins 和部署验证仍须当前轮明确授权。
|
||||
|
||||
@@ -15,8 +15,8 @@ test('默认全量按新财务链执行直接收付款隔离且不包含旧 fina
|
||||
const modules = [...match[1].matchAll(/'([^']+)'/g)].map((item) => item[1])
|
||||
const financeStart = modules.indexOf('finance-full-fixtures')
|
||||
assert.deepEqual(modules.slice(financeStart, financeStart + 5), [
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-direct-paid-isolation',
|
||||
'finance-settlement-full', 'finance-downloads',
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full',
|
||||
'finance-direct-paid-isolation', 'finance-downloads',
|
||||
])
|
||||
assert.equal(modules.includes('finance'), false)
|
||||
assert.match(source, /fullFinanceChain/)
|
||||
@@ -24,11 +24,16 @@ test('默认全量按新财务链执行直接收付款隔离且不包含旧 fina
|
||||
const normalized = normalizeModuleOrder(modules)
|
||||
const normalizedFinanceStart = normalized.indexOf('finance-full-fixtures')
|
||||
assert.deepEqual(normalized.slice(normalizedFinanceStart, normalizedFinanceStart + 5), [
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-direct-paid-isolation',
|
||||
'finance-settlement-full', 'finance-downloads',
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full',
|
||||
'finance-direct-paid-isolation', 'finance-downloads',
|
||||
])
|
||||
})
|
||||
|
||||
test('binary GET retries transient transport failures without retrying writes', () => {
|
||||
assert.match(source, /async requestBinary\([\s\S]*?const maxAttempts = \['GET', 'HEAD'\]/)
|
||||
assert.match(source, /async requestBinary\([\s\S]*?attempt < maxAttempts/)
|
||||
})
|
||||
|
||||
test('multipart request preserves FormData boundary and unified response evidence', () => {
|
||||
assert.match(source, /async requestMultipart\(/)
|
||||
assert.match(source, /form\.append\('file'/)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-direct-paid-isolation', 'finance-settlement-full', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'customer-operating-statistics']
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'customer-operating-statistics']
|
||||
const OPTIONAL_MODULES = ['manual-order-splitting-fixture', 'manual-order-splitting-verify',
|
||||
'manual-preload-push-fixture', 'manual-preload-push-verify',
|
||||
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
|
||||
@@ -80,7 +80,7 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['phase4-carrier-nodes', ['phase4-overseas-pda-operations']],
|
||||
['phase4-overseas-business', ['merchant-qualification-fixture']],
|
||||
['firstleg-container-fee-allocation', []],
|
||||
['finance-direct-paid-isolation', ['finance-full-fixtures', 'finance-reconciliation-full']],
|
||||
['finance-direct-paid-isolation', ['finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full']],
|
||||
['phase4-overseas-finance-cross-region', ['phase4-overseas-business', 'phase4-overseas-pda-operations']],
|
||||
])
|
||||
|
||||
@@ -189,7 +189,7 @@ export function normalizeModuleOrder(modules) {
|
||||
if (missing.length > 0) normalized.splice(targetIndex, 0, ...missing)
|
||||
}
|
||||
const financeChain = ['finance-full-fixtures', 'finance-reconciliation-full',
|
||||
'finance-direct-paid-isolation', 'finance-settlement-full']
|
||||
'finance-settlement-full', 'finance-direct-paid-isolation']
|
||||
const firstFinanceIndex = normalized.findIndex((name) => financeChain.includes(name))
|
||||
if (firstFinanceIndex >= 0) {
|
||||
const prerequisites = ['merchant-qualification-fixture', 'orders', 'inbound-measurement', 'phase3-container-loading']
|
||||
@@ -639,23 +639,31 @@ export class AcceptanceRun {
|
||||
|
||||
async requestBinary(method, path) {
|
||||
const headers = { Accept: '*/*', Authorization: `Bearer ${this.token}` }
|
||||
try {
|
||||
const response = await fetch(`${this.options.gateway}${path}`, { method, headers, signal: AbortSignal.timeout(120_000) })
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
let payload = null
|
||||
if (contentType.toLowerCase().includes('json')) {
|
||||
try { payload = JSON.parse(new TextDecoder().decode(bytes)) } catch { /* Invalid envelope remains binary evidence. */ }
|
||||
const maxAttempts = ['GET', 'HEAD'].includes(String(method).toUpperCase()) ? 3 : 1
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(`${this.options.gateway}${path}`, { method, headers, signal: AbortSignal.timeout(120_000) })
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
let payload = null
|
||||
if (contentType.toLowerCase().includes('json')) {
|
||||
try { payload = JSON.parse(new TextDecoder().decode(bytes)) } catch { /* Invalid envelope remains binary evidence. */ }
|
||||
}
|
||||
return {
|
||||
method, path, http: response.status, body: payload, bytes, contentType,
|
||||
raw: payload ? null : `${bytes.length} bytes (${contentType || 'unknown'})`,
|
||||
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
||||
traceId: payload?.traceId || response.headers.get('x-trace-id'),
|
||||
}
|
||||
} catch (error) {
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 250))
|
||||
continue
|
||||
}
|
||||
return { method, path, http: 0, body: null, bytes: new Uint8Array(), contentType: '', raw: String(error), requestId: null, traceId: null }
|
||||
}
|
||||
return {
|
||||
method, path, http: response.status, body: payload, bytes, contentType,
|
||||
raw: payload ? null : `${bytes.length} bytes (${contentType || 'unknown'})`,
|
||||
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
||||
traceId: payload?.traceId || response.headers.get('x-trace-id'),
|
||||
}
|
||||
} catch (error) {
|
||||
return { method, path, http: 0, body: null, bytes: new Uint8Array(), contentType: '', raw: String(error), requestId: null, traceId: null }
|
||||
}
|
||||
throw new Error('unreachable binary request attempt state')
|
||||
}
|
||||
|
||||
async requestOverseasBinary(method, path) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { buildDirectPaidBody, findDirectPaidCost, findOpenSettlement } from './f
|
||||
const source = await readFile(new URL('./finance-direct-paid-isolation.mjs', import.meta.url), 'utf8')
|
||||
const acceptance = await readFile(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('直接收付款幂等在全量结清模块消费 AR/AP 之前执行', () => {
|
||||
test('直接收付款幂等在全量结算模块产生收付款结果之后执行', () => {
|
||||
const modules = acceptance.match(/const CONFIGURED_MODULES = \[([^\n]+)\]/)?.[1] || ''
|
||||
assert.ok(modules.indexOf("'finance-reconciliation-full'") < modules.indexOf("'finance-direct-paid-isolation'"))
|
||||
assert.ok(modules.indexOf("'finance-direct-paid-isolation'") < modules.indexOf("'finance-settlement-full'"))
|
||||
assert.ok(modules.indexOf("'finance-reconciliation-full'") < modules.indexOf("'finance-settlement-full'"))
|
||||
assert.ok(modules.indexOf("'finance-settlement-full'") < modules.indexOf("'finance-direct-paid-isolation'"))
|
||||
})
|
||||
|
||||
test('DIRECT_PAID 请求只绑定客户、仓库、供应商且不伪造业务关联', () => {
|
||||
|
||||
@@ -23,8 +23,9 @@ test('八类 Finance XLSX 契约均有正式表头并在完整链末尾执行',
|
||||
])
|
||||
assert.ok(Object.values(EXPORT_SPECS).every((headers) => headers.length >= 9))
|
||||
const order = normalizeModuleOrder(['finance-downloads', 'finance-settlement-full'])
|
||||
assert.deepEqual(order.slice(-4), [
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads',
|
||||
assert.deepEqual(order.slice(-5), [
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full',
|
||||
'finance-direct-paid-isolation', 'finance-downloads',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ test('字段断言接入正式生产模块和默认全量入口', () => {
|
||||
})
|
||||
|
||||
test('矩阵登记 LANDED 与 UNKNOWN 且禁止 fixture 伪造未知字段', () => {
|
||||
const matrix = read(workspace, 'docs', '三期', '货柜履约原型字段验收矩阵-2026-08-17.md')
|
||||
const matrix = read(workspace, 'docs', '国内货柜履约', '货柜履约原型字段验收矩阵-2026-08-17.md')
|
||||
assert.match(matrix, /LANDED/)
|
||||
assert.match(matrix, /UNKNOWN/)
|
||||
assert.match(matrix, /不得由 fixture 伪造|不生成假值/)
|
||||
|
||||
@@ -28,13 +28,14 @@ test('随机线路只固定义乌仓并从客户真实可计价候选选择', ()
|
||||
assert.doesNotMatch(runner, /channel-name|channel-code/)
|
||||
})
|
||||
|
||||
test('称重只随机复用义乌仓原有两个托盘且禁止按订单新增', () => {
|
||||
test('称重优先复用固定托盘并允许为无基线仓创建本轮托盘', () => {
|
||||
assert.match(measurement, /REUSABLE_PALLET_CODES = Object\.freeze\(\['TP-001', 'YW-TP-002'\]\)/)
|
||||
assert.match(measurement, /context\.request\('GET',[\s\S]*\/api\/platform\/v1\/pallets\?page=1&size=20/)
|
||||
assert.match(measurement, /randomlyReusePalletIds\(candidates, palletCount/)
|
||||
assert.match(measurement, /随机复用原有称重托盘/)
|
||||
assert.doesNotMatch(measurement, /context\.request\('POST', '\/api\/platform\/v1\/pallets/)
|
||||
assert.doesNotMatch(measurement, /创建称重托盘主数据/)
|
||||
assert.match(measurement, /context\.request\('POST', '\/api\/platform\/v1\/pallets\?tenantId=0'/)
|
||||
assert.match(measurement, /创建本轮称重托盘/)
|
||||
assert.match(measurement, /measurement-fixture/)
|
||||
})
|
||||
|
||||
test('称重脚本不新增库位且容量不足由仓库服务复用 A0 公共位', () => {
|
||||
|
||||
@@ -31,9 +31,9 @@ test('DRAFT can be partial but release validates and freezes the five transport
|
||||
assert.match(fixture, new RegExp(`${field}:`), field)
|
||||
}
|
||||
assert.match(booking, /const frozen = transportSnapshot\(completedDraft\?\.oceanBooking \|\| data\)/)
|
||||
assert.match(booking, /documentType', 'BOOKING_CONFIRMATION'/)
|
||||
assert.match(booking, /documentType', 'BILL_OF_LADING'/)
|
||||
assert.match(booking, /\/documents\/upload/)
|
||||
assert.match(booking, /expectedVersion: documentVersion/)
|
||||
assert.match(booking, /expectedVersion: Number\(uploaded\.bookingVersion\)/)
|
||||
assert.match(booking, /完整 DRAFT 放舱为 RELEASED 且冻结五项运输事实/)
|
||||
assert.match(booking, /Object\.entries\(frozen\)\.every/)
|
||||
assert.match(booking, /RELEASED 编辑拒绝且零写入/)
|
||||
|
||||
@@ -223,8 +223,9 @@ export async function run(context) {
|
||||
if (!containerBookingId) return
|
||||
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
|
||||
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
|
||||
&& Number(item.availablePackageCount) > 0) : null
|
||||
const inboundCandidates = Array.isArray(candidates) ? candidates.filter((item) => id(item.inboundId) === inboundId
|
||||
&& Number(item.availablePackageCount) > 0) : []
|
||||
const candidate = inboundCandidates[0]
|
||||
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
|
||||
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
|
||||
&& id(candidate?.warehouseId) === warehouseId,
|
||||
@@ -237,16 +238,24 @@ export async function run(context) {
|
||||
const loadingPlan = context.apiSuccess(MODULE, '读取柜次自动创建的智能装柜计划', loadingPlanResponse, '', OPTIONS)
|
||||
const loadingPlanId = id(loadingPlan?.id)
|
||||
if (!loadingPlanId) return
|
||||
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
|
||||
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
|
||||
expectedVersion: Number(loadingPlan.version),
|
||||
operationId: `${context.marker}-${loadingPlanId}-bridge-assign`.slice(0, 128),
|
||||
})
|
||||
const dropshipAssignment = source?.fulfillmentType === 'ONE_CLICK_DROPSHIP'
|
||||
const initialAssignmentCount = dropshipAssignment
|
||||
? inboundCandidates.reduce((total, item) => total + Number(item.availablePackageCount || 0), 0) : 1
|
||||
const assignmentPath = dropshipAssignment
|
||||
? `${smartLoadingBase}/plans/${loadingPlanId}/inbound-assignments`
|
||||
: `${smartLoadingBase}/plans/${loadingPlanId}/assignments`
|
||||
const assignmentBody = dropshipAssignment
|
||||
? { inboundId, packageCount: initialAssignmentCount, expectedVersion: Number(loadingPlan.version),
|
||||
operationId: `${context.marker}-${loadingPlanId}-bridge-assign-inbound`.slice(0, 128) }
|
||||
: { inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: initialAssignmentCount,
|
||||
expectedVersion: Number(loadingPlan.version), operationId: `${context.marker}-${loadingPlanId}-bridge-assign`.slice(0, 128) }
|
||||
const assignmentResponse = await request(context, 'POST', assignmentPath, assignmentBody)
|
||||
const assignedLoadingPlan = context.apiSuccess(MODULE, '将本轮入库货物分配到柜次计划', assignmentResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '柜次计划明细和版本真实持久化', assignmentResponse,
|
||||
assignedLoadingPlan?.items?.some?.((item) => id(item.inboundId) === inboundId && Number(item.packageCount) === 1)
|
||||
&& Number(assignedLoadingPlan?.version) === 1,
|
||||
`loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
assignedLoadingPlan?.items?.filter?.((item) => id(item.inboundId) === inboundId)
|
||||
.reduce((total, item) => total + Number(item.packageCount || 0), 0) === initialAssignmentCount
|
||||
&& Number(assignedLoadingPlan?.version) >= 1,
|
||||
`loadingPlanId=${loadingPlanId}, assignedPackages=${initialAssignmentCount}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const assignedItem = assignedLoadingPlan?.items?.find?.((item) => id(item.inboundId) === inboundId)
|
||||
const orderRemarks = source || {}
|
||||
@@ -584,12 +593,15 @@ export async function run(context) {
|
||||
const canonicalTaskId = id(canonicalTask?.id)
|
||||
const taskDetailPath = `${pdaBase}/tasks/${canonicalTaskId}?warehouseId=${encodeURIComponent(warehouseId)}`
|
||||
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
|
||||
const canonicalPickItems = dropshipAssignment
|
||||
? canonicalTask?.items?.filter?.((item) => id(item.inboundId) === inboundId) || []
|
||||
: canonicalItem ? [canonicalItem] : []
|
||||
context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse,
|
||||
Boolean(canonicalTaskId) && canonicalTaskId === confirmedTaskId && id(canonicalTask?.loadingPlanId) === loadingPlanId
|
||||
&& id(canonicalTask?.containerBookingId) === containerBookingId
|
||||
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id),
|
||||
`taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS)
|
||||
if (!canonicalTaskId || !canonicalItem?.id) return
|
||||
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && canonicalPickItems.length > 0,
|
||||
`taskId=${canonicalTaskId || 'missing'}, itemCount=${canonicalPickItems.length}`, OPTIONS)
|
||||
if (!canonicalTaskId || !canonicalPickItems.length) return
|
||||
|
||||
const beforeClaimPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const beforeClaimPlan = context.apiSuccess(MODULE, '领取前读取柜次计划即时版本', beforeClaimPlanResponse, '', OPTIONS)
|
||||
@@ -665,15 +677,18 @@ export async function run(context) {
|
||||
const skipBatchKey = `${context.marker}-exception-skip-batch`.slice(0, 128)
|
||||
const partialBatchKey = `${context.marker}-exception-partial-batch`.slice(0, 128)
|
||||
const dynamicBatchKey = `${context.marker}-dynamic-pick`.slice(0, 128)
|
||||
const expectedBatchOperationIds = [batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey]
|
||||
const dropshipWholePallet = source?.fulfillmentType === 'ONE_CLICK_DROPSHIP'
|
||||
const expectedBatchOperationIds = dropshipWholePallet
|
||||
? [batchKey]
|
||||
: [batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey]
|
||||
const batchPayload = {
|
||||
warehouseId, loadingPlanId, containerBookingId,
|
||||
expectedVersion: Number(claimed?.version),
|
||||
inventorySnapshotToken: inventory.snapshotToken,
|
||||
items: [{
|
||||
itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
version: Number(canonicalItem.version),
|
||||
}],
|
||||
items: canonicalPickItems.map((item) => ({
|
||||
itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
|
||||
version: Number(item.version),
|
||||
})),
|
||||
}
|
||||
|
||||
const staleBatch = await request(context, 'POST', batchPath,
|
||||
@@ -1095,7 +1110,10 @@ export async function run(context) {
|
||||
let partialBatchVerified = false
|
||||
let partialBlockSnapshot = null
|
||||
let dynamicIncrementVerified = false
|
||||
if (!incrementalInboundId || incrementalFixture?.marker !== context.marker || incrementalInboundId === inboundId) {
|
||||
if (dropshipWholePallet) {
|
||||
context.addStep(MODULE, '一件代发整托生产者不执行可拆分动态增量场景', null, true,
|
||||
'逐箱封托事实必须整托守恒;SKIP、PARTIAL 和动态增删改由 STANDARD 生产者覆盖', OPTIONS)
|
||||
} else if (!incrementalInboundId || incrementalFixture?.marker !== context.marker || incrementalInboundId === inboundId) {
|
||||
context.addStep(MODULE, '取得本轮独立动态增量入库单', null, false,
|
||||
`inboundId=${incrementalInboundId || 'missing'}, primaryInboundId=${inboundId}, marker=${incrementalFixture?.marker || 'missing'}`, OPTIONS)
|
||||
} else {
|
||||
@@ -1507,9 +1525,9 @@ export async function run(context) {
|
||||
cartonMarks: stableLoadedCartonMarks(stagedTask?.items),
|
||||
fulfillmentType: source?.fulfillmentType || 'STANDARD',
|
||||
skuCodes: [...new Set([...(Array.isArray(source?.skuCodes) ? source.skuCodes : []),
|
||||
...(Array.isArray(incrementalFixture?.skuCodes) ? incrementalFixture.skuCodes : [])])],
|
||||
...(!dropshipWholePallet && Array.isArray(incrementalFixture?.skuCodes) ? incrementalFixture.skuCodes : [])])],
|
||||
skuLines: [...(Array.isArray(source?.skuLines) ? source.skuLines : []),
|
||||
...(Array.isArray(incrementalFixture?.skuLines) ? incrementalFixture.skuLines : [])],
|
||||
...(!dropshipWholePallet && Array.isArray(incrementalFixture?.skuLines) ? incrementalFixture.skuLines : [])],
|
||||
destinationType: initial?.summary?.destinationType || source?.destinationType || null,
|
||||
destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId),
|
||||
destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null,
|
||||
|
||||
@@ -27,10 +27,11 @@ test('AWAITING_REVIEW derives zero client actions and never needs a repeated bat
|
||||
assert.match(convergence, /\$\{taskPath\}\/evidence\?warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
|
||||
assert.doesNotMatch(convergence, /api\/platform\/v1\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
|
||||
assert.match(convergence, /filter\(\(item\) => item\.actionType === 'BATCH'/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.length === 4/)
|
||||
assert.match(convergence, /actualBatchOperationIds\.length === 4/)
|
||||
assert.match(convergence, /new Set\(expectedBatchOperationIds\)\.size === 4/)
|
||||
assert.match(convergence, /new Set\(actualBatchOperationIds\)\.size === 4/)
|
||||
assert.match(convergence, /expectedBatchCount = fixture\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? 1 : 4/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.length === expectedBatchCount/)
|
||||
assert.match(convergence, /actualBatchOperationIds\.length === expectedBatchCount/)
|
||||
assert.match(convergence, /new Set\(expectedBatchOperationIds\)\.size === expectedBatchCount/)
|
||||
assert.match(convergence, /new Set\(actualBatchOperationIds\)\.size === expectedBatchCount/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.every[\s\S]*actualBatchOperationIds\.every/)
|
||||
assert.doesNotMatch(convergence, /dynamicIncrementVerified === true \? 2 : 1/)
|
||||
assert.match(convergence, /LOADING 增量重开、刷新续拣与已拣不可删减/)
|
||||
|
||||
@@ -500,7 +500,7 @@ test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING au
|
||||
for (const key of ['bridge-batch', 'exception-skip-batch', 'exception-partial-batch', 'dynamic-pick']) {
|
||||
assert.match(loading, new RegExp(key))
|
||||
}
|
||||
assert.match(loading, /expectedBatchOperationIds = \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
|
||||
assert.match(loading, /expectedBatchOperationIds = dropshipWholePallet[\s\S]*\? \[batchKey\][\s\S]*: \[batchKey, skipBatchKey, partialBatchKey, dynamicBatchKey\]/)
|
||||
assert.match(loading, /skipBatchVerified, partialBatchVerified, dynamicIncrementVerified/)
|
||||
assert.match(convergence, /expectedBatchCount = fixture\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? 1 : 4/)
|
||||
assert.match(convergence, /expectedBatchOperationIds\.length === expectedBatchCount/)
|
||||
|
||||
@@ -67,7 +67,7 @@ test('domestic and overseas PDA operators use isolated authorities', () => {
|
||||
test('unified migration creates the phase-four role matrix in the IAM schema used by its assertion routine', () => {
|
||||
assert.match(
|
||||
unifiedSql,
|
||||
/USE baiyue_iam;\s*DROP TEMPORARY TABLE IF EXISTS tmp_phase4_warehouse_role_permission;\s*CREATE TEMPORARY TABLE tmp_phase4_warehouse_role_permission/,
|
||||
/USE baiyue_iam;[\s\S]*?DROP TEMPORARY TABLE IF EXISTS tmp_phase4_warehouse_role_permission;\s*CREATE TEMPORARY TABLE tmp_phase4_warehouse_role_permission/,
|
||||
)
|
||||
assert.match(unifiedSql, /CREATE PROCEDURE baiyue_iam\.assert_phase4_warehouse_role_minimum\(\)/)
|
||||
assert.match(unifiedSql, /CREATE PROCEDURE assert_phase4_warehouse_role_minimum\(\)/)
|
||||
})
|
||||
|
||||
@@ -76,16 +76,16 @@ test('new tailleg number is Shanghai daily LMO-yyMMdd-#### with concurrency and
|
||||
assert.match(generatorTests, /LMO-260828-0001/)
|
||||
})
|
||||
|
||||
test('one container with 15 inbound rows shares one new LMO number', () => {
|
||||
test('one container with 15 inbound rows shares one stable container order number', () => {
|
||||
const rows = Array.from({ length: 15 }, (_, index) => ({
|
||||
containerNo: 'CONT-15', orderNo: 'LMO-260827-0042', sourceInboundId: `inbound-${index + 1}`,
|
||||
}))
|
||||
assert.equal(validContainerOrderGroup(rows), true)
|
||||
assert.equal(validContainerOrderGroup(rows.map((row, index) => index === 14
|
||||
? { ...row, orderNo: 'LMO-260827-0043' } : row)), false)
|
||||
assert.match(consumer, /String orderNo\s*=\s*orderNoGenerator\.next\(\);[\s\S]*for \(int index/)
|
||||
assert.match(consumer, /String orderNo\s*=\s*orderNoGenerator\.forContainerNo\(payload\.containerNo\(\)\);[\s\S]*for \(int index/)
|
||||
assert.match(consumer, /createOrder\(envelope, payload, source, orderNo, rawSource, containerNo/)
|
||||
assert.match(consumerTests, /oneContainerWith15InboundRowsSharesOneNewLmoNumber/)
|
||||
assert.match(consumerTests, /oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber/)
|
||||
})
|
||||
|
||||
test('same event replay adds nothing and preserves the allocated number', () => {
|
||||
|
||||
@@ -4,8 +4,9 @@ import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('admin acceptance phone fixture keeps more than ten generated customers distinct', () => {
|
||||
assert.match(source, /slice\(-7\)\.padStart\(7, '0'\)/)
|
||||
assert.match(source, /phoneSequence % 100/)
|
||||
test('admin acceptance phone fixture resolves a currently available deterministic candidate', () => {
|
||||
assert.match(source, /const seed = `\$\{this\.marker\}:\$\{this\.phoneSequence\}:\$\{attempt\}`/)
|
||||
assert.match(source, /administrator-phone-availability\?phone=/)
|
||||
assert.match(source, /attempt < 100/)
|
||||
assert.doesNotMatch(source, /phoneSequence % 10\)/)
|
||||
})
|
||||
|
||||
@@ -34,7 +34,8 @@ test('coordinator runs selected tests once before parallel service packaging', (
|
||||
|
||||
test('AUTO detects database migrations without expanding them to all services', () => {
|
||||
assert.match(pipeline, /def databaseMigrationPaths = \['deploy\/sql\/'\]/)
|
||||
assert.match(pipeline, /!databaseMigration && commonPaths\.any/)
|
||||
assert.match(pipeline, /List serviceRelevantChanges = changed\.findAll/)
|
||||
assert.match(pipeline, /!databaseMigrationPaths\.any \{ prefix -> path\.startsWith\(prefix\) \}/)
|
||||
assert.match(pipeline, /env\.DATABASE_CHANGE_DETECTED = databaseChanged \? 'true' : 'false'/)
|
||||
})
|
||||
|
||||
|
||||
@@ -169,5 +169,8 @@ test('Finance-only HTTP entry expands producer prerequisites and runs downloads
|
||||
assert.deepEqual(focused.slice(0, 4), [
|
||||
'merchant-qualification-fixture', 'orders', 'inbound-measurement', 'phase3-container-loading',
|
||||
])
|
||||
assert.deepEqual(focused.slice(-4), ['finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads'])
|
||||
assert.deepEqual(focused.slice(-5), [
|
||||
'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full',
|
||||
'finance-direct-paid-isolation', 'finance-downloads',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"services": 15
|
||||
},
|
||||
"openapi": {
|
||||
"documents": 26,
|
||||
"coveredServices": 7,
|
||||
"codeFirstServices": 8
|
||||
"documents": 39,
|
||||
"coveredServices": 8,
|
||||
"codeFirstServices": 7
|
||||
},
|
||||
"reports": {
|
||||
"jacocoPresent": 21,
|
||||
|
||||
@@ -65,9 +65,9 @@ test('repository freezes the current topology and non-blocking aggregate summary
|
||||
const summary = await buildQualitySummary(repositoryRoot)
|
||||
const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8'))
|
||||
assert.deepEqual(summary.topology, { modules: 22, codeModules: 21, services: 15 })
|
||||
assert.equal(summary.openapi.documents, 26)
|
||||
assert.equal(summary.openapi.coveredServices.length, 7)
|
||||
assert.equal(summary.openapi.codeFirstServices.length, 8)
|
||||
assert.equal(summary.openapi.documents, 39)
|
||||
assert.equal(summary.openapi.coveredServices.length, 8)
|
||||
assert.equal(summary.openapi.codeFirstServices.length, 7)
|
||||
assert.equal(baseline.mode, 'report-only')
|
||||
assert.deepEqual(baseline.topology, summary.topology)
|
||||
assert.equal(baseline.modules.length, 21)
|
||||
|
||||
@@ -63,11 +63,15 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
'firstleg-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取装柜证据元数据/内容'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java',
|
||||
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '图片上传/作废非安全重放,查询可重复'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/OceanBookingFileClient.java',
|
||||
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '订舱附件上传无幂等键,元数据与下载可重复'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java',
|
||||
'firstleg-service', 'order-service', ['REQUEST', 'BACKGROUND'], 'READ', 'SAFE_READ',
|
||||
'在线测量/箱号/排柜读取;Order 审核 Outbox 创建 Firstleg 入库及箱号时也会后台回读订单货物'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java',
|
||||
'firstleg-service', 'order-service', ['REQUEST'], 'WRITE', 'IDEMPOTENCY_KEY', 'PDA 结算拆单携带 idempotencyKey/operationId'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java',
|
||||
'firstleg-service', 'order-service', ['REQUEST'], 'READ', 'SAFE_READ', '智能排柜分页读取待审核订单投影'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookFileClient.java',
|
||||
'firstleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '价目文件上传无幂等键,读取可重复'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookWarehouseClient.java',
|
||||
@@ -81,6 +85,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java',
|
||||
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ_WRITE', 'MIXED',
|
||||
'查询可重复;预占/选择释放/转移/结算使用业务幂等键,普通确认/释放按状态收敛'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePartnerClient.java',
|
||||
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取启用往来单位快照'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseSupplierClient.java',
|
||||
'firstleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取供应商快照'),
|
||||
feign('firstleg/src/main/java/com/baiyue/firstleg/customs/client/CustomsFileClient.java',
|
||||
@@ -98,12 +104,16 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/client/EventBridgeClient.java',
|
||||
'lastleg-service', 'event-bridge-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '卸柜完成 Outbox 按 eventId 投递'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/client/DomesticWarehouseDirectoryClient.java',
|
||||
'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取国内共享仓库权威目录'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java',
|
||||
'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '报价创建时读取国内合作仓快照'),
|
||||
feign('warehouse-overseas-service/src/main/java/com/baiyue/warehouse/client/DomesticWarehouseDirectoryClient.java',
|
||||
'warehouse-overseas-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ',
|
||||
'海外库位、托盘和资源锁写入前读取国内共享收货仓权威目录'),
|
||||
|
||||
feign('order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java',
|
||||
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', '批量读取同柜不可拆组聚合'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java',
|
||||
'order-service', 'firstleg-service', ['REQUEST', 'BACKGROUND'], 'READ_WRITE', 'BUSINESS_KEY',
|
||||
'列表查询/缺失修复在线执行;审核建单由 Outbox 投递,按 orderId/inboundCode 收敛'),
|
||||
@@ -111,6 +121,12 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', 'POST 校验计价渠道,不写履约状态'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java',
|
||||
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', 'POST 查询统一订单投影'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/MerchantPricingCatalogClient.java',
|
||||
'order-service', 'firstleg-service', ['REQUEST'], 'READ', 'SAFE_READ', '批量下单读取客户可用物流渠道'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/MerchantWarehouseCatalogClient.java',
|
||||
'order-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '批量下单分页读取启用仓库'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/OrderBatchFileClient.java',
|
||||
'order-service', 'file-service', ['REQUEST'], 'WRITE', 'NONE', '批量导入文件上传无通用幂等键'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/ProductImageClient.java',
|
||||
'order-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取商品图片元数据'),
|
||||
feign('order/src/main/java/com/baiyue/order/client/TenantEligibilityClient.java',
|
||||
@@ -120,6 +136,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
|
||||
http('common-audit/src/main/java/com/baiyue/common/audit/HttpAuditSink.java',
|
||||
'common-audit', 'audit-service', ['INFRASTRUCTURE'], 'WRITE', 'NONE', '同步审计投递失败当前仅记录告警'),
|
||||
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFinanceEventClient.java',
|
||||
'event-bridge-service', 'finance-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '跨区费用 Inbox 按 eventId 投递 Finance'),
|
||||
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFirstlegEventClient.java',
|
||||
'event-bridge-service', 'firstleg-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', 'Inbox 租约投递,目标按 eventId 幂等'),
|
||||
http('event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpLastlegEventClient.java',
|
||||
@@ -130,6 +148,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
'gateway-overseas', 'lastleg-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST 实时校验,失败关闭'),
|
||||
http('gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
|
||||
'gateway-overseas', 'auth-service', ['INFRASTRUCTURE'], 'READ', 'SAFE_READ', 'POST PDA 会话校验,失败关闭'),
|
||||
http('lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java',
|
||||
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '回访附件上传无幂等键,归属校验可重复'),
|
||||
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java',
|
||||
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'),
|
||||
http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java',
|
||||
|
||||
@@ -44,6 +44,7 @@ const expectedOnlineEdges = [
|
||||
]
|
||||
|
||||
const expectedBackgroundEdges = [
|
||||
'event-bridge-service->finance-service',
|
||||
'event-bridge-service->firstleg-service',
|
||||
'event-bridge-service->lastleg-service',
|
||||
'file-service->finance-service',
|
||||
@@ -96,15 +97,15 @@ async function discoverFeignEdges() {
|
||||
return clients.sort((left, right) => left.path.localeCompare(right.path))
|
||||
}
|
||||
|
||||
test('all 45 Feign clients have reviewed context, access and idempotency metadata', async () => {
|
||||
test('all 53 Feign clients have reviewed context, access and idempotency metadata', async () => {
|
||||
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN')
|
||||
.map(({ path, source, target }) => ({ path, source, target }))
|
||||
.sort((left, right) => left.path.localeCompare(right.path))
|
||||
assert.equal(reviewed.length, 45)
|
||||
assert.equal(reviewed.length, 53)
|
||||
assert.deepEqual(await discoverFeignEdges(), reviewed)
|
||||
})
|
||||
|
||||
test('all 16 raw HTTP transports have reviewed context, access and idempotency metadata', async () => {
|
||||
test('all 18 raw HTTP transports have reviewed context, access and idempotency metadata', async () => {
|
||||
const discovered = []
|
||||
for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) {
|
||||
for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) {
|
||||
@@ -116,7 +117,7 @@ test('all 16 raw HTTP transports have reviewed context, access and idempotency m
|
||||
}
|
||||
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP')
|
||||
.map(client => client.path).sort()
|
||||
assert.equal(reviewed.length, 16)
|
||||
assert.equal(reviewed.length, 18)
|
||||
assert.deepEqual(discovered.sort(), reviewed)
|
||||
})
|
||||
|
||||
@@ -213,13 +214,16 @@ test('Order and Firstleg remain a bilateral REQUEST SCC until loading-complete t
|
||||
const firstlegToOrder = requestClients.filter(client =>
|
||||
client.source === 'firstleg-service' && client.target === 'order-service')
|
||||
assert.deepEqual(orderToFirstleg.map(client => client.path).sort(), [
|
||||
'order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java',
|
||||
'order/src/main/java/com/baiyue/order/client/FirstlegInboundClient.java',
|
||||
'order/src/main/java/com/baiyue/order/client/FirstlegPricingClient.java',
|
||||
'order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java'
|
||||
'order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java',
|
||||
'order/src/main/java/com/baiyue/order/client/MerchantPricingCatalogClient.java'
|
||||
])
|
||||
assert.deepEqual(firstlegToOrder.map(client => client.path).sort(), [
|
||||
'firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java',
|
||||
'firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java'
|
||||
'firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java',
|
||||
'firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java'
|
||||
])
|
||||
assert.ok(stronglyConnectedComponents(graphEdges('REQUEST')).some(component =>
|
||||
component.includes('firstleg-service') && component.includes('order-service')))
|
||||
|
||||
Reference in New Issue
Block a user