Files
crawler-plugin/frontend-vue/tests/progress-response-cache.test.ts
T
huangzd1997 83b93eb39b task-82: 为进度响应 Map 增加 TTL 清理与最大条目数
新增纯 TS 进度响应缓存 progress-response-cache:写入后 ttlMs 过期、
条目数超 maxEntries 驱逐最旧、读取/枚举/清理惰性移除过期条目,时钟
可注入以便测试 TTL 边界。set 对非法 key fail-fast 抛错,时钟故障时
零部分写入,clear 释放全部条目。useTaskProgressLoop 新增可选
progressCache 选项,每轮响应快照写入有界缓存(默认不启用)。
2026-08-30 22:42:08 +08:00

210 lines
6.2 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createProgressResponseCache } from '../src/shared/progress-response-cache.ts'
interface FakeDetail {
id: number
name: string
status: string
}
function detail(id: number): FakeDetail {
return { id, name: `task-${id}`, status: id % 2 === 0 ? 'SUCCESS' : 'RUNNING' }
}
function clock(start = 1000) {
let now = start
return {
now: () => now,
tick: (ms: number) => {
now += ms
},
}
}
test('test_task_082_progress_cleanup_normal_default_path', () => {
const c = clock()
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 30_000,
maxEntries: 500,
now: c.now,
})
cache.set(1, detail(1))
assert.deepEqual(cache.get(1), detail(1))
assert.equal(cache.has(1), true)
assert.equal(cache.size, 1)
assert.deepEqual(cache.entries(), [[1, detail(1)]])
})
test('test_task_082_progress_cleanup_normal_multiple_items', () => {
const c = clock()
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 60_000,
maxEntries: 10,
now: c.now,
})
for (let i = 1; i <= 10; i++) cache.set(i, detail(i))
assert.equal(cache.size, 10)
assert.deepEqual(
cache.entries().map(([k]) => k),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
)
assert.deepEqual(
cache.entries().map(([, v]) => v.id),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
)
})
test('test_task_082_progress_cleanup_normal_repeated_operation_is_idempotent', () => {
const c = clock()
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 60_000,
maxEntries: 10,
now: c.now,
})
cache.set(1, detail(1))
cache.set(1, { ...detail(1), status: 'SUCCESS' })
assert.equal(cache.size, 1)
assert.equal(cache.get(1)?.status, 'SUCCESS')
cache.set(1, { ...detail(1), status: 'SUCCESS' })
assert.equal(cache.size, 1)
assert.deepEqual(
cache.entries().map(([k]) => k),
[1],
)
// 更新已存在条目不改变插入顺序
cache.set(2, detail(2))
cache.set(1, detail(1))
assert.deepEqual(
cache.entries().map(([k]) => k),
[1, 2],
)
})
test('test_task_082_progress_cleanup_boundary_empty_input', () => {
const c = clock()
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 1000,
maxEntries: 5,
now: c.now,
})
assert.equal(cache.size, 0)
assert.equal(cache.get(1), undefined)
assert.equal(cache.has(1), false)
assert.deepEqual(cache.entries(), [])
assert.equal(cache.purgeExpired(), 0)
cache.clear()
assert.equal(cache.size, 0)
assert.equal(cache.purgeExpired(), 0)
})
test('test_task_082_progress_cleanup_boundary_single_item', () => {
const c = clock()
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 1000,
maxEntries: 5,
now: c.now,
})
cache.set(7, detail(7))
assert.deepEqual(cache.get(7), detail(7))
assert.equal(cache.size, 1)
assert.equal(cache.has(7), true)
c.tick(1001)
assert.equal(cache.get(7), undefined)
assert.equal(cache.has(7), false)
assert.equal(cache.size, 0)
assert.equal(cache.purgeExpired(), 0)
})
test('test_task_082_progress_cleanup_boundary_limit_and_overflow', () => {
// 条目数超限:驱逐最旧,缓存有界
const c1 = clock()
const byEntries = createProgressResponseCache<FakeDetail>({
ttlMs: 60_000,
maxEntries: 3,
now: c1.now,
})
for (let i = 1; i <= 5; i++) byEntries.set(i, detail(i))
assert.equal(byEntries.size, 3)
assert.deepEqual(
byEntries.entries().map(([k]) => k),
[3, 4, 5],
)
assert.equal(byEntries.get(1), undefined)
assert.equal(byEntries.get(2), undefined)
// TTL 清理:到期条目被 purgeExpired 清除
const c2 = clock()
const byTtl = createProgressResponseCache<FakeDetail>({
ttlMs: 10_000,
maxEntries: 100,
now: c2.now,
})
byTtl.set(1, detail(1))
byTtl.set(2, detail(2))
c2.tick(5_000)
byTtl.set(3, detail(3))
assert.equal(byTtl.purgeExpired(), 0)
c2.tick(5_001)
assert.equal(byTtl.purgeExpired(), 2)
assert.deepEqual(
byTtl.entries().map(([k]) => k),
[3],
)
assert.equal(byTtl.size, 1)
// 读取路径惰性清理
c2.tick(5_000)
assert.equal(byTtl.get(3), undefined)
assert.equal(byTtl.size, 0)
assert.equal(byTtl.purgeExpired(), 0)
})
test('test_task_082_progress_cleanup_invalid_input_rejected', () => {
const c = clock()
assert.throws(() => createProgressResponseCache({ ttlMs: 0, maxEntries: 5 }), /ttlMs 必须为正数/)
assert.throws(() => createProgressResponseCache({ ttlMs: -1, maxEntries: 5 }), /ttlMs 必须为正数/)
assert.throws(() => createProgressResponseCache({ ttlMs: 1000, maxEntries: 0 }), /maxEntries 必须为正数/)
assert.throws(() => createProgressResponseCache({ ttlMs: 1000, maxEntries: -5 }), /maxEntries 必须为正数/)
const cache = createProgressResponseCache<FakeDetail>({ ttlMs: 1000, maxEntries: 5, now: c.now })
assert.throws(() => cache.set(0, detail(0)), /key 必须是正整数/)
assert.throws(() => cache.set(-1, detail(-1)), /key 必须是正整数/)
assert.throws(() => cache.set(NaN, detail(NaN)), /key 必须是正整数/)
assert.throws(() => cache.set(1.5, detail(1.5)), /key 必须是正整数/)
// 读取路径宽容:非法 key 不抛错
assert.equal(cache.get(-1), undefined)
assert.equal(cache.has(0), false)
assert.equal(cache.delete(0), false)
})
test('test_task_082_progress_cleanup_dependency_failure_releases_resources', () => {
const c = clock()
let broken = true
const faultyNow = () => {
if (broken) throw new Error('clock down')
return c.now()
}
const cache = createProgressResponseCache<FakeDetail>({
ttlMs: 1000,
maxEntries: 5,
now: faultyNow,
})
// 时钟故障时 set 失败,缓存零状态变更
assert.throws(() => cache.set(1, detail(1)), /clock down/)
assert.equal(cache.size, 0)
// 故障恢复后同一实例继续工作
broken = false
cache.set(1, detail(1))
assert.equal(cache.get(1)?.id, 1)
// 时钟故障时读取/清理抛错但不破坏条目
broken = true
assert.throws(() => cache.get(1), /clock down/)
assert.throws(() => cache.purgeExpired(), /clock down/)
assert.throws(() => cache.entries(), /clock down/)
broken = false
assert.equal(cache.get(1)?.id, 1)
assert.equal(cache.size, 1)
// clear 释放全部资源
cache.clear()
assert.equal(cache.size, 0)
})