moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,327 @@
/**
* Ingest API tests — POST /api/ingest
*
* Uses server.inject() with JSON body (no multipart).
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { buildLocalServer } from '../src/local/index.js';
import type { FastifyInstance } from 'fastify';
import { injectWithAuth } from './test-utils.js';
describe('POST /api/ingest', () => {
let server: FastifyInstance;
let tmpDir: string;
beforeAll(async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ingest-test-'));
server = await buildLocalServer({ dataDir: tmpDir });
});
afterAll(async () => {
await server.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
// ── Validation ──────────────────────────────────────────────────
it('returns 400 when files array is missing', async () => {
const res = await injectWithAuth(server, { method: 'POST', url: '/api/ingest', payload: {} });
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain('files');
});
it('returns 400 when files is empty', async () => {
const res = await injectWithAuth(server, { method: 'POST', url: '/api/ingest', payload: { files: [] } });
expect(res.statusCode).toBe(400);
});
it('returns 400 when a file entry has no name', async () => {
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ content: 'abc' }] },
});
expect(res.statusCode).toBe(400);
});
it('returns 413 for oversized files', async () => {
// Create a base64 string that decodes to > 10 MB
const bigContent = 'A'.repeat(14 * 1024 * 1024); // ~10.5 MB decoded
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'big.txt', content: bigContent }] },
});
expect(res.statusCode).toBe(413);
expect(JSON.parse(res.body).error).toContain('10 MB');
});
// ── Image processing ────────────────────────────────────────────
it('processes an image file and returns data URI', async () => {
const content = Buffer.from('fake-png-data').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'photo.png', content }] },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.files).toHaveLength(1);
expect(body.files[0].type).toBe('image');
expect(body.files[0].summary).toContain('PNG');
expect(body.files[0].content).toMatch(/^data:image\/png;base64,/);
});
it('handles JPEG extension correctly', async () => {
const content = Buffer.from('fake-jpg').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'pic.jpg', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].content).toMatch(/^data:image\/jpeg;base64,/);
});
// ── PDF processing ──────────────────────────────────────────────
it('processes a PDF and returns document type', async () => {
// Fake PDF data won't parse — should gracefully handle extraction failure
const content = Buffer.from('fake-pdf').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'report.pdf', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('document');
expect(body.files[0].summary).toContain('PDF');
});
// ── CSV processing ──────────────────────────────────────────────
it('processes a CSV and returns column/row stats', async () => {
const csvText = 'name,age,city\nAlice,30,NYC\nBob,25,LA\n';
const content = Buffer.from(csvText).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'data.csv', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('csv');
expect(body.files[0].summary).toContain('3 columns');
expect(body.files[0].summary).toContain('2 rows');
expect(body.files[0].content).toContain('Alice');
});
// ── Text processing ─────────────────────────────────────────────
it('processes a markdown file and returns content + line count', async () => {
const text = '# Hello\n\nSome content\nMore lines\n';
const content = Buffer.from(text).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'readme.md', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('text');
expect(body.files[0].summary).toContain('lines');
expect(body.files[0].content).toContain('# Hello');
});
it('processes TypeScript source code', async () => {
const code = 'const x = 1;\nconsole.log(x);\n';
const content = Buffer.from(code).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'main.ts', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('text');
expect(body.files[0].content).toContain('const x');
});
// ── Archive files ───────────────────────────────────────────────
it('processes a ZIP and returns archive type', async () => {
const content = Buffer.from('fake-zip-data').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'archive.zip', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('archive');
});
// ── Unsupported files ───────────────────────────────────────────
it('returns unsupported for unknown extensions', async () => {
const content = Buffer.from('binary').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'data.xyz', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('unsupported');
expect(body.files[0].summary).toContain('Unsupported');
});
// ── Multiple files ──────────────────────────────────────────────
it('processes multiple files in one request', async () => {
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: {
files: [
{ name: 'a.png', content: Buffer.from('img').toString('base64') },
{ name: 'b.csv', content: Buffer.from('h\n1').toString('base64') },
{ name: 'c.md', content: Buffer.from('# Hi').toString('base64') },
],
},
});
const body = JSON.parse(res.body);
expect(body.files).toHaveLength(3);
expect(body.files[0].type).toBe('image');
expect(body.files[1].type).toBe('csv');
expect(body.files[2].type).toBe('text');
});
// ── Base64 validation ──────────────────────────────────────────
it('returns 400 for invalid base64 content', async () => {
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'bad.txt', content: '!!!not-base64!!!' }] },
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain('Invalid base64');
});
// ── CSV with quoted fields ────────────────────────────────────
it('processes CSV with quoted fields containing commas', async () => {
const csvText = 'name,address,city\n"Smith, John","123 Main St, Apt 4",NYC\n';
const content = Buffer.from(csvText).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'quoted.csv', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('csv');
expect(body.files[0].summary).toContain('3 columns');
expect(body.files[0].summary).toContain('1 rows');
});
// ── Text line count accuracy ──────────────────────────────────
it('reports correct line count for text ending with newline', async () => {
const text = 'line1\nline2\nline3\n';
const content = Buffer.from(text).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'test.txt', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].summary).toBe('TXT file — 3 lines');
});
it('reports correct line count for text without trailing newline', async () => {
const text = 'line1\nline2\nline3';
const content = Buffer.from(text).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'test.txt', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].summary).toBe('TXT file — 3 lines');
});
// ── DOCX processing ─────────────────────────────────────────────
it('processes DOCX and returns document type', async () => {
const content = Buffer.from('fake-docx').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'report.docx', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('document');
expect(body.files[0].summary).toContain('DOCX');
});
// ── XLSX processing ────────────────────────────────────────────
it('processes XLSX and returns spreadsheet type', async () => {
const content = Buffer.from('fake-xlsx').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'data.xlsx', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('spreadsheet');
});
// ── New text extensions ────────────────────────────────────────
it('processes HTML files as text', async () => {
const html = '<html><body>Hello</body></html>';
const content = Buffer.from(html).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'page.html', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('text');
expect(body.files[0].content).toContain('<html>');
});
it('processes SQL files as text', async () => {
const sql = 'SELECT * FROM users WHERE id = 1;';
const content = Buffer.from(sql).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'query.sql', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('text');
expect(body.files[0].content).toContain('SELECT');
});
// ── SVG as image ──────────────────────────────────────────────
it('processes SVG as image', async () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><circle r="10"/></svg>';
const content = Buffer.from(svg).toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'icon.svg', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('image');
expect(body.files[0].content).toMatch(/^data:image\/svg\+xml;base64,/);
});
// ── PPTX processing ───────────────────────────────────────────
it('processes PPTX and returns document type', async () => {
const content = Buffer.from('fake-pptx').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'slides.pptx', content }] },
});
const body = JSON.parse(res.body);
expect(body.files[0].type).toBe('document');
expect(body.files[0].summary).toContain('PPTX');
});
// ── workspaceId ─────────────────────────────────────────────────
it('accepts optional workspaceId without error', async () => {
const content = Buffer.from('text').toString('base64');
const res = await injectWithAuth(server, {
method: 'POST', url: '/api/ingest',
payload: { files: [{ name: 'note.txt', content }], workspaceId: 'ws-123' },
});
expect(res.statusCode).toBe(200);
});
});