diff --git a/frontend-vue/tests/progress-response-cache.test.ts b/frontend-vue/tests/progress-response-cache.test.ts index 2e1b8915..5e01eb23 100644 --- a/frontend-vue/tests/progress-response-cache.test.ts +++ b/frontend-vue/tests/progress-response-cache.test.ts @@ -207,3 +207,116 @@ test('test_task_082_progress_cleanup_dependency_failure_releases_resources', () cache.clear() assert.equal(cache.size, 0) }) + +test('test_cache_get_hit', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 5000, maxEntries: 10, now: c.now }) + cache.set(1, detail(1)) + assert.deepEqual(cache.get(1), detail(1), 'TTL 内命中返回原值') + assert.equal(cache.has(1), true) +}) + +test('test_cache_get_expired_cleared', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 5000, maxEntries: 10, now: c.now }) + cache.set(1, detail(1)) + c.tick(4999) + assert.deepEqual(cache.get(1), detail(1), '恰好未过期仍命中') + c.tick(2) + assert.equal(cache.get(1), undefined, '过期后读取返回 undefined 并清理') + assert.equal(cache.size, 0, '过期条目已从缓存移除') +}) + +test('test_cache_max_entries_evict', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 60000, maxEntries: 2, now: c.now }) + cache.set(1, detail(1)) + cache.set(2, detail(2)) + cache.set(3, detail(3)) + assert.equal(cache.size, 2) + assert.equal(cache.get(1), undefined, '溢出淘汰最旧条目') + assert.equal(cache.get(3)?.id, 3, '最新条目保留') +}) + +test('test_cache_empty_get_undefined', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 1000, maxEntries: 5, now: c.now }) + assert.equal(cache.get(1), undefined, '空缓存 get 返回 undefined') + assert.equal(cache.has(1), false) + assert.deepEqual(cache.entries(), []) +}) + +test('test_cache_set_overwrites', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 60000, maxEntries: 5, now: c.now }) + cache.set(1, detail(1)) + c.tick(100) + cache.set(1, { ...detail(1), status: 'SUCCESS' }) + assert.equal(cache.get(1)?.status, 'SUCCESS', '同 key 覆盖新值') + assert.equal(cache.size, 1) + // 覆盖刷新 TTL:覆盖后再过一半 TTL 仍命中 + c.tick(30000) + assert.deepEqual(cache.get(1), { ...detail(1), status: 'SUCCESS' }, '覆盖后 TTL 重新计时') +}) + +test('test_cache_ttl_zero', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 1, maxEntries: 5, now: c.now }) + cache.set(1, detail(1)) + c.tick(1) + assert.equal(cache.get(1), undefined, 'TTL=1ms 过 1ms 即过期') + assert.equal(cache.size, 0) +}) + +test('test_cache_lazy_cleanup', () => { + const c = clock() + const cache = createProgressResponseCache({ ttlMs: 1000, maxEntries: 5, now: c.now }) + cache.set(1, detail(1)) + cache.set(2, detail(2)) + c.tick(2000) + // 未读取前缓存条目仍在(惰性:无后台定时清理) + assert.equal(cache.size, 2) + // 读取触发惰性清理:读 1 只清 1,读 2 再清 2 + assert.equal(cache.get(1), undefined) + assert.equal(cache.size, 1) + assert.equal(cache.get(2), undefined) + assert.equal(cache.size, 0) +}) + +test('test_cache_after_polling_cycle', async () => { + const c = clock() + const cache = createProgressResponseCache<{ taskId?: number; status?: string }>({ + ttlMs: 5000, + maxEntries: 10, + now: c.now, + }) + const storage = new Map() + ;(globalThis as Record).window = { + localStorage: { + getItem: (k: string) => storage.get(k) ?? null, + setItem: (k: string, v: string) => { storage.set(k, v) }, + removeItem: (k: string) => { storage.delete(k) }, + }, + setTimeout: (fn: () => void, ms: number) => globalThis.setTimeout(fn, ms), + clearTimeout: (id: unknown) => globalThis.clearTimeout(id as number), + setInterval: (fn: () => void, ms: number) => globalThis.setInterval(fn, ms), + clearInterval: (id: unknown) => globalThis.clearInterval(id as number), + } + const { useTaskProgressLoop } = await import('../src/shared/composables/useTaskProgressLoop.ts') + const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ + scope: 'cache-cycle', + fetchProgress: async (ids) => ({ + items: ids.map((id) => ({ taskId: id, status: 'RUNNING' })), + }), + extractTaskId: (d) => d?.taskId, + extractStatus: (d) => d?.status, + getIntervalMs: () => 60000, + progressCache: cache, + }) + loop.reset([1]) + await new Promise((r) => globalThis.setTimeout(r, 10)) + assert.equal(cache.get(1)?.status, 'RUNNING', '轮询周期内缓存可用') + c.tick(6000) + assert.equal(cache.get(1), undefined, '超 TTL 后读取清理') + loop.dispose() +})