mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Follow-up hardening applied during review of PR #120, addressing the adversarial multi-agent findings: - fix(preview): render auto-detected (workspace, unregistered) DOCX/PPTX via the file-preview route and PDFs via file-raw in openFilePreview. Previously the Preview button fell through to file-content, dumping the binary Office/PDF bytes as mojibake, and the new file-preview route was unreachable dead code. (MAJOR: file-preview-route-unreachable-detected-office) - perf(convert): add a global converter-concurrency limiter (document-conversion-limiter.ts) wrapping every pdftoppm / soffice / powershell spawn, so N simultaneous preview/thumbnail requests can no longer fork unbounded converter processes. Default cap 3, CODEMAN_MAX_DOCUMENT_CONVERSIONS. (MAJOR: no-converter-concurrency-limit) - fix(cache): bound the converted-PDF disk cache with LRU-by-mtime eviction (pruneDocumentPreviewCache, default 100 files, CODEMAN_MAX_PREVIEW_CACHE_FILES), run after each successful conversion. Was unbounded. (MAJOR/MINOR: preview-cache-unbounded-disk-growth) Tests: document-conversion-limiter.test.ts, document-preview-cache-eviction.test.ts, and route coverage for the four new endpoints in routes/file-routes-preview-thumbnail.test.ts (closes the missing-route-test gap). Verified end-to-end against real pdftoppm (thumbnail render + concurrency cap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { runWithConversionLimit, getActiveConversionCount } from '../src/document-conversion-limiter.js';
|
|
|
|
describe('document-conversion-limiter', () => {
|
|
it('never runs more converters than the cap (default 3) concurrently', async () => {
|
|
let running = 0;
|
|
let maxObserved = 0;
|
|
|
|
const task = () => async () => {
|
|
running++;
|
|
maxObserved = Math.max(maxObserved, running);
|
|
// The module's own accounting must also stay within the cap.
|
|
expect(getActiveConversionCount()).toBeLessThanOrEqual(3);
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
running--;
|
|
};
|
|
|
|
await Promise.all(Array.from({ length: 12 }, () => runWithConversionLimit(task())));
|
|
|
|
expect(maxObserved).toBeLessThanOrEqual(3);
|
|
expect(maxObserved).toBeGreaterThan(1); // proves it genuinely parallelizes, not serializes
|
|
expect(getActiveConversionCount()).toBe(0); // every slot released
|
|
});
|
|
|
|
it('processes every queued task even when far more are submitted than the cap', async () => {
|
|
let completed = 0;
|
|
await Promise.all(
|
|
Array.from({ length: 25 }, () =>
|
|
runWithConversionLimit(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
completed++;
|
|
})
|
|
)
|
|
);
|
|
expect(completed).toBe(25);
|
|
expect(getActiveConversionCount()).toBe(0);
|
|
});
|
|
|
|
it('releases the slot when a task throws', async () => {
|
|
await expect(
|
|
runWithConversionLimit(async () => {
|
|
throw new Error('boom');
|
|
})
|
|
).rejects.toThrow('boom');
|
|
expect(getActiveConversionCount()).toBe(0);
|
|
});
|
|
});
|