diff --git a/frontend-vue/src/shared/task-progress-config.ts b/frontend-vue/src/shared/task-progress-config.ts index f2ec1f11..c6666484 100644 --- a/frontend-vue/src/shared/task-progress-config.ts +++ b/frontend-vue/src/shared/task-progress-config.ts @@ -66,9 +66,9 @@ export function getTaskProgressCacheTtlMs() { return TASK_PROGRESS_VISIBLE_CACHE_MILLIS; } -/** 第 attempt 次退避延迟:base × factor^attempt,封顶 max;attempt 非法时按首次(0)处理 */ -export function getTaskPollBackoffMs(attempt: number) { - const safe = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0; +/** 第 attempt 次退避延迟:base × factor^attempt,封顶 max;attempt 省略/非法时按首次(0)处理 */ +export function getTaskPollBackoffMs(attempt?: number) { + const safe = Number.isFinite(attempt) && (attempt as number) > 0 ? Math.floor(attempt as number) : 0; const factor = getTaskPollBackoffFactor(); const scaled = config.backoffBaseMs * Math.pow(factor, safe); return scaled > config.backoffMaxMs ? config.backoffMaxMs : scaled; diff --git a/frontend-vue/tests/task-poll-backoff-sequence.test.ts b/frontend-vue/tests/task-poll-backoff-sequence.test.ts new file mode 100644 index 00000000..038edc43 --- /dev/null +++ b/frontend-vue/tests/task-poll-backoff-sequence.test.ts @@ -0,0 +1,60 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + getTaskPollBackoffMs, + configureTaskPolling, + resetTaskPollingConfig, +} from '../src/shared/task-progress-config.ts' + +test('test_backoff_attempt0_500', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMs(0), 500) +}) + +test('test_backoff_attempt1_1000', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMs(1), 1000) +}) + +test('test_backoff_attempt2_2000', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMs(2), 2000) +}) + +test('test_backoff_sequence_monotonic', () => { + resetTaskPollingConfig() + const sequence = [0, 1, 2, 3, 4, 5].map((n) => getTaskPollBackoffMs(n)) + assert.deepEqual(sequence, [500, 1000, 2000, 4000, 8000, 10000]) + for (let i = 1; i < sequence.length; i++) { + assert.ok(sequence[i] >= sequence[i - 1], '序列单调不减') + } +}) + +test('test_backoff_capped_at_max', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMs(5), 10000) + assert.equal(getTaskPollBackoffMs(10), 10000) + assert.equal(getTaskPollBackoffMs(50), 10000) +}) + +test('test_backoff_no_attempt_default', () => { + resetTaskPollingConfig() + const defaulted = getTaskPollBackoffMs(undefined as never) + assert.equal(defaulted, 500, '省略参数视为首次退避') +}) + +test('test_backoff_negative_attempt_zero', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMs(-1), 500) + assert.equal(getTaskPollBackoffMs(-100), 500) +}) + +test('test_backoff_custom_factor', () => { + resetTaskPollingConfig() + configureTaskPolling({ backoffFactor: 3 }) + assert.equal(getTaskPollBackoffMs(0), 500) + assert.equal(getTaskPollBackoffMs(1), 1500) + assert.equal(getTaskPollBackoffMs(2), 4500) + assert.equal(getTaskPollBackoffMs(3), 10000, '超过 max 封顶') + resetTaskPollingConfig() +})