import test from 'node:test' import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { uploadEvidence } from './phase3-loading.mjs' const context = () => ({ marker: 'CURRENT-SEAL', options: { gateway: 'https://offline.invalid' }, pdaToken: 'secret-test-token', token: 'different-admin-token', createdResources: {} }) const invoke = ctx => uploadEvidence(ctx, '2105979722985902081', '193000000000200001', 'SEAL', 0, 'SEAL', { sealNo: 'SEAL-UNIQUE' }) async function withTransport(handler, check) { const originalFetch = globalThis.fetch, originalTimeout = globalThis.setTimeout const waits = [] globalThis.fetch = handler globalThis.setTimeout = (callback, delay) => { waits.push(delay); queueMicrotask(callback); return 0 } try { await check(waits) } finally { globalThis.fetch = originalFetch; globalThis.setTimeout = originalTimeout } } function lost(code = 'ECONNRESET') { return new TypeError('fetch failed https://private-host.invalid secret-test-token', { cause: { code, address: 'private-host.invalid' } }) } test('committed upload with lost response replays frozen content/key and creates exactly one modeled evidence row', async () => { const ctx = context(), records = new Map(), calls = [] let mutations = 0 await withTransport(async (url, options) => { const file = options.body.get('file') const hash = createHash('sha256').update(`${url}|${file.name}|${file.type}|${file.size}|`) .update(Buffer.from(await file.arrayBuffer())).digest('hex') const key = options.headers['Idempotency-Key'] calls.push({ url, key, hash, auth: options.headers.Authorization, body: options.body }) if (records.has(key)) assert.equal(records.get(key).hash, hash, 'same-key different payload must be rejected') else { records.set(key, { hash, evidenceId: 'persisted-evidence' }); mutations++ } if (calls.length === 1) { ctx.marker = 'CHANGED'; ctx.pdaToken = 'new-token'; ctx.options.gateway = 'https://changed.invalid' throw lost() } return new Response(JSON.stringify({ code: '200', data: { evidenceId: records.get(key).evidenceId } }), { status: 200 }) }, async waits => { const result = await invoke(ctx) assert.equal(result.body.data.evidenceId, 'persisted-evidence') assert.equal(mutations, 1); assert.equal(records.size, 1); assert.equal(calls.length, 2) assert.deepEqual(calls[0], calls[1]); assert.deepEqual(waits, [250]) assert.deepEqual(result.transportAttempts.map(item => item.outcome), ['NO_HTTP_RESPONSE', 'HTTP_RESPONSE']) assert.equal(result.transportAttempts[0].causeCode, 'ECONNRESET') const evidence = JSON.stringify(ctx.createdResources) for (const secret of ['secret-test-token', 'new-token', 'private-host', 'offline.invalid', 'changed.invalid']) assert.equal(evidence.includes(secret), false) }) }) test('persistent no-response failures stop at three attempts with bounded backoff and sanitized cause', async () => { let calls = 0 await withTransport(async () => { calls++; throw lost('UND_ERR_CONNECT_TIMEOUT') }, async waits => { const result = await invoke(context()) assert.equal(calls, 3); assert.equal(result.http, 0); assert.equal(result.body, null) assert.deepEqual(waits, [250, 500]); assert.equal(result.transportAttempts.length, 3) assert.match(result.raw, /UND_ERR_CONNECT_TIMEOUT/) assert.doesNotMatch(JSON.stringify(result), /private-host|secret-test-token|address/) }) }) for (const [http, code] of [[200, 'A0409'], [200, 'A0422'], [503, 'S0503'], [403, 'A0403']]) { test(`HTTP ${http} business ${code} is returned without retry`, async () => { let calls = 0 await withTransport(async () => { calls++; return new Response(JSON.stringify({ code, message: 'rejected', requestId: 'request' }), { status: http }) }, async waits => { const result = await invoke(context()) assert.equal(calls, 1); assert.deepEqual(waits, []); assert.equal(result.http, http) assert.equal(result.body.code, code); assert.equal(result.requestId, 'request') }) }) } test('received HTTP response with unreadable body is not reclassified as no-response retry', async () => { let calls = 0 await withTransport(async () => { calls++; return { status: 502, headers: new Headers({ 'x-request-id': 'edge-request' }), text: async () => { throw lost() } } }, async waits => { const result = await invoke(context()) assert.equal(calls, 1); assert.deepEqual(waits, []); assert.equal(result.http, 502) assert.equal(result.requestId, 'edge-request'); assert.equal(result.transportAttempts[0].outcome, 'RESPONSE_READ_FAILED') }) }) test('unrecognized cause code containing sensitive material is omitted', async () => { await withTransport(async () => { throw lost('secret-test-token https://private-host.invalid') }, async () => { const result = await invoke(context()) assert.ok(result.transportAttempts.every(item => item.causeCode === null)) assert.doesNotMatch(JSON.stringify(result), /secret-test-token|private-host/) }) })