mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
Merge #502: read-only XLSX spreadsheet preview in the file-preview overlay
# Conflicts: # CLAUDE.md
This commit is contained in:
@@ -556,6 +556,52 @@ describe('file-routes attachment path guard (COD-53)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ===== XLSX: client-side preview, same guard and routes =====
|
||||
// xlsx joins the extension allowlist so the overlay can preview a workbook
|
||||
// outside the workspace by id. Nothing else about the pipeline changes; only
|
||||
// `?preview=true` adds a tighter size cap because the browser parses it.
|
||||
describe('xlsx attachments', () => {
|
||||
async function registerXlsx(size: number) {
|
||||
mockedStat.mockResolvedValue({ size, isFile: () => true, mtimeMs: 5 } as never);
|
||||
const res = await harness.app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/attachments`,
|
||||
payload: { path: '/tmp/report.xlsx', notify: false },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
return JSON.parse(res.body).data as { attachmentId: string; attachmentType: string };
|
||||
}
|
||||
|
||||
it('registers an xlsx as a spreadsheet attachment', async () => {
|
||||
const data = await registerXlsx(2048);
|
||||
expect(data.attachmentType).toBe('spreadsheet');
|
||||
});
|
||||
|
||||
it('still refuses xls and ods (download-only, no preview)', async () => {
|
||||
for (const path of ['/tmp/legacy.xls', '/tmp/open.ods']) {
|
||||
const res = await harness.app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/attachments`,
|
||||
payload: { path, notify: false },
|
||||
});
|
||||
expect(res.statusCode, path).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it('caps a ?preview=true fetch at 10 MB but still serves a plain download', async () => {
|
||||
const { attachmentId } = await registerXlsx(11 * 1024 * 1024);
|
||||
const base = `/api/sessions/${harness.ctx._sessionId}/attachments/${attachmentId}/raw`;
|
||||
|
||||
const preview = await harness.app.inject({ method: 'GET', url: `${base}?preview=true` });
|
||||
expect(preview.statusCode).toBe(413);
|
||||
expect(JSON.parse(preview.body).error).toMatch(/too large to preview/i);
|
||||
|
||||
mockedCreateReadStream.mockReturnValue(Readable.from([Buffer.from('PK')]) as never);
|
||||
const download = await harness.app.inject({ method: 'GET', url: `${base}?preview=true&download=true` });
|
||||
expect(download.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Quiet registration (click-to-preview) =====
|
||||
// The file-preview overlay registers a clicked out-of-workspace path to mint
|
||||
// an id it can render by. It is already putting the file on screen, so the
|
||||
|
||||
@@ -707,12 +707,12 @@ describe('file-routes', () => {
|
||||
expect(body.data.url).toContain('file-raw');
|
||||
});
|
||||
|
||||
it('flags known-binary extensions (e.g. xlsx) instead of dumping mojibake', async () => {
|
||||
it('flags known-binary extensions (e.g. xls) instead of dumping mojibake', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 4096 } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=sheet.xlsx`,
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=sheet.xls`,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
@@ -721,6 +721,30 @@ describe('file-routes', () => {
|
||||
expect(body.data.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('classifies xlsx as a client-side spreadsheet preview, never a text body', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 4096 } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=sheet.xlsx`,
|
||||
});
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.type).toBe('spreadsheet');
|
||||
expect(body.data.url).toBe(`/api/sessions/${harness.ctx._sessionId}/file-raw?path=sheet.xlsx`);
|
||||
expect(body.data.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps ods (and xls) download-only binaries', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 4096 } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=sheet.ods`,
|
||||
});
|
||||
expect(JSON.parse(res.body).data.type).toBe('binary');
|
||||
});
|
||||
|
||||
it('sniffs NUL bytes and flags binary content for unknown extensions', async () => {
|
||||
const binary = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x01, 0x02]);
|
||||
mockedReadFile.mockResolvedValue(binary as never);
|
||||
@@ -893,6 +917,22 @@ describe('file-routes', () => {
|
||||
expect(res.statusCode).toBe(413);
|
||||
expect(JSON.parse(res.body).error).toContain('CODEMAN_MAX_DOWNLOAD_BYTES');
|
||||
});
|
||||
|
||||
it('caps an xlsx ?preview=true fetch at 10 MB, leaving downloads and small previews alone', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 11 * 1024 * 1024 } as never);
|
||||
const url = `/api/sessions/${harness.ctx._sessionId}/file-raw?path=book.xlsx`;
|
||||
|
||||
const preview = await harness.app.inject({ method: 'GET', url: `${url}&preview=true` });
|
||||
expect(preview.statusCode).toBe(413);
|
||||
expect(JSON.parse(preview.body).error).toMatch(/too large to preview/i);
|
||||
|
||||
const download = await harness.app.inject({ method: 'GET', url: `${url}&preview=true&download=true` });
|
||||
expect(download.statusCode).toBe(200);
|
||||
|
||||
mockedStat.mockResolvedValue({ size: 2048 } as never);
|
||||
const small = await harness.app.inject({ method: 'GET', url: `${url}&preview=true` });
|
||||
expect(small.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== DELETE /api/sessions/:id/tail-file/:streamId ==========
|
||||
|
||||
Reference in New Issue
Block a user