Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Bouncer/src/background/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ async function processBatch(): Promise<void> {
const cacheKey = generateCacheKey(item.post, imageUrls);
if (evaluationCache.has(cacheKey)) {
const cached = evaluationCache.get(cacheKey)!;
// LRU: move to end of Map iteration order so it's evicted last
evaluationCache.delete(cacheKey);
evaluationCache.set(cacheKey, cached);
resolveWithDuplicates(batchTabId, item, { ...cached, cached: true });
inFlightBatches--;
if (pendingEvaluations.length > 0) scheduleBatch();
Expand Down
48 changes: 48 additions & 0 deletions Bouncer/tests/background/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,54 @@ describe('processBatch re-queue on inference queue cleared', () => {
});
});

// ==================== LRU cache eviction ====================

describe('LRU cache eviction', () => {
it('recently accessed cache entries survive eviction', async () => {
const { evaluationCache } = await import('../../src/background/pipeline.js');

evaluationCache.clear();
for (let i = 0; i < 500; i++) {
evaluationCache.set(`post-${i}`, {
shouldHide: false,
reasoning: 'test',
category: null,
rawResponse: null,
timestamp: Date.now(),
model: 'test',
cached: false,
});
}
expect(evaluationCache.size).toBe(500);

// Access post-0 (oldest) to mark it as recently used — simulating the LRU promotion
const oldest = evaluationCache.get('post-0')!;
evaluationCache.delete('post-0');
evaluationCache.set('post-0', oldest);

// Add a new entry — should evict post-1 (now the LRU), NOT post-0
evaluationCache.set('post-new', {
shouldHide: false,
reasoning: 'new',
category: null,
rawResponse: null,
timestamp: Date.now(),
model: 'test',
cached: false,
});

// Evict like pipeline does
if (evaluationCache.size > 500) {
const firstKey = evaluationCache.keys().next().value;
if (firstKey !== undefined) evaluationCache.delete(firstKey);
}

expect(evaluationCache.has('post-0')).toBe(true);
expect(evaluationCache.has('post-1')).toBe(false);
expect(evaluationCache.has('post-new')).toBe(true);
});
});

// Note: The no-model-configured path (empty selectedModel -> no_api_key error) is only
// reachable in no-Imbue builds where DEFAULT_MODEL is ''. Since DEFAULT_MODEL is computed
// at module load time from the build-time constant HAS_IMBUE_BACKEND, it can't be varied
Expand Down