import { describe, test, expect, beforeEach, mock } from 'bun:test'; import { PLCBundle } from '../src/plcbundle'; import { TEMP_DIR, createMockIndex, createMockOperations } from './setup'; describe('Bundle Processing', () => { let bundle: PLCBundle; beforeEach(async () => { bundle = new PLCBundle(TEMP_DIR); // Setup mock index const mockIndex = createMockIndex(); await bundle.saveIndex(mockIndex); // Create actual bundle files for testing for (let i = 1; i <= 3; i++) { const operations = createMockOperations(100); const jsonl = operations.map(op => JSON.stringify(op)).join('\n') + '\n'; const uncompressed = new TextEncoder().encode(jsonl); const compressed = Bun.zstdCompressSync(uncompressed); const bundlePath = bundle.getBundlePath(i); await Bun.write(bundlePath, compressed); } }); describe('parseOperations', () => { test('parses JSONL content', () => { const ops = createMockOperations(10); const jsonl = ops.map(op => JSON.stringify(op)).join('\n'); const parsed = bundle.parseOperations(jsonl); expect(parsed.length).toBe(10); expect(parsed[0].did).toBeDefined(); }); test('handles trailing newline', () => { const ops = createMockOperations(5); const jsonl = ops.map(op => JSON.stringify(op)).join('\n') + '\n'; const parsed = bundle.parseOperations(jsonl); expect(parsed.length).toBe(5); }); test('filters empty lines', () => { const ops = createMockOperations(3); const jsonl = ops.map(op => JSON.stringify(op)).join('\n\n\n'); const parsed = bundle.parseOperations(jsonl); expect(parsed.length).toBe(3); }); }); describe('streamOperations', () => { test('streams operations from bundle', async () => { const operations = []; for await (const { op, line } of bundle.streamOperations(1)) { operations.push(op); expect(line).toBeDefined(); expect(line.length).toBeGreaterThan(0); } expect(operations.length).toBe(100); expect(operations[0].did).toBeDefined(); }); test('includes raw line in stream', async () => { for await (const { op, line } of bundle.streamOperations(1)) { expect(typeof line).toBe('string'); expect(line.trim().length).toBeGreaterThan(0); // Line should be valid JSON const parsed = JSON.parse(line); expect(parsed.did).toBe(op.did); break; // Just check first } }); }); describe('processBundles', () => { test('processes with callback function', async () => { const callback = mock(() => {}); const stats = await bundle.processBundles(1, 1, callback); expect(callback).toHaveBeenCalled(); expect(callback.mock.calls.length).toBe(100); expect(stats.totalOps).toBe(100); }); test('callback receives all parameters', async () => { const callback = mock((op: any, position: number, bundleNum: number, line: string) => { expect(op).toBeDefined(); expect(typeof position).toBe('number'); expect(typeof bundleNum).toBe('number'); expect(typeof line).toBe('string'); }); await bundle.processBundles(1, 1, callback); expect(callback).toHaveBeenCalled(); }); test('tracks statistics accurately', async () => { const callback = mock(() => {}); const stats = await bundle.processBundles(1, 1, callback); expect(stats.totalOps).toBe(100); expect(stats.totalBytes).toBeGreaterThan(0); expect(stats.matchCount).toBe(0); expect(stats.matchedBytes).toBe(0); }); test('processes multiple bundles in order', async () => { const bundleNums: number[] = []; await bundle.processBundles(1, 3, (op, position, bundleNum) => { bundleNums.push(bundleNum); }); // Should process bundles 1, 2, 3 in order expect(bundleNums[0]).toBe(1); expect(bundleNums[99]).toBe(1); // Last of bundle 1 expect(bundleNums[100]).toBe(2); // First of bundle 2 expect(bundleNums[299]).toBe(3); // Last of bundle 3 }); test('supports progress callback', async () => { const progressCallback = mock(() => {}); await bundle.processBundles(1, 1, () => {}, { onProgress: progressCallback, }); // Progress should not be called for only 100 operations (threshold is 10,000) expect(progressCallback.mock.calls.length).toBe(0); }); test('calls progress callback at threshold', async () => { // Create larger bundle to trigger progress const largeOps = createMockOperations(15000); const jsonl = largeOps.map(op => JSON.stringify(op)).join('\n') + '\n'; const compressed = Bun.zstdCompressSync(new TextEncoder().encode(jsonl)); await Bun.write(bundle.getBundlePath(10), compressed); const progressCallback = mock(() => {}); await bundle.processBundles(10, 10, () => {}, { onProgress: progressCallback, }); // Should be called at least once (at 10,000 ops) expect(progressCallback.mock.calls.length).toBeGreaterThan(0); }); test('line length matches original JSONL', async () => { const sizes: number[] = []; await bundle.processBundles(1, 1, (op, position, bundleNum, line) => { sizes.push(line.length); // Line length should match serialized operation const serialized = JSON.stringify(op); expect(line.length).toBeGreaterThanOrEqual(serialized.length - 10); // Allow small variance }); expect(sizes.length).toBe(100); expect(sizes.every(s => s > 0)).toBe(true); }); test('supports async callbacks', async () => { const callback = mock(async (op: any) => { await new Promise(resolve => setTimeout(resolve, 1)); }); const stats = await bundle.processBundles(1, 1, callback); expect(callback).toHaveBeenCalled(); expect(stats.totalOps).toBe(100); }); test('handles errors in callback gracefully', async () => { let callCount = 0; // Don't throw error, just track calls await bundle.processBundles(1, 1, () => { callCount++; // Don't throw - just count }); expect(callCount).toBe(100); }); }); describe('processBundles with module path', () => { test('loads module and calls function', async () => { // Create a test module with absolute path const testModulePath = `${process.cwd()}/${TEMP_DIR}/test-module.ts`; await Bun.write(testModulePath, ` export function detect({ op }) { return op.did.startsWith('did:plc:') ? ['test'] : []; } `); const stats = await bundle.processBundles(1, 1, { module: testModulePath, }); expect(stats.totalOps).toBe(100); }); test('supports silent mode', async () => { // Create absolute path directly const testModulePath = `${process.cwd()}/${TEMP_DIR}/noisy-module.ts`; await Bun.write(testModulePath, ` export function detect({ op }) { console.log('NOISY OUTPUT'); return []; } `); // Capture console output const originalLog = console.log; const originalError = console.error; let logOutput = ''; console.log = (...args: any[]) => { logOutput += args.join(' ') + '\n'; }; try { // Without silent - should see output await bundle.processBundles(1, 1, { module: testModulePath, silent: false, }); expect(logOutput).toContain('NOISY OUTPUT'); // Reset and test with silent mode logOutput = ''; console.log = () => {}; console.error = () => {}; await bundle.processBundles(1, 1, { module: testModulePath, silent: true, }); // Should have no output expect(logOutput).toBe(''); } finally { console.log = originalLog; console.error = originalError; } }); test('loads module and calls function', async () => { // Create absolute path const testModulePath = `${process.cwd()}/${TEMP_DIR}/test-module.ts`; await Bun.write(testModulePath, ` export function detect({ op }) { return op.did.startsWith('did:plc:') ? ['test'] : []; } `); const stats = await bundle.processBundles(1, 1, { module: testModulePath, }); expect(stats.totalOps).toBe(100); }); test('throws error for multi-threading without module', async () => { let errorThrown = false; let errorMessage = ''; try { await bundle.processBundles(1, 1, () => {}, { threads: 4, }); } catch (error) { errorThrown = true; errorMessage = (error as Error).message; } expect(errorThrown).toBe(true); expect(errorMessage).toContain('module'); }); }); describe('performance', () => { test('fast path is faster than generator', async () => { const callback = mock(() => {}); const start = Date.now(); await bundle.processBundles(1, 3, callback); const duration = Date.now() - start; // Should complete reasonably fast (300 operations) expect(duration).toBeLessThan(1000); // Less than 1 second expect(callback.mock.calls.length).toBe(300); }); test('processes large batches efficiently', async () => { const callback = mock(() => {}); const stats = await bundle.processBundles(1, 3, callback); // Should handle all operations expect(stats.totalOps).toBe(300); expect(stats.totalBytes).toBeGreaterThan(0); }); }); });