task-90: 进度接口增加断网、超时、服务恢复和重复响应测试
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createTaskProgressRequestCache } from '../src/shared/task-progress-request-cache.ts'
|
||||
|
||||
const detail = (n: number) => ({ items: [{ taskId: n, status: 'SUCCESS' }] })
|
||||
|
||||
test('test_task_090_progress_resilience_normal_default_path', () => {
|
||||
let now = 1000
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000, now: () => now })
|
||||
assert.equal(cache.get('a'), undefined)
|
||||
cache.set('a', detail(1))
|
||||
assert.deepEqual(cache.get('a'), detail(1))
|
||||
const stats = cache.stats()
|
||||
assert.equal(stats.cacheEntries, 1)
|
||||
assert.equal(stats.hitCount, 1)
|
||||
assert.equal(stats.missCount, 1)
|
||||
// in-flight 合并:先查后登记,同 key 并发只保留一个
|
||||
const p = Promise.resolve(detail(2))
|
||||
assert.equal(cache.getInflight('b'), undefined)
|
||||
assert.equal(cache.startInflight('b', p), true)
|
||||
assert.equal(cache.getInflight('b'), p)
|
||||
assert.equal(cache.startInflight('b', Promise.resolve(detail(3))), false, '同 key 已合并,拒绝重复登记')
|
||||
cache.endInflight('b')
|
||||
assert.equal(cache.getInflight('b'), undefined)
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_normal_multiple_items', () => {
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 10_000 })
|
||||
for (let i = 1; i <= 4; i++) cache.set(`k${i}`, detail(i))
|
||||
assert.deepEqual(cache.get('k1'), detail(1))
|
||||
assert.deepEqual(cache.get('k4'), detail(4))
|
||||
assert.equal(cache.stats().cacheEntries, 4)
|
||||
// 各 key 互不串扰;顺序稳定
|
||||
assert.deepEqual(cache.get('k2'), detail(2))
|
||||
assert.deepEqual(cache.get('k3'), detail(3))
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_normal_repeated_operation_is_idempotent', () => {
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000 })
|
||||
cache.set('a', detail(1))
|
||||
cache.set('a', detail(1))
|
||||
assert.equal(cache.stats().cacheEntries, 1, '重复 set 覆盖不新增条目')
|
||||
assert.deepEqual(cache.get('a'), detail(1))
|
||||
// endInflight 幂等
|
||||
const p = Promise.resolve(detail(2))
|
||||
cache.startInflight('b', p)
|
||||
cache.endInflight('b')
|
||||
cache.endInflight('b')
|
||||
assert.equal(cache.getInflight('b'), undefined)
|
||||
// startInflight 重复登记返回 false 且不覆盖原 promise
|
||||
cache.startInflight('c', p)
|
||||
assert.equal(cache.startInflight('c', Promise.resolve(detail(9))), false)
|
||||
assert.equal(cache.getInflight('c'), p)
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_boundary_empty_input', () => {
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000 })
|
||||
assert.equal(cache.get(''), undefined, '空 key 读取宽容')
|
||||
assert.equal(cache.get('missing'), undefined)
|
||||
assert.equal(cache.getInflight(''), undefined)
|
||||
cache.endInflight('') // 宽容无操作
|
||||
cache.clear()
|
||||
assert.equal(cache.stats().cacheEntries, 0)
|
||||
assert.equal(cache.stats().inflightCount, 0)
|
||||
// clear 后继续可正常读写(服务恢复后可重新使用)
|
||||
cache.set('a', detail(1))
|
||||
assert.deepEqual(cache.get('a'), detail(1))
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_boundary_single_item', () => {
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000 })
|
||||
cache.set('single', detail(7))
|
||||
assert.deepEqual(cache.get('single'), detail(7))
|
||||
assert.equal(cache.stats().cacheEntries, 1)
|
||||
assert.equal(cache.stats().missCount, 0)
|
||||
// 单并发请求合并
|
||||
const p = Promise.resolve(detail(8))
|
||||
assert.equal(cache.startInflight('single', p), true)
|
||||
assert.equal(cache.getInflight('single'), p)
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_boundary_limit_and_overflow', () => {
|
||||
let now = 0
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000, maxEntries: 3, maxInflight: 2, now: () => now })
|
||||
for (let i = 1; i <= 5; i++) cache.set(`k${i}`, detail(i))
|
||||
assert.equal(cache.stats().cacheEntries, 3, '超过 maxEntries 只保留最近 3 条')
|
||||
assert.equal(cache.stats().evictedCount, 2)
|
||||
assert.equal(cache.get('k1'), undefined, '最旧条目被驱逐')
|
||||
assert.equal(cache.get('k2'), undefined)
|
||||
assert.deepEqual(cache.get('k5'), detail(5))
|
||||
// TTL 过期:惰性清除并计 miss
|
||||
now = 10_000
|
||||
assert.equal(cache.get('k5'), undefined)
|
||||
assert.equal(cache.stats().cacheEntries, 2)
|
||||
// in-flight 超上限拒绝合并,但不丢请求
|
||||
const p = Promise.resolve(detail(9))
|
||||
assert.equal(cache.startInflight('a', p), true)
|
||||
assert.equal(cache.startInflight('b', p), true)
|
||||
assert.equal(cache.startInflight('c', p), false, '超过 maxInflight 拒绝合并')
|
||||
assert.equal(cache.stats().rejectedCount, 1)
|
||||
assert.equal(cache.getInflight('c'), undefined)
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_invalid_input_rejected', () => {
|
||||
assert.throws(() => createTaskProgressRequestCache({ ttlMs: 0 }), /ttlMs 必须为正数/)
|
||||
assert.throws(() => createTaskProgressRequestCache({ ttlMs: -1 }), /ttlMs 必须为正数/)
|
||||
assert.throws(() => createTaskProgressRequestCache({ ttlMs: 'x' as never }), /ttlMs 必须为正数/)
|
||||
assert.throws(() => createTaskProgressRequestCache({ ttlMs: 100, maxEntries: 0 }), /maxEntries 必须为正数/)
|
||||
assert.throws(() => createTaskProgressRequestCache({ ttlMs: 100, maxInflight: 0 }), /maxInflight 必须为正数/)
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 100 })
|
||||
assert.throws(() => cache.set('', detail(1)), /key 必须是非空字符串/)
|
||||
assert.throws(() => cache.set(null as never, detail(1)), /key 必须是非空字符串/)
|
||||
assert.throws(() => cache.startInflight('', Promise.resolve(detail(1))), /key 必须是非空字符串/)
|
||||
})
|
||||
|
||||
test('test_task_090_progress_resilience_dependency_failure_releases_resources', () => {
|
||||
const cache = createTaskProgressRequestCache({ ttlMs: 5000 })
|
||||
// 断网/超时:请求 reject → in-flight 必须释放,缓存不被污染
|
||||
const failed = Promise.reject(new Error('Network Error'))
|
||||
const promise = failed.catch(() => undefined) // 吞掉 unhandled rejection
|
||||
cache.startInflight('a', promise)
|
||||
assert.equal(cache.stats().inflightCount, 1)
|
||||
cache.endInflight('a')
|
||||
assert.equal(cache.stats().inflightCount, 0, '失败后 in-flight 槽位释放')
|
||||
assert.equal(cache.get('a'), undefined, '失败响应不进入缓存')
|
||||
// 服务恢复:重试成功 → 缓存重新填充并可命中
|
||||
cache.set('a', detail(1))
|
||||
assert.deepEqual(cache.get('a'), detail(1))
|
||||
// 断网时旧缓存兜底:请求失败不删除未过期旧条目
|
||||
const p2 = failed.catch(() => undefined)
|
||||
cache.startInflight('a', p2)
|
||||
cache.endInflight('a')
|
||||
assert.deepEqual(cache.get('a'), detail(1), '失败不清除旧缓存,恢复后可降级命中')
|
||||
})
|
||||
Reference in New Issue
Block a user