Files
hertzbeat/web-next/lib/workbench-load-cache.test.ts
T
Logic 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.

AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
2026-07-10 14:54:43 +08:00

91 lines
3.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { consumeWorkbenchLoad, forgetWorkbenchLoad, resetWorkbenchLoadCache } from './workbench-load-cache';
describe('workbench load cache', () => {
afterEach(() => {
resetWorkbenchLoadCache();
});
it('reuses the same in-flight promise for the same cache key', async () => {
let resolveLoad: ((value: string) => void) | null = null;
const load = vi.fn(() => new Promise<string>(resolve => {
resolveLoad = resolve;
}));
const first = consumeWorkbenchLoad('trace:list?traceId=demo', load);
const second = consumeWorkbenchLoad('trace:list?traceId=demo', load);
expect(load).toHaveBeenCalledTimes(1);
expect(second).toBe(first);
resolveLoad?.('ready');
await expect(first).resolves.toBe('ready');
await expect(second).resolves.toBe('ready');
});
it('clears the cache after the promise settles', async () => {
const load = vi.fn()
.mockResolvedValueOnce('first')
.mockResolvedValueOnce('second');
await expect(consumeWorkbenchLoad('trace:list?traceId=demo', load)).resolves.toBe('first');
await expect(consumeWorkbenchLoad('trace:list?traceId=demo', load)).resolves.toBe('second');
expect(load).toHaveBeenCalledTimes(2);
});
it('can keep a settled result for a short ttl when shared chrome needs the same state', async () => {
let now = 1_000;
const load = vi.fn()
.mockResolvedValueOnce('first')
.mockResolvedValueOnce('second');
await expect(
consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
settledTtlMs: 500,
now: () => now,
})
).resolves.toBe('first');
await expect(
consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
settledTtlMs: 500,
now: () => now,
})
).resolves.toBe('first');
now = 1_501;
await expect(
consumeWorkbenchLoad('app-frame:header-state:zh-CN', load, {
settledTtlMs: 500,
now: () => now,
})
).resolves.toBe('second');
expect(load).toHaveBeenCalledTimes(2);
});
it('clears the cache after a rejection so the next attempt can retry', async () => {
const load = vi.fn()
.mockRejectedValueOnce(new Error('boom'))
.mockResolvedValueOnce('recovered');
await expect(consumeWorkbenchLoad('trace:list?traceId=demo', load)).rejects.toThrow('boom');
await expect(consumeWorkbenchLoad('trace:list?traceId=demo', load)).resolves.toBe('recovered');
expect(load).toHaveBeenCalledTimes(2);
});
it('can forget a single pending cache key so a timeout retry starts a fresh load', async () => {
const load = vi.fn(() => new Promise<string>(() => {}));
const first = consumeWorkbenchLoad('entity-detail:/entities/42/detail', load);
forgetWorkbenchLoad('entity-detail:/entities/42/detail');
const second = consumeWorkbenchLoad('entity-detail:/entities/42/detail', load);
expect(load).toHaveBeenCalledTimes(2);
expect(second).not.toBe(first);
});
});