From 97333da34cce48cdfcfa1bbd5e8d48340802d2ef Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 30 Aug 2026 01:22:55 +0800 Subject: [PATCH] test(ql3): bound provider convergence retries --- ...edential-test-kubernetes-live-contract.cjs | 26 ++++++++++++----- ...dentialTestKubernetesLiveContract.test.cjs | 28 +++++++++++++++++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs b/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs index 53ea733a..5026b206 100644 --- a/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs +++ b/scripts/ql3-provider-credential-test-kubernetes-live-contract.cjs @@ -1105,11 +1105,24 @@ request.on('timeout',()=>request.destroy(Object.assign(new Error('timeout'),{cod return evidence; } -async function retryProviderEvidence(read, pause = undefined) { +async function retryProviderEvidence(read, options = {}) { assert.equal(typeof read, 'function'); - assert.ok(pause === undefined || typeof pause === 'function'); + assert.equal(typeof options, 'object'); + assert.ok(options !== null && !Array.isArray(options)); + const pause = + options.pause ?? + ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))); + const now = options.now ?? Date.now; + const intervalMs = options.intervalMs ?? 1_000; + const timeoutMs = options.timeoutMs ?? 3 * 60_000; + const maxAttempts = options.maxAttempts ?? 16; + assert.equal(typeof pause, 'function'); + assert.equal(typeof now, 'function'); + assert.ok(Number.isSafeInteger(intervalMs) && intervalMs >= 0); + assert.ok(Number.isSafeInteger(timeoutMs) && timeoutMs > 0); + assert.ok(Number.isSafeInteger(maxAttempts) && maxAttempts > 0); + const startedAt = now(); let lastError; - const maxAttempts = 8; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { return await read(); @@ -1119,13 +1132,12 @@ async function retryProviderEvidence(read, pause = undefined) { !/\b(?:ECONNREFUSED|ECONNRESET|ETIMEDOUT|TIMEOUT)\b/.test( error instanceof Error ? error.message : String(error), ) || - attempt === maxAttempts + attempt === maxAttempts || + now() - startedAt >= timeoutMs ) { throw error; } - await ( - pause ?? (() => new Promise((resolve) => setTimeout(resolve, 1_000))) - )(); + await pause(intervalMs); } } throw lastError; diff --git a/test/back/ql3ProviderCredentialTestKubernetesLiveContract.test.cjs b/test/back/ql3ProviderCredentialTestKubernetesLiveContract.test.cjs index 5087229f..cfefb63d 100644 --- a/test/back/ql3ProviderCredentialTestKubernetesLiveContract.test.cjs +++ b/test/back/ql3ProviderCredentialTestKubernetesLiveContract.test.cjs @@ -377,7 +377,7 @@ test('retries only bounded transient provider evidence failures', async () => { if (attempts < 3) throw new Error('{"code":"ECONNREFUSED"}'); return { requestCount: 1 }; }, - async () => {}, + { pause: async () => {} }, ); assert.deepEqual(evidence, { requestCount: 1 }); assert.equal(attempts, 3); @@ -386,7 +386,7 @@ test('retries only bounded transient provider evidence failures', async () => { async () => { throw new Error('invalid evidence schema'); }, - async () => {}, + { pause: async () => {} }, ), /invalid evidence schema/, ); @@ -398,11 +398,33 @@ test('retries only bounded transient provider evidence failures', async () => { attempts += 1; throw new Error('{"code":"ECONNREFUSED"}'); }, - async () => {}, + { maxAttempts: 8, pause: async () => {} }, ), /ECONNREFUSED/, ); assert.equal(attempts, 8); + + attempts = 0; + let elapsedMs = 0; + await assert.rejects( + retryProviderEvidence( + async () => { + attempts += 1; + throw new Error('{"code":"ETIMEDOUT"}'); + }, + { + intervalMs: 1_000, + maxAttempts: 100, + now: () => elapsedMs, + pause: async (delayMs) => { + elapsedMs += delayMs; + }, + timeoutMs: 3_000, + }, + ), + /ETIMEDOUT/, + ); + assert.equal(attempts, 4); }); test('provider fixture logs only generation and authorization decision', () => {