import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' import { run as runPdaReleases } from './pda-releases.mjs' import { run as runAudit } from './audit.mjs' import { run as runSailingCalendar } from './sailing-calendar.mjs' const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8') function context(handler) { const calls = [] const gaps = [] return { marker: 'ADMIN-API-OFFLINE', calls, gaps, async request(method, path, body) { calls.push({ method, path, body }) return handler(method, path, body) }, async requestMultipart(method, path, form) { calls.push({ method, path, multipart: form }) return handler(method, path, form) }, apiSuccess(_module, _name, response) { assert.equal(response.http, 200) assert.equal(String(response.body?.code), '200') return response.body.data }, assert(_module, name, _response, condition, evidence) { assert.equal(Boolean(condition), true, `${name}: ${evidence}`) return true }, addCoverageGap(module, scenario, name, reason, category, metadata) { gaps.push({ module, scenario, name, reason, category, metadata }) }, } } test('PDA 版本模块注册为默认模块并只读核对双端发布列表', async () => { assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'pda-releases'/) const releases = (app) => [{ id: '1', app, appid: `__UNI__${app}`, packageType: 'apk', versionName: '1.0.0', versionCode: 1, minAppVersionCode: 1, mandatory: false, releaseNotes: 'offline', status: 'PUBLISHED', current: true, sha256: 'a'.repeat(64), sizeBytes: 1024, downloadUrl: '/download', createdAt: '2026-10-01T00:00:00Z', publishedAt: '2026-10-01T00:00:00Z', }] const ctx = context((method, path) => { if (method === 'GET') return { http: 200, body: { code: '200', data: releases(path.endsWith('/domestic') ? 'domestic' : 'overseas') } } if (method === 'POST' && path.includes('/publish')) return { http: 200, body: { code: 'A0404', message: '不存在' } } if (method === 'POST' && path.includes('/withdraw')) return { http: 200, body: { code: 'A0404', message: '不存在' } } return { http: 400, body: { code: 'A0400', message: '文件不能为空' } } }) await runPdaReleases(ctx) assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'POST', 'POST', 'POST']) assert.ok(ctx.calls[0].path.endsWith('/domestic')) assert.ok(ctx.calls[1].path.endsWith('/overseas')) assert.ok(ctx.gaps.some((gap) => gap.name === '真实 APK/WGT 上传、发布、停发与下载' && gap.metadata.blocking === false)) }) test('平台审计模块注册为默认模块,列表不泄漏快照且详情身份一致', async () => { assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'audit'/) const row = { id: '7', eventId: 'e-7', eventType: 'ORDER_SUBMIT', resourceType: 'ORDER', resourceId: '1', accountId: '0', operatorTenantId: '0', userId: '1', occurredAt: '2026-10-01T00:00:00Z', success: true } const detailCtx = context((method, path) => { if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } } if (path.endsWith('/7')) return { http: 200, body: { code: '200', data: { ...row, httpMethod: 'POST' } } } return { http: 200, body: { code: '200', data: { records: [row], page: 1, size: 20, total: 1, pages: 1 } } } }) const withSnapshot = context((method, path) => { if (path.includes('/audit/logs/')) return { http: 200, body: { code: 'A0404', message: '不存在' } } return { http: 200, body: { code: '200', data: { records: [{ ...row, beforeData: '{}' }], page: 1, size: 20, total: 1, pages: 1 } } } }) await runAudit(detailCtx) assert.equal(detailCtx.calls[0].method, 'GET') assert.ok(detailCtx.calls[0].path.includes('/audit/logs?page=1&size=20')) assert.equal(detailCtx.calls[1].path, '/api/platform/v1/audit/logs/7') assert.ok(detailCtx.calls[2].path.endsWith('/999999999999999')) await assert.rejects(runAudit(withSnapshot), /不返回变更前后快照/) }) test('船期日历模块注册为默认模块并核对月历、渠道、模板与异常分配', async () => { assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'sailing-calendar'/) const summary = { actualReceivedCbm: 1, catchableForecastCbm: 2, riskUnmatchedCbm: 0, bookedContainerCount: 1, recommendedContainerCount: 1, pendingContainerCount: 0, lateBatchCount: 0, overCapacity: false, planningCapacityCbm: 10, bookedPlanningCapacityCbm: 5, catchableReceivedCbm: 1, lateReceivedCbm: 0, snapshotAt: null, sailingCount: 1, } const sailing = { id: '11', sailingNo: 'SAIL-11', warehouseId: '1', channelCode: 'SEA', channelName: '海运', templateId: null, channelId: null, etaAt: '2026-10-20T00:00:00', sourceType: 'MANUAL', plannedCapacityCbm: 10, volumeSummary: summary, version: 0, } const ctx = context((method, path) => { if (path.includes('/calendar')) return { http: 200, body: { code: '200', data: { records: [sailing], summary, filterOptions: { warehouses: [], channels: [], departurePorts: [], destinationPorts: [] } } } } if (path.includes('/sailing-channels')) return { http: 200, body: { code: '200', data: [{ id: '1', channelCode: 'SEA', defaultCutoffDays: 3, version: 0 }] } } if (path.includes('/sailing-templates')) return { http: 200, body: { code: '200', data: { records: [{ id: '1', channelId: '1', warehouseId: '1', weekdays: [1, 2], cutoffDays: 3, version: 0 }] } } } if (path.includes('/sailing-assignments/exceptions')) return { http: 200, body: { code: '200', data: { records: [{ assignmentId: '1', volumeCbm: 1 }], total: 1 } } } if (path.endsWith('/11')) return { http: 200, body: { code: '200', data: { sailing: { ...sailing, volumeSummary: summary }, histories: [] } } } if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } } throw new Error(`unexpected ${method} ${path}`) }) await runSailingCalendar(ctx) assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'GET', 'GET', 'GET', 'GET']) assert.ok(ctx.calls[0].path.includes('/sailings/calendar?month=')) assert.ok(ctx.calls[1].path.endsWith('/sailing-channels')) assert.ok(ctx.calls[2].path.includes('/sailing-templates?page=1&size=200')) 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), /月历记录、汇总与筛选选项结构完整/) })