2026-09-02 19:29:23 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import test from 'node:test'
|
2026-10-08 19:09:30 +08:00
|
|
|
import { AcceptanceRun, transportFailureDetails } from './admin-test-env-acceptance.mjs'
|
2026-09-02 19:29:23 +08:00
|
|
|
|
|
|
|
|
function runContext() {
|
|
|
|
|
return new AcceptanceRun({ gateway: 'http://acceptance.invalid', overseasGateway: 'http://overseas.invalid' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('read-only HTTP requests retry transient network failures', async () => {
|
|
|
|
|
const originalFetch = globalThis.fetch
|
|
|
|
|
let attempts = 0
|
|
|
|
|
globalThis.fetch = async () => {
|
|
|
|
|
attempts += 1
|
|
|
|
|
if (attempts < 3) throw new TypeError('fetch failed')
|
|
|
|
|
return new Response(JSON.stringify({ code: '200', data: { ready: true } }), {
|
|
|
|
|
status: 200,
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const response = await runContext().request('GET', '/api/readiness')
|
|
|
|
|
assert.equal(attempts, 3)
|
|
|
|
|
assert.equal(response.http, 200)
|
|
|
|
|
assert.equal(response.body?.data?.ready, true)
|
|
|
|
|
} finally {
|
|
|
|
|
globalThis.fetch = originalFetch
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('mutating HTTP requests never retry an ambiguous network failure', async () => {
|
|
|
|
|
const originalFetch = globalThis.fetch
|
|
|
|
|
let attempts = 0
|
|
|
|
|
globalThis.fetch = async () => {
|
|
|
|
|
attempts += 1
|
2026-10-08 19:09:30 +08:00
|
|
|
throw new TypeError('fetch failed', { cause: Object.assign(new Error('private host and token'), { code: 'ECONNRESET', syscall: 'read' }) })
|
2026-09-02 19:29:23 +08:00
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const response = await runContext().request('POST', '/api/orders', { value: 1 })
|
|
|
|
|
assert.equal(attempts, 1)
|
|
|
|
|
assert.equal(response.http, 0)
|
|
|
|
|
assert.equal(response.raw, 'fetch failed')
|
2026-10-08 19:09:30 +08:00
|
|
|
assert.equal(response.transportError.causes[1].code, 'ECONNRESET')
|
|
|
|
|
const run = runContext(); run.options.quiet = true
|
|
|
|
|
run.addStep('orders', 'conflict request', response, false)
|
|
|
|
|
assert.deepEqual(run.steps[0].transportError, response.transportError)
|
|
|
|
|
assert.ok(!JSON.stringify(run.steps).includes('private host and token'))
|
2026-09-02 19:29:23 +08:00
|
|
|
} finally {
|
|
|
|
|
globalThis.fetch = originalFetch
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-10-08 19:09:30 +08:00
|
|
|
|
|
|
|
|
test('transport diagnostics retain nested connection codes without addresses or exception messages', () => {
|
|
|
|
|
const cause = Object.assign(new Error('secret endpoint'), { code: 'ETIMEDOUT', address: 'private', syscall: 'connect' })
|
|
|
|
|
const error = new TypeError('fetch failed', { cause: new AggregateError([cause]) })
|
|
|
|
|
cause.cause = error
|
|
|
|
|
const result = transportFailureDetails(error)
|
|
|
|
|
assert.equal(result.causes.length, 3)
|
|
|
|
|
assert.equal(result.causes[2].code, 'ETIMEDOUT')
|
|
|
|
|
assert.doesNotMatch(JSON.stringify(result), /secret|private|endpoint/)
|
|
|
|
|
})
|