test: harden admin acceptance fixtures and transport retries
This commit is contained in:
@@ -79,8 +79,12 @@ ADMIN_TEST_USERNAME
|
|||||||
ADMIN_TEST_PASSWORD
|
ADMIN_TEST_PASSWORD
|
||||||
MERCHANT_TEST_USERNAME
|
MERCHANT_TEST_USERNAME
|
||||||
MERCHANT_TEST_PASSWORD
|
MERCHANT_TEST_PASSWORD
|
||||||
|
ADMIN_PDA_USERNAME
|
||||||
|
ADMIN_PDA_PASSWORD
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Admin 默认全量及依赖国内装柜链的定向验收必须提供 `ADMIN_PDA_USERNAME/PASSWORD`。管理员用于计划确认,PDA 作业员用于领取与执行;缺少作业员凭据在启动前阻断,不能以管理员身份代替已分配的作业员。
|
||||||
|
|
||||||
脚本只读取当前 Node 进程的环境变量,不会自行查找或加载上述文件。运行时必须把 `--env-file` 放在脚本路径之前,明确选择环境:
|
脚本只读取当前 Node 进程的环境变量,不会自行查找或加载上述文件。运行时必须把 `--env-file` 放在脚本路径之前,明确选择环境:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ function usage() {
|
|||||||
--overseas-gateway <url> 四期海外模块必填,海外 Gateway 地址
|
--overseas-gateway <url> 四期海外模块必填,海外 Gateway 地址
|
||||||
--username <name> 必填,管理员账号
|
--username <name> 必填,管理员账号
|
||||||
--password <value> 必填,管理员密码
|
--password <value> 必填,管理员密码
|
||||||
|
--pda-username <name> 国内装柜链必填,实际领取任务的 PDA 作业员账号
|
||||||
|
--pda-password <value> 国内装柜链必填,PDA 作业员密码
|
||||||
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
|
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
|
||||||
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
|
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
|
||||||
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
|
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
|
||||||
@@ -398,6 +400,10 @@ export function validateExecutionOptions(options) {
|
|||||||
&& (!options.overseasGateway || options.overseasGateway.trim() === '')) {
|
&& (!options.overseasGateway || options.overseasGateway.trim() === '')) {
|
||||||
throw new Error('四期海外模块缺少必填执行参数: overseas-gateway')
|
throw new Error('四期海外模块缺少必填执行参数: overseas-gateway')
|
||||||
}
|
}
|
||||||
|
if (options.modules.some((module) => ['phase3-loading', 'phase3-container-loading'].includes(module))
|
||||||
|
&& (!options.pdaUsername?.trim() || !options.pdaPassword?.trim())) {
|
||||||
|
throw new Error('国内装柜链缺少 PDA 作业员凭据: pda-username / pda-password(ADMIN_PDA_USERNAME / ADMIN_PDA_PASSWORD)')
|
||||||
|
}
|
||||||
if (!Number.isInteger(options.waitSplitCount) || options.waitSplitCount < 1 || options.waitSplitCount > 100) {
|
if (!Number.isInteger(options.waitSplitCount) || options.waitSplitCount < 1 || options.waitSplitCount > 100) {
|
||||||
throw new Error('wait-split-count必须是1-100的整数')
|
throw new Error('wait-split-count必须是1-100的整数')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,28 +537,36 @@ function findIsolatedInbound(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function multipartRequest(context, path, data) {
|
async function multipartRequest(context, path, data) {
|
||||||
const form = new FormData()
|
let lastResponse = null
|
||||||
form.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' }))
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
form.append('image', new Blob([WEIGHT_IMAGE], { type: 'image/png' }), 'carton-evidence.png')
|
const form = new FormData()
|
||||||
try {
|
form.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' }))
|
||||||
const response = await fetch(`${context.options.gateway}${path}`, {
|
form.append('image', new Blob([WEIGHT_IMAGE], { type: 'image/png' }), 'carton-evidence.png')
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
||||||
body: form,
|
method: 'POST',
|
||||||
signal: AbortSignal.timeout(60_000),
|
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
||||||
})
|
body: form,
|
||||||
const content = await response.text()
|
signal: AbortSignal.timeout(60_000),
|
||||||
let body = null
|
})
|
||||||
try { body = content ? JSON.parse(content) : null } catch { /* Preserve only a bounded error string. */ }
|
const content = await response.text()
|
||||||
return {
|
let body = null
|
||||||
method: 'POST', path, http: response.status, body,
|
try { body = content ? JSON.parse(content) : null } catch { /* Preserve only a bounded error string. */ }
|
||||||
raw: body === null ? content.slice(0, 500) : null,
|
lastResponse = {
|
||||||
requestId: body?.requestId || response.headers.get('x-request-id'),
|
method: 'POST', path, http: response.status, body,
|
||||||
traceId: body?.traceId || response.headers.get('x-trace-id'),
|
raw: body === null ? content.slice(0, 500) : null,
|
||||||
|
requestId: body?.requestId || response.headers.get('x-request-id'),
|
||||||
|
traceId: body?.traceId || response.headers.get('x-trace-id'),
|
||||||
|
}
|
||||||
|
if (lastResponse.http !== 0 || attempt === 3) return lastResponse
|
||||||
|
} catch (error) {
|
||||||
|
lastResponse = { method: 'POST', path, http: 0, body: null,
|
||||||
|
raw: error instanceof Error ? error.message : String(error), requestId: null, traceId: null }
|
||||||
|
if (attempt === 3) return lastResponse
|
||||||
}
|
}
|
||||||
} catch (error) {
|
await new Promise((resolve) => setTimeout(resolve, 400 * attempt))
|
||||||
return { method: 'POST', path, http: 0, body: null, raw: error instanceof Error ? error.message : String(error), requestId: null, traceId: null }
|
|
||||||
}
|
}
|
||||||
|
return lastResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestWithHeaders(context, method, path, body, extraHeaders) {
|
async function requestWithHeaders(context, method, path, body, extraHeaders) {
|
||||||
|
|||||||
@@ -753,7 +753,14 @@ export async function run(context) {
|
|||||||
}
|
}
|
||||||
const fixturesByAuxiliaryCapacity = [...handoff.completedFixtures]
|
const fixturesByAuxiliaryCapacity = [...handoff.completedFixtures]
|
||||||
.sort((left, right) => (right?.auxiliaryPallets?.length || 0) - (left?.auxiliaryPallets?.length || 0))
|
.sort((left, right) => (right?.auxiliaryPallets?.length || 0) - (left?.auxiliaryPallets?.length || 0))
|
||||||
|
// P4/P5 deliberately consumes and rearranges one multi-carton source. Keep the
|
||||||
|
// ABE8 producer untouched when the matrix contains both destinations so the
|
||||||
|
// following dispatch module can still observe RECEIVED candidates for ABE4+ABE8.
|
||||||
const fixture = fixturesByAuxiliaryCapacity.find((item) => item?.destinationType === 'FBA'
|
const fixture = fixturesByAuxiliaryCapacity.find((item) => item?.destinationType === 'FBA'
|
||||||
|
&& item?.fbaWarehouseCode === 'ABE4'
|
||||||
|
&& (item?.auxiliaryPallets?.length || 0) > 0 && Array.isArray(item?.inventory)
|
||||||
|
&& item.inventory.reduce((sum, line) => sum + Number(line?.availablePackages || 0), 0) > 3)
|
||||||
|
|| fixturesByAuxiliaryCapacity.find((item) => item?.destinationType === 'FBA'
|
||||||
&& (item?.auxiliaryPallets?.length || 0) > 0 && Array.isArray(item?.inventory)
|
&& (item?.auxiliaryPallets?.length || 0) > 0 && Array.isArray(item?.inventory)
|
||||||
&& item.inventory.reduce((sum, line) => sum + Number(line?.availablePackages || 0), 0) > 3)
|
&& item.inventory.reduce((sum, line) => sum + Number(line?.availablePackages || 0), 0) > 3)
|
||||||
|| fixturesByAuxiliaryCapacity.find((item) => item?.destinationType === 'OVERSEAS'
|
|| fixturesByAuxiliaryCapacity.find((item) => item?.destinationType === 'OVERSEAS'
|
||||||
|
|||||||
@@ -264,10 +264,19 @@ export async function run(context) {
|
|||||||
...firstBody, trackingNo: `TRACK-${suffix}-CHANGED`,
|
...firstBody, trackingNo: `TRACK-${suffix}-CHANGED`,
|
||||||
}, {}, picker.token))
|
}, {}, picker.token))
|
||||||
|
|
||||||
await reject('超收被拒绝且不得生成费用批次', conflict, () => context.requestOverseas('POST', `${PDA_RECEIPTS}/confirm`, {
|
await reject('超收被拒绝且不得生成费用批次', conflict, async () => {
|
||||||
returnNo, startedBy: 'SKU', palletCode: cartonA, items: [{ skuCode: skuA, receivedQuantity: 2 }],
|
const body = {
|
||||||
idempotencyKey: key(context, 'OVER-RECEIPT'),
|
returnNo, startedBy: 'SKU', palletCode: cartonA, items: [{ skuCode: skuA, receivedQuantity: 2 }],
|
||||||
}, {}, picker.token))
|
idempotencyKey: key(context, 'OVER-RECEIPT'),
|
||||||
|
}
|
||||||
|
let response = null
|
||||||
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
|
response = await context.requestOverseas('POST', `${PDA_RECEIPTS}/confirm`, body, {}, picker.token)
|
||||||
|
if (response?.http !== 0 || attempt === 3) break
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400 * attempt))
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
for (const [name, changes, expected] of [
|
for (const [name, changes, expected] of [
|
||||||
['零实收数量', { items: [{ skuCode: skuA, receivedQuantity: 0 }] }, validationFailure],
|
['零实收数量', { items: [{ skuCode: skuA, receivedQuantity: 0 }] }, validationFailure],
|
||||||
|
|||||||
@@ -131,10 +131,26 @@ test('entry has no built-in environment address or password and requires explici
|
|||||||
const options = parseArguments([
|
const options = parseArguments([
|
||||||
'--gateway', 'http://localhost:9000', '--overseas-gateway', 'http://localhost:9300',
|
'--gateway', 'http://localhost:9000', '--overseas-gateway', 'http://localhost:9300',
|
||||||
'--username', 'contract-user', '--password', 'contract-password', '--modules', 'phase4-overseas-pda',
|
'--username', 'contract-user', '--password', 'contract-password', '--modules', 'phase4-overseas-pda',
|
||||||
|
'--pda-username', 'contract-picker', '--pda-password', 'contract-picker-password',
|
||||||
])
|
])
|
||||||
assert.equal(validateExecutionOptions(options), options)
|
assert.equal(validateExecutionOptions(options), options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('loading dependency chains require PDA credentials before making any HTTP requests', () => {
|
||||||
|
for (const module of ['phase3-loading', 'phase3-container-loading', 'phase5-overseas-fulfillment']) {
|
||||||
|
const options = parseArguments(['--gateway', 'http://localhost:9000',
|
||||||
|
'--overseas-gateway', 'http://localhost:9300', '--username', 'admin', '--password', 'fixture',
|
||||||
|
'--modules', module])
|
||||||
|
options.pdaUsername = ''
|
||||||
|
options.pdaPassword = ''
|
||||||
|
assert.throws(() => validateExecutionOptions(options), /PDA 作业员凭据/)
|
||||||
|
options.pdaUsername = 'picker'
|
||||||
|
assert.throws(() => validateExecutionOptions(options), /PDA 作业员凭据/)
|
||||||
|
options.pdaPassword = 'fixture'
|
||||||
|
assert.equal(validateExecutionOptions(options), options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('core coverage gaps fail closed while device and external-provider gaps remain report-only', () => {
|
test('core coverage gaps fail closed while device and external-provider gaps remain report-only', () => {
|
||||||
const run = new AcceptanceRun({ gateway: 'http://localhost:9000', overseasGateway: 'http://localhost:9300',
|
const run = new AcceptanceRun({ gateway: 'http://localhost:9000', overseasGateway: 'http://localhost:9300',
|
||||||
username: 'contract-user', password: 'contract-password', modules: [], output: '' })
|
username: 'contract-user', password: 'contract-password', modules: [], output: '' })
|
||||||
|
|||||||
Reference in New Issue
Block a user