import assert from 'node:assert/strict' import test from 'node:test' import { pickerFixtureSuffix, createPicker, disablePicker, pickerSessionHandoff } from './phase4-overseas-pda.mjs' import { verifyPdaWarehouseRevocation } from './unified-pda-session-revocation.mjs' test('fixed PDA actors are leased separately and only logged out, never modified', async () => { const actors = ['11', '12'].map(membershipId => ({ membershipId, warehouseId: '91', username: `test-${membershipId}`, password: 'fixture-only' })) const calls = [], gaps = [] const context = { options: { fixedCustomerId: '101', fixedPdaActors: actors }, async request(method, path, body) { calls.push({ method, path }) let data = {} if (path.includes('/iam/users?')) data = { total: 2, records: actors.map(actor => ({ ...actor, overseasWarehouseId: '91', tenantId: '0', status: 'ACTIVE', roleIds: ['7'] })) } if (path === '/api/auth/login') data = { accessToken: body.loginId, refreshToken: `refresh-${body.loginId}` } return { http: 200, body: { code: '200', data } } }, apiSuccess(_module, _name, result) { return result.body.data }, addStep() {}, addCoverageGap(...args) { gaps.push(args) }, } const primary = await createPicker(context, 1, '91', { role: { id: '7' } }) const secondary = await createPicker(context, 2, '91', { role: { id: '7' } }) assert.notEqual(primary.membershipId, secondary.membershipId) assert.equal(await createPicker(context, 3, '91', { role: { id: '7' } }), null) const handoff = pickerSessionHandoff(primary) assert.equal(handoff.reused, true) assert.equal(handoff.password, undefined) await verifyPdaWarehouseRevocation(context, handoff) assert.equal(gaps.length, 1) await disablePicker(context, handoff, 1) await disablePicker(context, primary, 1) await disablePicker(context, secondary, 2) assert.equal(context.fixedPdaLeases.size, 0) assert.equal(calls.filter(call => call.path === '/api/auth/logout').length, 2) assert.ok(calls.every(call => call.method === 'GET' || ['/api/auth/login', '/api/auth/logout'].includes(call.path))) }) test('missing fixed PDA pool stops before network or phone allocation', async () => { const context = { options: { fixedCustomerId: '101' }, addStep() {}, request() { throw Error('unexpected HTTP') }, findAvailablePhone() { throw Error('unexpected allocation') } } assert.equal(await createPicker(context, 1, '91', { role: { id: '7' } }), null) }) test('failed PDA projection checks logout and retains leases on rejected or uncertain logout', async () => { for (const logout of ['success', 'rejected', 'throws']) { const actor = { membershipId: '11', warehouseId: '91', username: 'fixture-actor', password: 'fixture-only' } const steps = [], calls = [] const context = { options: { fixedCustomerId: '101', fixedPdaActors: [actor] }, async request(method, path) { calls.push(path) if (path.includes('/iam/users?')) return { body: { data: { records: [{ ...actor, overseasWarehouseId: '91', tenantId: '0', status: 'ACTIVE', roleIds: ['7'] }] } } } if (path === '/api/auth/login') return { body: { data: { accessToken: 'fixture-token', refreshToken: 'fixture-refresh' } } } if (path === '/api/auth/logout' && logout === 'throws') throw Error('network failure') return { http: 200, body: { code: path === '/api/auth/logout' && logout === 'success' ? '200' : 'A0403' } } }, apiSuccess(_module, _name, response) { return response.body.data }, addStep(...args) { steps.push(args) }, } assert.equal(await createPicker(context, 1, '91', { role: { id: '7' } }), null) assert.equal(calls.filter(path => path === '/api/auth/logout').length, 1) assert.equal(context.fixedPdaLeases.has('11'), logout !== 'success') assert.ok(steps.some(step => step[1].includes('核验失败') && step[3] === false)) if (logout !== 'success') { assert.ok(steps.some(step => step[1].includes('退出固定PDA') && step[3] === false)) const before = calls.length assert.equal(await createPicker(context, 2, '91', { role: { id: '7' } }), null) assert.equal(calls.length, before) } } }) test('logout exception does not prevent remaining fixed actors from being cleaned up', async () => { const calls = [] const context = { fixedPdaLeases: new Set(['11', '12']), addStep() {}, async request(_method, _path, body) { calls.push(body.refreshToken) if (body.refreshToken === 'first') throw Error('network failure') return { http: 200, body: { code: '200' } } } } await disablePicker(context, { membershipId: '11', refreshToken: 'first', reused: true }, 1) await disablePicker(context, { membershipId: '12', refreshToken: 'second', reused: true }, 2) assert.deepEqual(calls, ['first', 'second']) assert.deepEqual([...context.fixedPdaLeases], ['11']) }) test('长场景尾部相同时PDA账号仍保留完整运行标识的区别', () => { const first = { marker: 'ADMIN-API-20261005134027677-COMPLETED-F40-PDA' } const second = { marker: 'ADMIN-API-20261005170914227-COMPLETED-F40-PDA' } const oldSuffix = value => value.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16) assert.equal(oldSuffix(first), oldSuffix(second)) assert.notEqual(pickerFixtureSuffix(first), pickerFixtureSuffix(second)) assert.equal(pickerFixtureSuffix(first), pickerFixtureSuffix({ ...first })) assert.match(pickerFixtureSuffix(first), /^[0-9A-F]{32}$/) assert.notEqual(pickerFixtureSuffix(first), pickerFixtureSuffix({ ...first, pdaContainerSuffix: 'B' })) assert.throws(() => pickerFixtureSuffix({ marker: '' })) })