Files
Endoleap/tests/headless/display-cache.test.mjs

57 lines
5.4 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createDisplayCache } from '../../src/lib/canvas-runtime/display-cache.ts';
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
const tick = () => new Promise(resolve => setImmediate(resolve));
test('concurrent display leases share one source until the last idempotent release; there is no idle URL retention', async () => {
const cache = createDisplayCache(), owner = {}; let loads = 0, disposed = 0;
const load = async () => ({ url: `blob:${++loads}`, release: () => disposed++ });
const a = cache.acquire(owner, 'asset', load), b = cache.acquire(owner, 'asset', load);
assert.deepEqual(await Promise.all([a.ready, b.ready]), ['blob:1', 'blob:1']); assert.equal(loads, 1);
a.release(); a.release(); assert.equal(disposed, 0); b.release(); assert.equal(disposed, 1);
const c = cache.acquire(owner, 'asset', load); assert.equal(await c.ready, 'blob:2'); c.release(); cache.close(); assert.equal(disposed, 2);
});
test('cancelling one waiting lease does not abort a shared load or another reader', async () => {
const cache = createDisplayCache(), owner = {}, gate = deferred(); let signal, disposed = 0;
const load = async value => { signal = value; return gate.promise; };
const a = cache.acquire(owner, 'asset', load), b = cache.acquire(owner, 'asset', load); await tick();
const cancelled = assert.rejects(a.ready, { code: 'display_released' }); a.release(); await cancelled; assert.equal(signal.aborted, false);
gate.resolve({ url: 'blob:shared', release: () => disposed++ }); assert.equal(await b.ready, 'blob:shared'); b.release(); assert.equal(signal.aborted, true); assert.equal(disposed, 1);
});
test('last release aborts a load; a late source cannot replace or evict a newly acquired entry', async () => {
const cache = createDisplayCache(), owner = {}, gate = deferred(); let signal, oldDisposed = 0, newDisposed = 0;
const old = cache.acquire(owner, 'asset', async value => { signal = value; return gate.promise; }); await tick();
const cancelled = assert.rejects(old.ready, { code: 'display_released' }); old.release(); await cancelled; assert.equal(signal.aborted, true);
const next = cache.acquire(owner, 'asset', async () => ({ url: 'blob:new', release: () => newDisposed++ })); assert.equal(await next.ready, 'blob:new');
gate.resolve({ url: 'blob:old', release: () => oldDisposed++ }); await tick(); assert.equal(oldDisposed, 1); assert.equal(newDisposed, 0);
const peer = cache.acquire(owner, 'asset', async () => { throw Error('unexpected duplicate read'); }); assert.equal(await peer.ready, 'blob:new'); peer.release(); next.release(); assert.equal(newDisposed, 1);
});
test('read failure retains its cause and removes the failed cache entry for a fresh retry', async () => {
const cache = createDisplayCache(), owner = {};
const a = cache.acquire(owner, 'asset', async () => { throw Object.assign(Error('disk_failed'), { code: 'disk_failed' }); });
await assert.rejects(a.ready, { code: 'disk_failed' });
const b = cache.acquire(owner, 'asset', async () => ({ url: '/remote' })); assert.equal(await b.ready, '/remote'); b.release(); cache.close();
});
test('project owner scopes isolate identical IDs and retired owners cannot reacquire', async () => {
const cache = createDisplayCache(), a = {}, b = {}; let disposed = 0;
const first = cache.acquire(a, 'same-id', async () => ({ url: 'blob:a', release: () => disposed++ }));
const second = cache.acquire(b, 'same-id', async () => ({ url: 'blob:b', release: () => disposed++ }));
assert.deepEqual(await Promise.all([first.ready, second.ready]), ['blob:a', 'blob:b']); cache.releaseOwner(a); assert.equal(disposed, 1);
assert.throws(() => cache.acquire(a, 'same-id', async () => ({ url: '/forbidden' })), { code: 'display_scope_closed' });
const peer = cache.acquire(b, 'same-id', async () => { throw Error('unexpected'); }); assert.equal(await peer.ready, 'blob:b'); peer.release(); second.release(); assert.equal(disposed, 2);
});
test('cache close rejects waiting readers immediately and disposes late data without reopening a scope', async () => {
const cache = createDisplayCache(), gate = deferred(); let disposed = 0;
const lease = cache.acquire({}, 'asset', () => gate.promise); await tick();
const cancelled = assert.rejects(lease.ready, { code: 'display_released' }); cache.close(); await cancelled;
gate.resolve({ url: 'blob:late', release: () => disposed++ }); await tick(); assert.equal(disposed, 1);
assert.throws(() => cache.acquire({}, 'asset', async () => ({ url: '/late' })), { code: 'display_scope_closed' }); cache.close();
});
test('invalid sources clean their allocation and cleanup errors do not interrupt other owners', async () => {
const errors = [], cache = createDisplayCache({ onError: error => errors.push(error) }); let disposed = 0;
const invalid = cache.acquire({}, 'invalid', async () => ({ url: '', release: () => disposed++ })); await assert.rejects(invalid.ready, { code: 'invalid_display_source' }); assert.equal(disposed, 1);
const a = cache.acquire({}, 'a', async () => ({ url: 'blob:a', release: () => { throw Error('cleanup_failed'); } }));
const b = cache.acquire({}, 'b', async () => ({ url: 'blob:b', release: () => disposed++ })); await Promise.all([a.ready, b.ready]); cache.close(); assert.equal(disposed, 2); assert.equal(errors[0].message, 'cleanup_failed');
});