Three things the load path lacked. The retrieval slots had an unbounded line behind them, so a burst meant silent waiting; past a bounded line, or after eight seconds in it, a caller now gets 'the library is busy' and a 503 with a retry hint. The paid routes had no per-account ceiling; they now get one, counted in Redis so every replica sees the same count and nothing is refused when Redis is absent. And the same library search asked twice within a minute (a retry, a refresh) went to the library twice; it is now answered from Redis, with 0 turning that off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
68 lines
3.5 KiB
JavaScript
68 lines
3.5 KiB
JavaScript
// A person gets a bounded number of paid questions a minute, counted in
|
|
// Redis; and the same library search twice in a minute is answered from
|
|
// Redis. Neither refuses anything when Redis is absent.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const Module = require('node:module');
|
|
|
|
function fakeRedis() {
|
|
const store = new Map();
|
|
return {
|
|
store,
|
|
incr: async k => { const v = (store.get(k) || 0) + 1; store.set(k, v); return v; },
|
|
expire: async () => 1
|
|
};
|
|
}
|
|
|
|
function loadLimiter(redis) {
|
|
const target = require.resolve('../src/middleware/rateLimit');
|
|
delete require.cache[target];
|
|
const original = Module._load;
|
|
Module._load = function(request, parent) {
|
|
if (/utils\/redis$/.test(request)) return { getRedis: async () => redis };
|
|
return original.apply(this, arguments);
|
|
};
|
|
try { return require(target); } finally { Module._load = original; }
|
|
}
|
|
|
|
function run(mw, userId) {
|
|
return new Promise(resolve => {
|
|
const res = { headers: {}, set(k, v) { this.headers[k] = v; }, status(c) { this.code = c; return this; }, json(b) { this.body = b; resolve({ res, next: false }); } };
|
|
mw({ user: { id: userId } }, res, () => resolve({ res, next: true }));
|
|
});
|
|
}
|
|
|
|
test('the limit counts per user per window and answers 429 with a retry hint past it', async () => {
|
|
const { rateLimit } = loadLimiter(fakeRedis());
|
|
const mw = rateLimit('t', { limit: 2, windowSeconds: 60 });
|
|
assert.equal((await run(mw, 1)).next, true);
|
|
assert.equal((await run(mw, 1)).next, true);
|
|
const third = await run(mw, 1);
|
|
assert.equal(third.next, false);
|
|
assert.equal(third.res.code, 429);
|
|
assert.equal(third.res.body.code, 'RATE_LIMITED');
|
|
assert.ok(Number(third.res.headers['Retry-After']) >= 1);
|
|
assert.equal((await run(mw, 2)).next, true, 'another user has their own count');
|
|
});
|
|
|
|
test('without Redis nothing is refused', async () => {
|
|
const { rateLimit } = loadLimiter(null);
|
|
const mw = rateLimit('t', { limit: 1, windowSeconds: 60 });
|
|
assert.equal((await run(mw, 1)).next, true);
|
|
assert.equal((await run(mw, 1)).next, true);
|
|
});
|
|
|
|
test('the paid assistant routes sit behind the limiter, and the library search is cached briefly', () => {
|
|
const route = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
assert.match(route, /router\.post\(\['\/clinical-assistant\/chat', '\/clinical-assistant\/chat\/stream', '\/clinical-assistant\/image', '\/clinical-assistant\/image\/jobs'\], askLimit\)/);
|
|
assert.match(route, /rateLimit\('clinical-ask', \{\s*limit: positiveIntEnv\('CLINICAL_ASSISTANT_ASK_LIMIT_PER_MINUTE', 30\), windowSeconds: 60/);
|
|
assert.match(route, /await cachedSemanticSearch\(searchQuery, \{/);
|
|
assert.match(route, /RETRIEVAL_CACHE_TTL_S = positiveIntEnv\('CLINICAL_ASSISTANT_RETRIEVAL_CACHE_TTL_S', 60\)/);
|
|
assert.match(route, /if \(!RETRIEVAL_CACHE_TTL_S \|\| !cache\) return semanticSearch\(query, opts\)/, '0 turns the cache off, and so does a missing cache');
|
|
assert.match(route, /cache\.setJson\(key, response, RETRIEVAL_CACHE_TTL_S\)\.catch/, 'a cache write failure never fails the answer');
|
|
const client = fs.readFileSync(path.join(__dirname, '..', 'src/utils/clinicalMcpClient.js'), 'utf8');
|
|
assert.match(client, /e\.message\s*=|new Error\('The library is busy right now/, 'the busy answer reads as a sentence');
|
|
assert.doesNotMatch(client.slice(client.indexOf('function busyError'), client.indexOf('function acquireSlot')), /MCP/, 'so assistantErrorMessage passes it through unchanged');
|
|
});
|