Multi-Hoster-Upload/tests/semaphore.test.js
Administrator 61681de9a3 test: add unit tests (41) and UI smoke tests (21), fix semaphore listener leak
- 12 Semaphore tests: FIFO ordering, abort support, limit updates, listener cleanup
- 8 Throttle tests: rate limiting, abort signal, concurrent consume, updateRate
- 9 ConfigStore tests: defaults, merge, round-trip, corruption fallback, history cap
- 12 UploadManager tests: progress events, retry, cancel, size filter, concurrency
- 21 UI smoke tests: tab navigation, settings panels, statusbar, context menu
- Fix: Semaphore.release() and updateLimit() now properly remove abort listeners

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:55:50 +01:00

166 lines
4.6 KiB
JavaScript

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const Semaphore = require('../lib/semaphore');
describe('Semaphore', () => {
it('clamps limit to at least 1', () => {
assert.equal(new Semaphore(0).limit, 1);
assert.equal(new Semaphore(-5).limit, 1);
assert.equal(new Semaphore(undefined).limit, 1);
assert.equal(new Semaphore(3).limit, 3);
});
it('acquire resolves immediately when slots available', async () => {
const sem = new Semaphore(2);
await sem.acquire();
await sem.acquire();
assert.equal(sem.active, 2);
});
it('acquire blocks when all slots taken', async () => {
const sem = new Semaphore(1);
await sem.acquire();
let resolved = false;
const p = sem.acquire().then(() => { resolved = true; });
// Give microtask a chance to resolve
await new Promise(r => setTimeout(r, 10));
assert.equal(resolved, false, 'should not resolve while slot is taken');
assert.equal(sem.pending, 1);
sem.release();
await p;
assert.equal(resolved, true);
});
it('FIFO ordering', async () => {
const sem = new Semaphore(1);
await sem.acquire(); // take the one slot
const order = [];
const p1 = sem.acquire().then(() => order.push(1));
const p2 = sem.acquire().then(() => order.push(2));
const p3 = sem.acquire().then(() => order.push(3));
assert.equal(sem.pending, 3);
sem.release(); await p1;
sem.release(); await p2;
sem.release(); await p3;
assert.deepEqual(order, [1, 2, 3]);
});
it('release with no waiters decrements active', async () => {
const sem = new Semaphore(2);
await sem.acquire();
assert.equal(sem.active, 1);
sem.release();
assert.equal(sem.active, 0);
});
it('release never goes below 0', () => {
const sem = new Semaphore(2);
sem.release();
assert.equal(sem.active, 0);
sem.release();
assert.equal(sem.active, 0);
});
it('acquire rejects immediately if signal already aborted', async () => {
const sem = new Semaphore(2);
const ac = new AbortController();
ac.abort();
await assert.rejects(sem.acquire(ac.signal), /Aborted/);
assert.equal(sem.active, 0, 'no slot should be acquired');
});
it('abort while waiting in queue removes entry and rejects', async () => {
const sem = new Semaphore(1);
await sem.acquire(); // take the slot
const ac = new AbortController();
const p = sem.acquire(ac.signal);
assert.equal(sem.pending, 1);
ac.abort();
await assert.rejects(p, /Aborted/);
assert.equal(sem.pending, 0, 'entry should be removed from queue');
// Release original slot - should not cause issues
sem.release();
assert.equal(sem.active, 0);
});
it('abort listener is cleaned up when slot is granted via release', async () => {
const sem = new Semaphore(1);
await sem.acquire();
const ac = new AbortController();
let rejected = false;
const p = sem.acquire(ac.signal).catch(() => { rejected = true; });
sem.release(); // grants slot to the waiter
await p;
// Now abort after the slot was already granted
ac.abort();
await new Promise(r => setTimeout(r, 10));
assert.equal(rejected, false, 'reject should not fire after slot was granted');
});
it('updateLimit wakes waiters', async () => {
const sem = new Semaphore(1);
await sem.acquire();
const resolved = [];
const p1 = sem.acquire().then(() => resolved.push(1));
const p2 = sem.acquire().then(() => resolved.push(2));
sem.updateLimit(3);
await Promise.all([p1, p2]);
assert.deepEqual(resolved, [1, 2]);
});
it('updateLimit to lower value does not kill active slots', async () => {
const sem = new Semaphore(3);
await sem.acquire();
await sem.acquire();
await sem.acquire();
assert.equal(sem.active, 3);
sem.updateLimit(1);
assert.equal(sem.active, 3, 'existing active slots should not be evicted');
sem.release();
sem.release();
sem.release();
assert.equal(sem.active, 0);
// Now only 1 slot should be available
await sem.acquire();
let blocked = false;
const p = sem.acquire().then(() => { blocked = true; });
await new Promise(r => setTimeout(r, 10));
assert.equal(blocked, false, 'should block at limit 1');
sem.release();
await p;
});
it('pending getter tracks queue size', async () => {
const sem = new Semaphore(1);
assert.equal(sem.pending, 0);
await sem.acquire();
sem.acquire(); // blocked
sem.acquire(); // blocked
assert.equal(sem.pending, 2);
sem.release();
await new Promise(r => setTimeout(r, 5));
assert.equal(sem.pending, 1);
});
});