Files
Codeman/test/document-thumbnailer.test.ts
T
Codeman maintainer 13c877f938 fix(review): harden Codex generated-artifact attachment pipeline (PR #150)
- Pass the attachment request `source` through the server deps lambda and make
  it a required param on SessionListenerDeps.registerAttachment + the wiring
  event type (the 2-arg lambda silently dropped `source`, force-confining every
  codex-generated artifact — the feature never worked outside the workspace);
  new test/session-listener-wiring.test.ts asserts the pass-through
- Gate the Codex `Saved to: file://` scanner on mode === 'codex' via a
  codexArtifacts option threaded from the session call site; magic links stay
  mode-agnostic; tests assert claude/shell sessions never emit codex-generated
  requests
- Decide the generated-artifact trust policy on the realpath-RESOLVED path
  (unresolvable → force-confined) and anchor the ~/.codex marker dirs to
  os.homedir() prefixes with startsWith instead of substring matching; symlink
  escape + unanchored-marker regression tests added
- Run the Codex scanner on stripAnsi'd data so trailing SGR sequences don't
  ride into the captured URL; styled 'Saved to:' test added
- Extend generateFirstPageThumbnail with jpg/jpeg/gif/webp passthrough and
  per-extension content types (mirrors the png passthrough) so the PR's new
  image formats render real thumbnails instead of 204 letter-tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:50:21 +02:00

113 lines
4.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { execFile } from 'node:child_process';
import fs from 'node:fs/promises';
import { generateFirstPageThumbnail } from '../src/document-thumbnailer.js';
import { clearDocumentPreviewCache } from '../src/document-preview-cache.js';
vi.mock('node:child_process', () => ({
execFile: vi.fn((_cmd, _args, _options, callback) => {
callback(null, { stdout: '', stderr: '' });
return {};
}),
}));
vi.mock('node:fs/promises', () => ({
default: {
stat: vi.fn(async () => ({ size: 100, isFile: () => true })),
readFile: vi.fn(async () => Buffer.from('png')),
readdir: vi.fn(async () => ['converted.pdf']),
mkdir: vi.fn(async () => undefined),
mkdtemp: vi.fn(async () => '/tmp/codeman-thumb-test'),
rename: vi.fn(async () => undefined),
rm: vi.fn(async () => undefined),
},
}));
const mockedExecFile = vi.mocked(execFile);
const mockedStat = vi.mocked(fs.stat);
const mockedReadFile = vi.mocked(fs.readFile);
const mockedReaddir = vi.mocked(fs.readdir);
const mockedMkdir = vi.mocked(fs.mkdir);
const mockedMkdtemp = vi.mocked(fs.mkdtemp);
const mockedRename = vi.mocked(fs.rename);
const mockedRm = vi.mocked(fs.rm);
describe('document-thumbnailer', () => {
beforeEach(() => {
vi.clearAllMocks();
clearDocumentPreviewCache();
mockedStat.mockImplementation(async (path) => {
const pathText = String(path);
if (pathText.includes('codeman-document-preview-cache') && pathText.endsWith('.pdf')) {
throw new Error('ENOENT');
}
return { size: 500 * 1024 * 1024, mtimeMs: 12345, isFile: () => true } as never;
});
mockedReadFile.mockResolvedValue(Buffer.from('large thumbnail') as never);
mockedReaddir.mockResolvedValue(['converted.pdf'] as never);
mockedMkdir.mockResolvedValue(undefined as never);
mockedMkdtemp.mockResolvedValue('/tmp/codeman-thumb-test' as never);
mockedRename.mockResolvedValue(undefined as never);
mockedRm.mockResolvedValue(undefined as never);
mockedExecFile.mockImplementation((_cmd, _args, _options, callback) => {
callback(null, { stdout: '', stderr: '' });
return {} as never;
});
});
it('renders first-page thumbnails for large documents without an app-level size cap', async () => {
const result = await generateFirstPageThumbnail('/tmp/large-deck.pdf', 'pdf');
expect(result).toEqual({
content: Buffer.from('large thumbnail'),
contentType: 'image/png',
});
expect(mockedExecFile).toHaveBeenCalledWith(
'pdftoppm',
expect.arrayContaining(['-png', '-singlefile', '-f', '1', '-l', '1']),
expect.any(Object),
expect.any(Function)
);
});
it('passes through generated image formats with per-extension content types', async () => {
const expectations: Array<[string, string]> = [
['jpg', 'image/jpeg'],
['jpeg', 'image/jpeg'],
['gif', 'image/gif'],
['webp', 'image/webp'],
['png', 'image/png'],
];
for (const [ext, contentType] of expectations) {
const result = await generateFirstPageThumbnail(`/tmp/mockup.${ext}`, ext);
expect(result).toEqual({ content: Buffer.from('large thumbnail'), contentType });
}
expect(mockedExecFile).not.toHaveBeenCalled();
});
it('renders Office thumbnails from the cached converted PDF after conversion cleanup', async () => {
mockedMkdtemp.mockImplementation(async (prefix) =>
String(prefix).includes('codeman-document-preview-cache')
? '/tmp/codeman-document-preview-cache/work-test'
: '/tmp/codeman-thumb-pdf-test'
);
mockedExecFile.mockImplementation((_cmd, _args, _options, callback) => {
callback(null, { stdout: '', stderr: '' });
return {} as never;
});
const result = await generateFirstPageThumbnail('/tmp/deck.pptx', 'pptx');
expect(result).toEqual({
content: Buffer.from('large thumbnail'),
contentType: 'image/png',
});
const pdftoppmCall = mockedExecFile.mock.calls.find(([cmd]) => cmd === 'pdftoppm');
expect(pdftoppmCall?.[1]).toEqual(
expect.arrayContaining([expect.stringContaining('codeman-document-preview-cache')])
);
});
});