25d5a5a5e3
新增纯 TS force 节流守卫 asin-force-throttle:为每个 taskId 维护 TTL 冷却窗口,窗口内不重复发 force 请求(避免反复触发后端生成文件),到期 后允许重试,任务终态后 clear 立即释放记录。BrandSimilarAsinTab 的 refreshTaskProgress 改为按 pendingFileTaskIds 逐项查守卫决定本轮是否 带 force,请求成功后整批进入冷却。8 个测试覆盖默认、批量、幂等、空、 单元素、200 任务溢出、非法输入与时钟故障恢复。
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createAsinForceThrottle } from '../src/shared/asin-force-throttle.ts'
|
|
|
|
function clock(start = 1000) {
|
|
let now = start
|
|
return {
|
|
now: () => now,
|
|
tick: (ms: number) => {
|
|
now += ms
|
|
},
|
|
}
|
|
}
|
|
|
|
test('test_task_085_asin_polling_normal_default_path', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
assert.equal(throttle.shouldForce(101), true)
|
|
throttle.markForce(101)
|
|
assert.equal(throttle.shouldForce(101), false)
|
|
assert.equal(throttle.isThrottled(101), true)
|
|
c.tick(29_999)
|
|
assert.equal(throttle.shouldForce(101), false)
|
|
c.tick(2)
|
|
assert.equal(throttle.shouldForce(101), true)
|
|
assert.equal(throttle.isThrottled(101), false)
|
|
const stats = throttle.stats()
|
|
assert.equal(stats.forcedCount, 1)
|
|
assert.equal(stats.skippedCount, 2)
|
|
assert.equal(stats.activeCount, 0)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_normal_multiple_items', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
const ids = [201, 202, 203, 204, 205]
|
|
for (const id of ids) throttle.markForce(id)
|
|
assert.equal(throttle.activeCount, 5)
|
|
for (const id of ids) assert.equal(throttle.shouldForce(id), false)
|
|
// 不同 taskId 的 TTL 相互独立
|
|
c.tick(31_000)
|
|
for (const id of ids) assert.equal(throttle.shouldForce(id), true)
|
|
throttle.markForce(202)
|
|
assert.equal(throttle.shouldForce(202), false)
|
|
assert.equal(throttle.shouldForce(201), true)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_normal_repeated_operation_is_idempotent', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
throttle.markForce(301)
|
|
throttle.markForce(301)
|
|
throttle.markForce(301)
|
|
assert.equal(throttle.activeCount, 1)
|
|
assert.equal(throttle.shouldForce(301), false)
|
|
assert.equal(throttle.stats().forcedCount, 1)
|
|
// 重复检查不改变状态
|
|
assert.equal(throttle.shouldForce(301), false)
|
|
assert.equal(throttle.isThrottled(301), true)
|
|
assert.equal(throttle.stats().forcedCount, 1)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_boundary_empty_input', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
assert.equal(throttle.activeCount, 0)
|
|
assert.equal(throttle.shouldForce(401), true)
|
|
throttle.clear(401)
|
|
assert.equal(throttle.activeCount, 0)
|
|
// clear 不存在的 taskId 无副作用
|
|
throttle.clearAll()
|
|
assert.deepEqual(throttle.stats(), { forcedCount: 0, skippedCount: 0, activeCount: 0 })
|
|
})
|
|
|
|
test('test_task_085_asin_polling_boundary_single_item', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 1000, now: c.now })
|
|
assert.equal(throttle.shouldForce(501), true)
|
|
throttle.markForce(501)
|
|
assert.equal(throttle.isThrottled(501), true)
|
|
assert.equal(throttle.activeCount, 1)
|
|
c.tick(1000)
|
|
assert.equal(throttle.isThrottled(501), false)
|
|
assert.equal(throttle.activeCount, 0)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_boundary_limit_and_overflow', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
// 大量任务:所有任务都被记录,未标记的任务不被节流
|
|
for (let i = 1; i <= 200; i++) throttle.markForce(i)
|
|
assert.equal(throttle.activeCount, 200)
|
|
assert.equal(throttle.shouldForce(1), false)
|
|
assert.equal(throttle.shouldForce(200), false)
|
|
assert.equal(throttle.shouldForce(201), true)
|
|
// TTL 到期后整批可重新 force(重试窗口),记录不无界增长
|
|
c.tick(60_001)
|
|
assert.equal(throttle.shouldForce(1), true)
|
|
assert.equal(throttle.activeCount, 0)
|
|
throttle.markForce(1)
|
|
assert.equal(throttle.activeCount, 1)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_invalid_input_rejected', () => {
|
|
const c = clock()
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: c.now })
|
|
assert.throws(() => throttle.markForce(0), /taskId 必须是正整数/)
|
|
assert.throws(() => throttle.markForce(-1), /taskId 必须是正整数/)
|
|
assert.throws(() => throttle.markForce(NaN), /taskId 必须是正整数/)
|
|
assert.throws(() => throttle.markForce(1.5), /taskId 必须是正整数/)
|
|
// 读取路径宽容:非法 id 安全返回
|
|
assert.equal(throttle.shouldForce(0), false)
|
|
assert.equal(throttle.isThrottled(-1), false)
|
|
assert.equal(throttle.clear(0), false)
|
|
assert.throws(() => createAsinForceThrottle({ ttlMs: 0 }), /ttlMs 必须为正数/)
|
|
})
|
|
|
|
test('test_task_085_asin_polling_dependency_failure_releases_resources', () => {
|
|
let now = 1000
|
|
let broken = false
|
|
const faultyNow = () => {
|
|
if (broken) throw new Error('clock down')
|
|
return now
|
|
}
|
|
const throttle = createAsinForceThrottle({ ttlMs: 30_000, now: faultyNow })
|
|
throttle.markForce(701)
|
|
assert.equal(throttle.activeCount, 1)
|
|
// 时钟故障时读取抛错,但记录不丢失
|
|
broken = true
|
|
assert.throws(() => throttle.shouldForce(701), /clock down/)
|
|
assert.throws(() => throttle.isThrottled(701), /clock down/)
|
|
assert.throws(() => throttle.stats(), /clock down/)
|
|
// 恢复后同一实例继续工作,记录仍在
|
|
broken = false
|
|
assert.equal(throttle.isThrottled(701), true)
|
|
assert.equal(throttle.shouldForce(701), false)
|
|
// 终态后 clear 释放记录
|
|
throttle.clear(701)
|
|
assert.equal(throttle.activeCount, 0)
|
|
assert.equal(throttle.shouldForce(701), true)
|
|
})
|