- 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>
87 lines
3.0 KiB
JavaScript
87 lines
3.0 KiB
JavaScript
const { describe, it } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const Throttle = require('../lib/throttle');
|
|
|
|
describe('Throttle', () => {
|
|
it('unlimited mode (0) returns immediately', async () => {
|
|
const t = new Throttle(0);
|
|
const start = Date.now();
|
|
await t.consume(10_000_000);
|
|
assert.ok(Date.now() - start < 50, 'should be instant');
|
|
});
|
|
|
|
it('unlimited with falsy values', async () => {
|
|
for (const val of [undefined, null, false, 0]) {
|
|
const t = new Throttle(val);
|
|
const start = Date.now();
|
|
await t.consume(1_000_000);
|
|
assert.ok(Date.now() - start < 50, `should be instant for ${val}`);
|
|
}
|
|
});
|
|
|
|
it('small consume within initial token budget resolves immediately', async () => {
|
|
const t = new Throttle(1024 * 1024); // 1 MB/s
|
|
const start = Date.now();
|
|
await t.consume(100); // 100 bytes, well within 1MB budget
|
|
assert.ok(Date.now() - start < 50);
|
|
});
|
|
|
|
it('large consume exceeding tokens introduces delay', async () => {
|
|
const t = new Throttle(1000); // 1000 bytes/sec
|
|
// Drain initial tokens
|
|
await t.consume(1000);
|
|
|
|
const start = Date.now();
|
|
await t.consume(500); // needs ~500ms of refill
|
|
const elapsed = Date.now() - start;
|
|
assert.ok(elapsed >= 400, `expected >=400ms, got ${elapsed}ms`);
|
|
assert.ok(elapsed < 2000, `expected <2000ms, got ${elapsed}ms`);
|
|
});
|
|
|
|
it('aborted signal stops consumption early', async () => {
|
|
const t = new Throttle(100); // 100 bytes/sec
|
|
await t.consume(100); // drain budget
|
|
|
|
const ac = new AbortController();
|
|
setTimeout(() => ac.abort(), 100);
|
|
|
|
const start = Date.now();
|
|
await t.consume(10000, ac.signal); // would take ~100s without abort
|
|
const elapsed = Date.now() - start;
|
|
assert.ok(elapsed < 1000, `should abort quickly, took ${elapsed}ms`);
|
|
});
|
|
|
|
it('updateRate changes behavior', async () => {
|
|
const t = new Throttle(100);
|
|
await t.consume(100); // drain
|
|
|
|
t.updateRate(0); // switch to unlimited
|
|
const start = Date.now();
|
|
await t.consume(999999);
|
|
assert.ok(Date.now() - start < 50, 'should be instant after switching to unlimited');
|
|
});
|
|
|
|
it('_refill does not exceed maxBps', () => {
|
|
const t = new Throttle(1000);
|
|
t.tokens = 0;
|
|
t.lastRefill = Date.now() - 60000; // simulate 60 seconds elapsed
|
|
t._refill();
|
|
assert.ok(t.tokens <= 1000, `tokens should not exceed maxBps, got ${t.tokens}`);
|
|
});
|
|
|
|
it('concurrent consume calls share the token pool', async () => {
|
|
const t = new Throttle(2000); // 2000 bytes/sec, initial tokens = 2000
|
|
|
|
// Two concurrent consumes of 1000 each - should both fit in initial budget
|
|
const start = Date.now();
|
|
await Promise.all([t.consume(1000), t.consume(1000)]);
|
|
assert.ok(Date.now() - start < 100, 'both should resolve from initial budget');
|
|
|
|
// Third consume should need to wait for refill
|
|
const start2 = Date.now();
|
|
await t.consume(500);
|
|
const elapsed = Date.now() - start2;
|
|
assert.ok(elapsed >= 150, `third consume should wait for refill, took ${elapsed}ms`);
|
|
});
|
|
});
|