2026-08-21 10:38:28 +08:00
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import { resolve } from 'node:path'
|
2026-10-02 20:14:17 +08:00
|
|
|
import { AcceptanceRun, configureReadOnlyEvidence, unloadRecoveryWasVerified, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs'
|
2026-08-21 10:38:28 +08:00
|
|
|
|
|
|
|
|
const source = readFileSync(resolve('scripts/admin-test-env-acceptance.mjs'), 'utf8')
|
|
|
|
|
|
2026-10-02 20:14:17 +08:00
|
|
|
test('full CLI evidence preflight attaches readers and fails before business writes when unavailable', async () => {
|
|
|
|
|
const run = { options: { modules: ['manual-state-matrix'] }, context: {} }
|
|
|
|
|
const readReceiving = () => {}, readCompletion = () => {}
|
|
|
|
|
let checked = false
|
|
|
|
|
await configureReadOnlyEvidence(run, {}, () => ({ readReceiving, readCompletion,
|
|
|
|
|
preflight: async () => { checked = true } }))
|
|
|
|
|
assert.equal(checked, true)
|
|
|
|
|
assert.equal(run.context.phase4ReceivingEvidenceReader, readReceiving)
|
|
|
|
|
assert.equal(run.context.f40ReadOnlyOutboxEvidence, readCompletion)
|
|
|
|
|
const unavailable = { options: run.options, context: {} }
|
|
|
|
|
await assert.rejects(configureReadOnlyEvidence(unavailable, {}, () => ({
|
|
|
|
|
preflight: async () => { throw Error('evidence unavailable') } })), /unavailable/)
|
|
|
|
|
assert.deepEqual(unavailable.context, {})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('explicit unloading fault uses a separate first-container hook and restores both directions on failure', async () => {
|
|
|
|
|
const calls = []
|
|
|
|
|
const run = { marker: 'TEST-UNLOAD', options: { modules: ['phase4-overseas-pda', 'phase4-cross-region-events'],
|
|
|
|
|
verifyUnloadEventRecovery: true }, context: {} }
|
|
|
|
|
const fault = name => ({ prepare: async scope => calls.push([name, 'prepare', scope]),
|
|
|
|
|
readProof: async scope => ({ name, scope, restored: true }),
|
|
|
|
|
cleanup: async () => { calls.push([name, 'cleanup']); if (name === 'reverse') throw Error('recovery failed') } })
|
|
|
|
|
const cleanup = await configureReadOnlyEvidence(run, {}, () => ({ readReceiving() {}, readCompletion() {}, preflight: async () => {} }),
|
|
|
|
|
{ createTestFaultExecutor: () => fault('forward'), createTestUnloadFaultExecutor: () => fault('reverse') })
|
|
|
|
|
assert.equal(calls.length, 0)
|
|
|
|
|
await run.context.beforeReceivingComplete({ customerId: '7', bookingId: '66' })
|
|
|
|
|
await run.context.beforeReceivingComplete({ customerId: '8', bookingId: '77' })
|
|
|
|
|
assert.deepEqual(calls, [['reverse', 'prepare', { marker: 'TEST-UNLOAD', customerId: '7', containerBookingId: '66' }]])
|
|
|
|
|
assert.equal(await run.context.afterReceivingComplete({ customerId: '8', bookingId: '77' }), null)
|
|
|
|
|
const proof = await run.context.afterReceivingComplete({ customerId: '7', bookingId: '66' })
|
|
|
|
|
assert.equal(proof.name, 'reverse')
|
|
|
|
|
await assert.rejects(cleanup.cleanup(), /恢复失败/)
|
|
|
|
|
assert.deepEqual(calls.slice(-2), [['reverse', 'cleanup'], ['forward', 'cleanup']])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('unloading recovery is opt-in and cannot run against unrelated modules', () => {
|
|
|
|
|
const args = ['--modules', 'phase4-overseas-return-followups', '--gateway', 'http://localhost:9000',
|
|
|
|
|
'--username', 'contract', '--password', 'contract']
|
|
|
|
|
assert.equal(parseArguments(args).verifyUnloadEventRecovery, false)
|
|
|
|
|
assert.throws(() => validateExecutionOptions(parseArguments([...args, '--verify-unload-event-recovery', 'true'])), /收货链/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('requested unloading fault cannot pass without exact current-run recovery and inventory proof', () => {
|
|
|
|
|
const run = { marker: 'CURRENT', createdResources: {} }
|
|
|
|
|
assert.equal(unloadRecoveryWasVerified(run), false)
|
|
|
|
|
const row = { status: 'VERIFIED', eventRecoveryInventoryUnchanged: true,
|
|
|
|
|
eventRecovery: { restored: true, marker: 'CURRENT', containerBookingId: '66' } }
|
|
|
|
|
run.createdResources.phase4OverseasPda = { receivingCompletionEvidence: { '66': row } }
|
|
|
|
|
assert.equal(unloadRecoveryWasVerified(run), true)
|
|
|
|
|
for (const change of [r => r.eventRecoveryInventoryUnchanged = false, r => r.status = 'UNPROVEN',
|
|
|
|
|
r => r.eventRecovery.restored = false, r => r.eventRecovery.marker = 'OLD', r => r.eventRecovery.containerBookingId = '77']) {
|
|
|
|
|
const changed = structuredClone(run)
|
|
|
|
|
change(changed.createdResources.phase4OverseasPda.receivingCompletionEvidence['66'])
|
|
|
|
|
assert.equal(unloadRecoveryWasVerified(changed), false)
|
|
|
|
|
}
|
|
|
|
|
run.createdResources.phase4OverseasPda.receivingCompletionEvidence['77'] = structuredClone(row)
|
|
|
|
|
assert.equal(unloadRecoveryWasVerified(run), false)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-30 15:16:39 +08:00
|
|
|
test('F40 optional chain always creates its customer and rejects absent domestic PDA credentials', () => {
|
|
|
|
|
const options = parseArguments(['--modules', 'phase5-root-fulfillment',
|
|
|
|
|
'--gateway', 'http://localhost:9000', '--username', 'contract-user', '--password', 'contract-password',
|
|
|
|
|
'--merchant-username', 'prior-client', '--merchant-password', 'prior-password'])
|
|
|
|
|
assert.deepEqual(options.modules, ['merchant-qualification-fixture', 'phase5-root-fulfillment'])
|
|
|
|
|
options.pdaUsername = ''; options.pdaPassword = ''
|
|
|
|
|
assert.throws(() => validateExecutionOptions(options), /PDA/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-21 10:38:28 +08:00
|
|
|
test('phase-four HTTP acceptance modules are registered in the Admin acceptance entry', () => {
|
|
|
|
|
for (const module of [
|
|
|
|
|
'phase4-overseas-warehouse',
|
2026-08-24 15:38:28 +08:00
|
|
|
'phase4-overseas-pda',
|
2026-08-25 11:25:13 +08:00
|
|
|
'phase4-overseas-pda-operations',
|
2026-08-25 00:24:43 +08:00
|
|
|
'phase4-overseas-inbound-operations',
|
|
|
|
|
'phase4-overseas-client-services',
|
|
|
|
|
'phase4-overseas-dispatch',
|
2026-08-24 19:40:46 +08:00
|
|
|
'phase4-cross-region-events',
|
2026-08-21 10:38:28 +08:00
|
|
|
'phase4-overseas-business',
|
|
|
|
|
'phase4-overseas-master-data',
|
|
|
|
|
'phase4-carrier-nodes',
|
2026-09-06 11:00:06 +08:00
|
|
|
'phase4-overseas-wallet-and-pallet-cost',
|
2026-08-21 10:38:28 +08:00
|
|
|
]) {
|
|
|
|
|
assert.match(source, new RegExp(`CONFIGURED_MODULES\\s*=\\s*\\[[^\\]]*'${module}'`, 's'))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-06 11:00:06 +08:00
|
|
|
test('wallet and destuff cost closure expands the fresh merchant and completed overseas PDA chain', () => {
|
|
|
|
|
const modules = parseArguments(['--modules', 'phase4-overseas-wallet-and-pallet-cost']).modules
|
|
|
|
|
for (const prerequisite of ['merchant-qualification-fixture', 'phase3-loading-finalize',
|
|
|
|
|
'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-business']) {
|
|
|
|
|
assert.ok(modules.indexOf(prerequisite) >= 0
|
|
|
|
|
&& modules.indexOf(prerequisite) < modules.indexOf('phase4-overseas-wallet-and-pallet-cost'))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-25 00:24:43 +08:00
|
|
|
test('overseas inbound commands, client services and dispatch follow completed PDA receiving', () => {
|
|
|
|
|
for (const consumer of ['phase4-overseas-inbound-operations', 'phase4-overseas-client-services',
|
|
|
|
|
'phase4-overseas-dispatch']) {
|
|
|
|
|
const modules = parseArguments(['--modules', consumer]).modules
|
|
|
|
|
for (const prerequisite of ['merchant-qualification-fixture', 'phase3-loading',
|
2026-08-25 11:25:13 +08:00
|
|
|
'phase3-loading-finalize', 'phase4-overseas-pda', 'phase4-overseas-pda-operations']) {
|
2026-08-25 00:24:43 +08:00
|
|
|
assert.ok(modules.indexOf(prerequisite) >= 0
|
|
|
|
|
&& modules.indexOf(prerequisite) < modules.indexOf(consumer))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-29 19:48:34 +08:00
|
|
|
test('entry uses one TEST gateway and rejects retired regional options', () => {
|
|
|
|
|
const options = parseArguments(['--gateway', 'http://localhost:9000/', '--modules', 'phase4-overseas-warehouse'])
|
2026-08-21 10:38:28 +08:00
|
|
|
assert.equal(options.gateway, 'http://localhost:9000')
|
2026-09-29 19:48:34 +08:00
|
|
|
assert.equal(options.environment, 'test')
|
|
|
|
|
assert.throws(() => parseArguments(['--overseas-gateway', 'http://localhost:9300']), /one TEST gateway/)
|
|
|
|
|
assert.throws(() => parseArguments(['--environment', 'cn']), /TEST|test/)
|
|
|
|
|
assert.doesNotMatch(source, /async requestOverseas|this\.overseasToken|overseasAccessToken/)
|
|
|
|
|
assert.match(source, /this\.refreshToken = login\.refreshToken/)
|
|
|
|
|
assert.match(source, /system后新admin统一Token只读探针可用/)
|
2026-08-25 16:40:48 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-02 16:48:05 +08:00
|
|
|
test('overseas business keeps its marker-scoped customer fixture with reusable credentials configured', () => {
|
|
|
|
|
const previousUsername = process.env.MERCHANT_TEST_USERNAME
|
|
|
|
|
const previousPassword = process.env.MERCHANT_TEST_PASSWORD
|
|
|
|
|
process.env.MERCHANT_TEST_USERNAME = 'reusable-client'
|
|
|
|
|
process.env.MERCHANT_TEST_PASSWORD = 'reusable-password'
|
|
|
|
|
try {
|
|
|
|
|
const modules = parseArguments(['--modules', 'phase4-overseas-business']).modules
|
|
|
|
|
assert.ok(modules.includes('merchant-qualification-fixture'))
|
|
|
|
|
assert.ok(modules.indexOf('merchant-qualification-fixture') < modules.indexOf('phase4-overseas-business'))
|
|
|
|
|
} finally {
|
|
|
|
|
if (previousUsername === undefined) delete process.env.MERCHANT_TEST_USERNAME
|
|
|
|
|
else process.env.MERCHANT_TEST_USERNAME = previousUsername
|
|
|
|
|
if (previousPassword === undefined) delete process.env.MERCHANT_TEST_PASSWORD
|
|
|
|
|
else process.env.MERCHANT_TEST_PASSWORD = previousPassword
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('a fixed origin route does not require randomized order fixtures', () => {
|
|
|
|
|
const options = parseArguments(['--origin-warehouse-name', '东莞仓', '--modules', 'phase4-overseas-pda'])
|
|
|
|
|
assert.equal(options.originWarehouseName, '东莞仓')
|
|
|
|
|
assert.equal(options.routeMode, 'fixed')
|
|
|
|
|
assert.equal(options.orderProfileMode, 'fixed')
|
|
|
|
|
assert.match(source, /else if \(this\.options\.originWarehouseName \|\| this\.options\.channelName \|\| this\.options\.channelCode\)/)
|
|
|
|
|
assert.match(source, /this\.context\.waitSplitRoute = \{[\s\S]*randomize: false/)
|
|
|
|
|
assert.match(source, /this\.context\.allowAnyExistingPallet = true/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-29 19:48:34 +08:00
|
|
|
test('unified-session refresh replaces the whole session only after a complete response', async () => {
|
|
|
|
|
const run = new AcceptanceRun({ gateway: 'http://localhost:9000',
|
2026-08-25 16:40:48 +08:00
|
|
|
username: 'contract-user', password: 'contract-password', modules: [], output: '' })
|
2026-09-29 19:48:34 +08:00
|
|
|
Object.assign(run, { token: 'old-access', refreshToken: 'old-refresh', expiresAt: 1 })
|
2026-08-25 16:40:48 +08:00
|
|
|
run.request = async (method, path, body, token) => {
|
|
|
|
|
assert.equal(method, 'POST'); assert.equal(path, '/api/auth/refresh')
|
|
|
|
|
assert.deepEqual(body, { refreshToken: 'old-refresh' }); assert.equal(token, '')
|
|
|
|
|
return { method, path, http: 200, body: { code: '200', data: {
|
2026-09-29 19:48:34 +08:00
|
|
|
accessToken: 'new-access', refreshToken: 'new-refresh',
|
|
|
|
|
expiresIn: 900,
|
2026-08-25 16:40:48 +08:00
|
|
|
} }, requestId: 'refresh-request', traceId: 'refresh-trace' }
|
|
|
|
|
}
|
|
|
|
|
assert.equal(await run.refreshSession('contract'), true)
|
|
|
|
|
assert.equal(run.token, 'new-access')
|
|
|
|
|
assert.equal(run.refreshToken, 'new-refresh')
|
2026-09-29 19:48:34 +08:00
|
|
|
assert.ok(run.expiresAt > Date.now())
|
2026-08-25 16:40:48 +08:00
|
|
|
|
|
|
|
|
const frozen = { token: run.token, refreshToken: run.refreshToken,
|
2026-09-29 19:48:34 +08:00
|
|
|
expiresAt: run.expiresAt }
|
2026-08-25 16:40:48 +08:00
|
|
|
run.request = async (method, path) => ({ method, path, http: 200,
|
|
|
|
|
body: { code: '200', data: { accessToken: 'partial-only' } },
|
|
|
|
|
requestId: 'partial-request', traceId: 'partial-trace' })
|
|
|
|
|
assert.equal(await run.refreshSession('incomplete-contract'), false)
|
|
|
|
|
assert.deepEqual({ token: run.token, refreshToken: run.refreshToken,
|
2026-09-29 19:48:34 +08:00
|
|
|
expiresAt: run.expiresAt }, frozen)
|
2026-08-21 10:38:28 +08:00
|
|
|
})
|
2026-08-24 19:58:35 +08:00
|
|
|
|
|
|
|
|
test('entry has no built-in environment address or password and requires explicit execution options', () => {
|
|
|
|
|
assert.doesNotMatch(source, /8\.163\.|ChangeMe123|DEFAULT_PASSWORD|DEFAULT_GATEWAY/)
|
|
|
|
|
assert.throws(() => validateExecutionOptions(parseArguments(['--modules', 'phase4-overseas-pda'])),
|
|
|
|
|
/缺少必填执行参数/)
|
|
|
|
|
const options = parseArguments([
|
2026-09-29 19:48:34 +08:00
|
|
|
'--gateway', 'http://localhost:9000',
|
2026-08-24 19:58:35 +08:00
|
|
|
'--username', 'contract-user', '--password', 'contract-password', '--modules', 'phase4-overseas-pda',
|
2026-09-24 04:38:38 +08:00
|
|
|
'--pda-username', 'contract-picker', '--pda-password', 'contract-picker-password',
|
2026-08-24 19:58:35 +08:00
|
|
|
])
|
|
|
|
|
assert.equal(validateExecutionOptions(options), options)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-24 04:38:38 +08:00
|
|
|
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',
|
2026-09-29 19:48:34 +08:00
|
|
|
'--username', 'admin', '--password', 'fixture',
|
2026-09-24 04:38:38 +08:00
|
|
|
'--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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-24 19:58:35 +08:00
|
|
|
test('core coverage gaps fail closed while device and external-provider gaps remain report-only', () => {
|
2026-09-29 19:48:34 +08:00
|
|
|
const run = new AcceptanceRun({ gateway: 'http://localhost:9000',
|
2026-08-24 19:58:35 +08:00
|
|
|
username: 'contract-user', password: 'contract-password', modules: [], output: '' })
|
|
|
|
|
run.addCoverageGap('phase4', 'core', 'fixture', 'missing fixture', 'fixture')
|
|
|
|
|
assert.equal(run.steps.at(-1)?.passed, false)
|
|
|
|
|
assert.equal(run.steps.at(-1)?.deploymentBlocker, true)
|
|
|
|
|
const stepCount = run.steps.length
|
|
|
|
|
run.addCoverageGap('phase3', 'device', 'Android', 'manual device validation', 'device')
|
|
|
|
|
run.addCoverageGap('phase3', 'provider', 'captcha', 'external captcha', 'external-third-party')
|
2026-09-06 11:41:46 +08:00
|
|
|
run.addCoverageGap('phase3', 'device', 'Android', 'manual device validation', 'device')
|
|
|
|
|
run.addCoverageGap('phase3', 'provider', 'captcha', 'external captcha', 'external-third-party')
|
2026-08-24 19:58:35 +08:00
|
|
|
assert.equal(run.steps.length, stepCount)
|
2026-09-06 11:41:46 +08:00
|
|
|
assert.equal(run.coverageGaps.length, 3)
|
2026-08-24 19:58:35 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('PDA entry expands the real phase-three producer chain and builds an FBA/OVERSEAS/second-container matrix', () => {
|
|
|
|
|
const modules = parseArguments(['--modules', 'phase4-overseas-pda']).modules
|
|
|
|
|
for (const prerequisite of ['merchant-qualification-fixture', 'phase3-loading', 'phase3-pda-convergence',
|
|
|
|
|
'phase3-evidence-gate', 'phase3-loading-finalize']) {
|
|
|
|
|
assert.ok(modules.indexOf(prerequisite) >= 0 && modules.indexOf(prerequisite) < modules.indexOf('phase4-overseas-pda'))
|
|
|
|
|
}
|
|
|
|
|
assert.match(source, /async ensurePhase4PdaProducerMatrix\(\)/)
|
2026-08-25 15:27:26 +08:00
|
|
|
assert.match(source, /runPhase4ProducerVariant\('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP', true\)/)
|
2026-09-09 13:20:11 +08:00
|
|
|
assert.match(source, /runPhase4ProducerVariant\('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true\)/)
|
2026-09-09 21:13:10 +08:00
|
|
|
assert.match(source, /runPhase4ProducerVariant\('FBA-STANDARD', 'FBA', 'STANDARD', true\)/)
|
2026-09-24 05:08:46 +08:00
|
|
|
assert.match(source, /runPhase4ProducerVariant\('FBA-OPERATIONS', 'FBA', 'STANDARD', true\)/)
|
2026-09-10 21:18:52 +08:00
|
|
|
assert.match(source, /-P4-FBA-STANDARD\(\?:-R\\d\+\)\?:/)
|
2026-08-25 15:27:26 +08:00
|
|
|
assert.match(source, /runPhase4ProducerVariant\('SECONDARY', 'FBA', 'STANDARD', false\)/)
|
|
|
|
|
assert.match(source, /runUntilPickupReady\(this\)/)
|
2026-09-08 23:26:09 +08:00
|
|
|
const variant = source.slice(source.indexOf('async runPhase4ProducerVariant'), source.indexOf('async ensurePhase4PdaProducerMatrix'))
|
2026-09-10 21:18:52 +08:00
|
|
|
assert.match(variant, /this\.phase4ProducerAttempts\.get\(label\)/)
|
|
|
|
|
assert.match(variant, /attempt === 1 \? '' : `-R\$\{attempt\}`/)
|
2026-09-09 01:09:31 +08:00
|
|
|
assert.ok(variant.indexOf('ensureInitialCalculated') < variant.indexOf('phase3-loading-phase4-'))
|
2026-09-09 00:25:34 +08:00
|
|
|
assert.match(variant, /fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
|
2026-09-09 01:09:31 +08:00
|
|
|
assert.doesNotMatch(variant, /inboundFees\.run\(this\)/)
|
2026-09-08 23:26:09 +08:00
|
|
|
assert.ok(variant.indexOf('phase3-outbound-dispatch-phase4-') < variant.indexOf('phase3-container-documents-phase4-'))
|
|
|
|
|
assert.ok(variant.indexOf('phase3-container-documents-phase4-') < variant.indexOf('phase3-carrier-schedule-queries-phase4-'))
|
2026-09-08 23:56:54 +08:00
|
|
|
assert.match(source, /\['phase3-container-documents', \['phase3-outbound-dispatch'\]\]/)
|
|
|
|
|
assert.doesNotMatch(source, /\['phase3-outbound-dispatch', \['phase3-container-documents'/)
|
2026-08-25 15:27:26 +08:00
|
|
|
assert.match(source, /carrierScheduleQueryId/)
|
2026-08-24 19:58:35 +08:00
|
|
|
assert.match(source, /readyForPhase4Pda === true/)
|
2026-08-25 11:25:13 +08:00
|
|
|
assert.equal(modules.indexOf('phase4-overseas-pda-operations'), modules.indexOf('phase4-overseas-pda') + 1)
|
2026-08-24 19:58:35 +08:00
|
|
|
})
|