import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { resolveLinksSecondPass } from '../lib/sync/link-resolution-pass.js'; import type { SpaceConfigWithState } from '../lib/space-config.js'; describe('Link Resolution Second Pass', () => { let testDir: string; beforeEach(() => { testDir = join(tmpdir(), `cn-link-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); test('resolves unresolved links in second pass', () => { // Create markdown files with unresolved links const pageAPath = join(testDir, 'page-a.md'); const pageBPath = join(testDir, 'page-b.md'); // Page A has an unresolved link to Page B (as Confluence HTML) const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A This is a link to . `; // Page B exists const pageBContent = `--- page_id: page-b-id title: Page B version: 1 --- # Page B Content of page B. `; writeFileSync(pageAPath, pageAContent, 'utf-8'); writeFileSync(pageBPath, pageBContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', 'page-b-id': 'page-b.md', }, folders: {}, lastSync: new Date().toISOString(), }; // Run second pass const result = resolveLinksSecondPass(testDir, config); // Check results expect(result.filesUpdated).toBe(1); // Only page-a.md should be updated expect(result.linksResolved).toBe(1); expect(result.warnings).toHaveLength(0); // Verify the link was converted const updatedPageA = readFileSync(pageAPath, 'utf-8'); expect(updatedPageA).toContain('[Page B](./page-b.md)'); expect(updatedPageA).not.toContain(''); }); test('handles multiple unresolved links in one file', () => { const pageAPath = join(testDir, 'page-a.md'); const pageBPath = join(testDir, 'page-b.md'); const pageCPath = join(testDir, 'page-c.md'); const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Link to . Link to . `; const pageBContent = `--- page_id: page-b-id title: Page B version: 1 --- # Page B `; const pageCContent = `--- page_id: page-c-id title: Page C version: 1 --- # Page C `; writeFileSync(pageAPath, pageAContent, 'utf-8'); writeFileSync(pageBPath, pageBContent, 'utf-8'); writeFileSync(pageCPath, pageCContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', 'page-b-id': 'page-b.md', 'page-c-id': 'page-c.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); expect(result.filesUpdated).toBe(1); expect(result.linksResolved).toBe(2); const updatedPageA = readFileSync(pageAPath, 'utf-8'); expect(updatedPageA).toContain('[Page B](./page-b.md)'); expect(updatedPageA).toContain('[Page C](./page-c.md)'); }); test('skips files with no unresolved links', () => { const pageAPath = join(testDir, 'page-a.md'); const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Normal content with no links. `; writeFileSync(pageAPath, pageAContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); expect(result.filesUpdated).toBe(0); expect(result.linksResolved).toBe(0); }); test('leaves unresolvable links as-is', () => { const pageAPath = join(testDir, 'page-a.md'); // Link to a page that doesn't exist const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Link to . `; writeFileSync(pageAPath, pageAContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); // No changes should be made expect(result.filesUpdated).toBe(0); expect(result.linksResolved).toBe(0); // Link should still be unresolved const updatedPageA = readFileSync(pageAPath, 'utf-8'); expect(updatedPageA).toContain(''); expect(updatedPageA).toContain('Nonexistent Page'); }); test('resolves links in nested directories', () => { const subDir = join(testDir, 'subdir'); mkdirSync(subDir, { recursive: true }); const pageAPath = join(subDir, 'page-a.md'); const pageBPath = join(testDir, 'page-b.md'); const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Link to . `; const pageBContent = `--- page_id: page-b-id title: Page B version: 1 --- # Page B `; writeFileSync(pageAPath, pageAContent, 'utf-8'); writeFileSync(pageBPath, pageBContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'subdir/page-a.md', 'page-b-id': 'page-b.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); expect(result.filesUpdated).toBe(1); expect(result.linksResolved).toBe(1); const updatedPageA = readFileSync(pageAPath, 'utf-8'); // Link should be relative from subdir/ to root expect(updatedPageA).toContain('[Page B](../page-b.md)'); }); test('decodes HTML entities in page titles', () => { const pageAPath = join(testDir, 'page-a.md'); const pageBPath = join(testDir, 'page-b.md'); // Page A has a link with HTML entities in the title const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Link to . `; // Page B has a title with ampersand const pageBContent = `--- page_id: page-b-id title: Page & Info version: 1 --- # Page & Info `; writeFileSync(pageAPath, pageAContent, 'utf-8'); writeFileSync(pageBPath, pageBContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', 'page-b-id': 'page-b.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); expect(result.filesUpdated).toBe(1); expect(result.linksResolved).toBe(1); const updatedPageA = readFileSync(pageAPath, 'utf-8'); // Link text should also be decoded expect(updatedPageA).toContain('[API & Docs](./page-b.md)'); expect(updatedPageA).not.toContain('&'); }); test('handles links without CDATA link text', () => { const pageAPath = join(testDir, 'page-a.md'); const pageBPath = join(testDir, 'page-b.md'); // Link without ac:plain-text-link-body section const pageAContent = `--- page_id: page-a-id title: Page A version: 1 --- # Page A Link to . `; const pageBContent = `--- page_id: page-b-id title: Page B version: 1 --- # Page B `; writeFileSync(pageAPath, pageAContent, 'utf-8'); writeFileSync(pageBPath, pageBContent, 'utf-8'); const config: SpaceConfigWithState = { spaceKey: 'TEST', spaceId: 'test-space-id', spaceName: 'Test Space', pages: { 'page-a-id': 'page-a.md', 'page-b-id': 'page-b.md', }, folders: {}, lastSync: new Date().toISOString(), }; const result = resolveLinksSecondPass(testDir, config); expect(result.filesUpdated).toBe(1); expect(result.linksResolved).toBe(1); const updatedPageA = readFileSync(pageAPath, 'utf-8'); // Should use title as link text when CDATA is missing expect(updatedPageA).toContain('[Page B](./page-b.md)'); }); });