/** * Tests for the pure import injector (Phase 3) * * These tests verify the injector's ability to replace import markers * with resolved content WITHOUT any filesystem dependencies. */ import { describe, it, expect } from 'bun:test'; import { injectImports, createResolvedImport } from './imports-injector'; import type { ResolvedImport, ImportAction } from './imports-types'; describe('injectImports', () => { describe('basic injection', () => { it('injects single file import', () => { const original = 'Before @./file.md After'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './file.md', original: '@./file.md', index: 7, }, content: 'INJECTED', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Before INJECTED After'); }); it('injects at start of string', () => { const original = '@./file.md rest'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './file.md', original: '@./file.md', index: 0, }, content: 'START', }, ]; const result = injectImports(original, resolved); expect(result).toBe('START rest'); }); it('injects at end of string', () => { const original = 'start @./file.md'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './file.md', original: '@./file.md', index: 6, }, content: 'END', }, ]; const result = injectImports(original, resolved); expect(result).toBe('start END'); }); it('returns original when no imports', () => { const original = 'No imports here'; const result = injectImports(original, []); expect(result).toBe(original); }); }); describe('multiple imports', () => { it('injects multiple imports in order', () => { const original = '@./a.md and @./b.md'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './a.md', original: '@./a.md', index: 0, }, content: 'AAA', }, { action: { type: 'file', path: './b.md', original: '@./b.md', index: 12, }, content: 'BBB', }, ]; const result = injectImports(original, resolved); expect(result).toBe('AAA and BBB'); }); it('handles many imports', () => { let original = ''; const resolved: ResolvedImport[] = []; for (let i = 0; i < 10; i++) { const marker = `@./f${i}.md`; const startIndex = original.length; original += marker + ' '; resolved.push({ action: { type: 'file', path: `./f${i}.md`, original: marker, index: startIndex, }, content: `[${i}]`, }); } const result = injectImports(original, resolved); expect(result).toBe('[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] '); }); it('handles imports out of order in resolved array', () => { const original = '@./first.md middle @./second.md'; // Provide in reverse order - injector should handle it const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './second.md', original: '@./second.md', index: 19, }, content: 'TWO', }, { action: { type: 'file', path: './first.md', original: '@./first.md', index: 0, }, content: 'ONE', }, ]; const result = injectImports(original, resolved); expect(result).toBe('ONE middle TWO'); }); }); describe('mixed import types', () => { it('injects file, URL, and command imports', () => { const original = 'File: @./f.md URL: @https://x.com Cmd: !`ls`'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './f.md', original: '@./f.md', index: 6, }, content: 'FILE_CONTENT', }, { action: { type: 'url', url: 'https://x.com', original: '@https://x.com', index: 19, }, content: 'URL_CONTENT', }, { action: { type: 'command', command: 'ls', original: '!`ls`', index: 39, }, content: 'CMD_OUTPUT', }, ]; const result = injectImports(original, resolved); expect(result).toBe('File: FILE_CONTENT URL: URL_CONTENT Cmd: CMD_OUTPUT'); }); it('injects glob import', () => { const original = 'Files: @./src/*.ts'; const resolved: ResolvedImport[] = [ { action: { type: 'glob', pattern: './src/*.ts', original: '@./src/*.ts', index: 7, }, content: 'content1\ncontent2', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Files: content1\ncontent2'); }); it('injects symbol import', () => { const original = 'Type: @./types.ts#User'; const resolved: ResolvedImport[] = [ { action: { type: 'symbol', path: './types.ts', symbol: 'User', original: '@./types.ts#User', index: 6, }, content: 'interface User { name: string; }', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Type: interface User { name: string; }'); }); }); describe('content variations', () => { it('handles empty content injection', () => { const original = 'A @./empty.md B'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './empty.md', original: '@./empty.md', index: 2, }, content: '', }, ]; const result = injectImports(original, resolved); expect(result).toBe('A B'); }); it('handles multiline content injection', () => { const original = 'Start @./multi.md End'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './multi.md', original: '@./multi.md', index: 6, }, content: 'Line 1\nLine 2\nLine 3', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Start Line 1\nLine 2\nLine 3 End'); }); it('handles content with special characters', () => { const original = 'Data: @./data.json'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './data.json', original: '@./data.json', index: 6, }, content: '{"key": "value", "arr": [1, 2, 3]}', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Data: {"key": "value", "arr": [1, 2, 3]}'); }); it('handles content with regex-special characters', () => { const original = '@./regex.txt'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './regex.txt', original: '@./regex.txt', index: 0, }, content: '$1 $2 .* \\d+ [a-z]', }, ]; const result = injectImports(original, resolved); expect(result).toBe('$1 $2 .* \\d+ [a-z]'); }); it('handles content larger than original', () => { const original = '@./x.md'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './x.md', original: '@./x.md', index: 0, }, content: 'This is a much longer piece of content that replaces a short import marker', }, ]; const result = injectImports(original, resolved); expect(result).toBe('This is a much longer piece of content that replaces a short import marker'); }); it('handles unicode content', () => { const original = 'Emoji: @./emoji.md'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './emoji.md', original: '@./emoji.md', index: 7, }, content: '\u{1F680} \u{1F389} \u{2728}', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Emoji: \u{1F680} \u{1F389} \u{2728}'); }); }); describe('edge cases', () => { it('handles adjacent imports', () => { const original = '@./a.md@./b.md'; // Note: In real parsing, @./a.md@./b.md might be parsed as one long path // This test assumes they're somehow parsed separately const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './a.md', original: '@./a.md', index: 0, }, content: 'A', }, { action: { type: 'file', path: './b.md', original: '@./b.md', index: 7, }, content: 'B', }, ]; const result = injectImports(original, resolved); expect(result).toBe('AB'); }); it('preserves surrounding whitespace', () => { const original = ' @./f.md '; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './f.md', original: '@./f.md', index: 2, }, content: 'X', }, ]; const result = injectImports(original, resolved); expect(result).toBe(' X '); }); it('handles newlines around imports', () => { const original = 'Line 1\n@./f.md\nLine 3'; const resolved: ResolvedImport[] = [ { action: { type: 'file', path: './f.md', original: '@./f.md', index: 7, }, content: 'Line 2', }, ]; const result = injectImports(original, resolved); expect(result).toBe('Line 1\nLine 2\nLine 3'); }); }); }); describe('createResolvedImport', () => { it('creates resolved import from action and content', () => { const action: ImportAction = { type: 'file', path: './test.md', original: '@./test.md', index: 0, }; const resolved = createResolvedImport(action, 'test content'); expect(resolved).toEqual({ action, content: 'test content', }); }); });