From a0ef76d9d251d463fb0379e0b4f1b1ec77072e64 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 26 Jun 2026 15:28:40 +0800 Subject: [PATCH] =?UTF-8?q?test(delete-utils):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=87=8D=E8=A9=A6=E3=80=81=E7=8B=80=E6=85=8B=E5=88=86=E6=B5=81?= =?UTF-8?q?=E8=88=87=E4=BD=B5=E7=99=BC=E4=B8=8A=E9=99=90=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 涵蓋 isRetryableStatus、deleteWithRetry(成功/暫時性重試/永久性即止/網路例外用盡) 與 runWithConcurrency(順序、併發上限、空陣列)。測試共 75 項全數通過。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/test/delete-utils.test.js | 97 +++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 app/test/delete-utils.test.js diff --git a/app/test/delete-utils.test.js b/app/test/delete-utils.test.js new file mode 100644 index 0000000..e2814cd --- /dev/null +++ b/app/test/delete-utils.test.js @@ -0,0 +1,97 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + isRetryableStatus, + deleteWithRetry, + runWithConcurrency, +} from '../delete-utils.js' + +const noSleep = () => Promise.resolve() + +test('isRetryableStatus 僅對暫時性狀態碼回傳 true', () => { + for (const s of [429, 502, 503, 504]) assert.equal(isRetryableStatus(s), true) + for (const s of [200, 204, 400, 401, 403, 404, 500]) { + assert.equal(isRetryableStatus(s), false) + } +}) + +test('deleteWithRetry 成功(204)時不重試', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + return 204 + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 204) + assert.equal(calls, 1) +}) + +test('deleteWithRetry 對暫時性錯誤重試後成功', async () => { + const statuses = [503, 503, 204] + let i = 0 + const client = { + async deleteResource() { + return statuses[i++] + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 204) + assert.equal(result.attempts, 3) +}) + +test('deleteWithRetry 對永久性錯誤(403)立即停止重試', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + return 403 + }, + } + const result = await deleteWithRetry(client, 'u', { sleep: noSleep }) + assert.equal(result.status, 403) + assert.equal(calls, 1) +}) + +test('deleteWithRetry 對網路例外重試至次數用盡並回傳 error', async () => { + let calls = 0 + const client = { + async deleteResource() { + calls += 1 + throw new Error('network down') + }, + } + const result = await deleteWithRetry(client, 'u', { attempts: 3, sleep: noSleep }) + assert.equal(result.status, 0) + assert.match(result.error.message, /network down/) + assert.equal(calls, 3) +}) + +test('runWithConcurrency 回傳對應輸入順序的結果', async () => { + const items = [1, 2, 3, 4, 5] + const out = await runWithConcurrency(items, async (n) => n * 2, 2) + assert.deepEqual(out, [2, 4, 6, 8, 10]) +}) + +test('runWithConcurrency 不超過併發上限', async () => { + let active = 0 + let maxActive = 0 + const items = Array.from({ length: 10 }, (_, i) => i) + await runWithConcurrency( + items, + async () => { + active += 1 + maxActive = Math.max(maxActive, active) + await Promise.resolve() + active -= 1 + }, + 3, + ) + assert.ok(maxActive <= 3, `maxActive=${maxActive} 應 <= 3`) +}) + +test('runWithConcurrency 對空陣列回傳空陣列', async () => { + const out = await runWithConcurrency([], async () => 1, 4) + assert.deepEqual(out, []) +})