From 25a3142db38a411b8ab3ee1905e683048f0a2c70 Mon Sep 17 00:00:00 2001 From: Greg Priday Date: Thu, 26 Feb 2026 14:05:06 +1100 Subject: [PATCH] test(e2e): add golden file tests for Markdown formatter variants - Add tests/e2e/markdown-formatter.test.js with 9 test cases covering all supported Markdown output modes and flag combinations - Test base markdown output, --with-line-numbers, --only-tree, --with-git-status, combined flags, and --show-size - Add fence language detection test verifying correct syntax highlighting tags for js, css, json, yaml, python, go, rust, bash, txt extensions - Add begin/end marker placement test with interleaving order validation - Generate 8 golden files in tests/fixtures/goldens/markdown/ covering all variants for regression detection - Follow existing E2E patterns from output-formats.test.js and flags-and-combos.test.js for consistency --- tests/e2e/markdown-formatter.test.js | 354 ++++++++++++++++++ .../fixtures/goldens/markdown/base.md.golden | 73 ++++ .../markdown/language-detection.md.golden | 144 +++++++ .../line-numbers-git-status.md.golden | 74 ++++ .../markdown/only-tree-git-status.md.golden | 36 ++ .../goldens/markdown/only-tree.md.golden | 36 ++ .../markdown/with-git-status.md.golden | 74 ++++ .../markdown/with-line-numbers.md.golden | 73 ++++ .../goldens/markdown/with-show-size.md.golden | 73 ++++ 9 files changed, 937 insertions(+) create mode 100644 tests/e2e/markdown-formatter.test.js create mode 100644 tests/fixtures/goldens/markdown/base.md.golden create mode 100644 tests/fixtures/goldens/markdown/language-detection.md.golden create mode 100644 tests/fixtures/goldens/markdown/line-numbers-git-status.md.golden create mode 100644 tests/fixtures/goldens/markdown/only-tree-git-status.md.golden create mode 100644 tests/fixtures/goldens/markdown/only-tree.md.golden create mode 100644 tests/fixtures/goldens/markdown/with-git-status.md.golden create mode 100644 tests/fixtures/goldens/markdown/with-line-numbers.md.golden create mode 100644 tests/fixtures/goldens/markdown/with-show-size.md.golden diff --git a/tests/e2e/markdown-formatter.test.js b/tests/e2e/markdown-formatter.test.js new file mode 100644 index 0000000..3b91aa1 --- /dev/null +++ b/tests/e2e/markdown-formatter.test.js @@ -0,0 +1,354 @@ +/** + * E2E Tests: Markdown Formatter Variants + * + * Dedicated golden file tests for the Markdown formatter covering + * all supported modes and flag combinations: + * - Base output (no flags) + * - --with-line-numbers + * - --only-tree + * - --with-git-status + * - Combined flags + * - Fence language detection + */ + +import path from 'path'; +import os from 'os'; +import { randomUUID } from 'crypto'; +import { execSync } from 'child_process'; +import { mkdirSync, cpSync, rmSync, appendFileSync, writeFileSync } from 'fs'; +import { runCli, normalize, getGitEnv } from './_utils.js'; + +const PROJECT = path.resolve(process.cwd(), 'tests/fixtures/simple-project'); + +describe('Markdown formatter variants', () => { + test('base markdown output (no extra flags)', async () => { + const { code, stdout, stderr } = await runCli([PROJECT, '--format', 'markdown', '--display']); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: PROJECT }); + expect(normalized).toMatchGolden('markdown/base.md.golden'); + }, 30000); + + test('markdown with --with-line-numbers', async () => { + const { code, stdout, stderr } = await runCli([ + PROJECT, + '--format', + 'markdown', + '--with-line-numbers', + '--display', + ]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: PROJECT }); + + // Verify line numbers appear in the Markdown-specific format (e.g. " 1: content") + expect(normalized).toMatch(/^\s+1:\s/m); + // Verify subsequent line numbers also appear + expect(normalized).toMatch(/^\s+2:\s/m); + + expect(normalized).toMatchGolden('markdown/with-line-numbers.md.golden'); + }, 30000); + + test('markdown with --only-tree', async () => { + const { code, stdout, stderr } = await runCli([ + PROJECT, + '--format', + 'markdown', + '--only-tree', + '--display', + ]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: PROJECT }); + + // Verify only_tree is true in front matter + expect(normalized).toMatch(/only_tree:\s*true/); + // Verify no Files section + expect(normalized).not.toMatch(/^## Files/m); + // Verify no file-begin markers + expect(normalized).not.toContain('copytree:file-begin'); + + expect(normalized).toMatchGolden('markdown/only-tree.md.golden'); + }, 30000); + + test('markdown with --with-git-status', async () => { + const tmpDir = path.join(os.tmpdir(), `copytree-md-git-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); + + try { + cpSync(PROJECT, tmpDir, { recursive: true }); + const gitEnv = getGitEnv(); + + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); + execSync('git config user.name copytree-bot', { + cwd: tmpDir, + stdio: 'pipe', + }); + execSync('git config user.email bot@example.com', { + cwd: tmpDir, + stdio: 'pipe', + }); + + execSync('git add .', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + execSync('git commit -m "baseline"', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + + // Modify a file and add an untracked file + appendFileSync(path.join(tmpDir, 'README.md'), '\nTemp change for testing.'); + writeFileSync(path.join(tmpDir, 'UNTRACKED.tmp'), 'untracked content'); + + const { code, stdout, stderr } = await runCli([ + tmpDir, + '--format', + 'markdown', + '--with-git-status', + '--display', + ]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: tmpDir }); + + // Verify git status is reflected in front matter + expect(normalized).toMatch(/include_git_status:\s*true/); + + expect(normalized).toMatchGolden('markdown/with-git-status.md.golden'); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + }, 30000); + + test('markdown with --with-line-numbers --with-git-status (combined)', async () => { + const tmpDir = path.join(os.tmpdir(), `copytree-md-combined-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); + + try { + cpSync(PROJECT, tmpDir, { recursive: true }); + const gitEnv = getGitEnv(); + + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); + execSync('git config user.name copytree-bot', { + cwd: tmpDir, + stdio: 'pipe', + }); + execSync('git config user.email bot@example.com', { + cwd: tmpDir, + stdio: 'pipe', + }); + + execSync('git add .', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + execSync('git commit -m "baseline"', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + + appendFileSync(path.join(tmpDir, 'index.js'), '\n// modified line'); + + const { code, stdout, stderr } = await runCli([ + tmpDir, + '--format', + 'markdown', + '--with-line-numbers', + '--with-git-status', + '--display', + ]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: tmpDir }); + + // Verify both flags reflected in front matter + expect(normalized).toMatch(/include_git_status:\s*true/); + expect(normalized).toMatch(/include_line_numbers:\s*true/); + + expect(normalized).toMatchGolden('markdown/line-numbers-git-status.md.golden'); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + }, 30000); + + test('markdown with --only-tree --with-git-status (combined)', async () => { + const tmpDir = path.join(os.tmpdir(), `copytree-md-tree-git-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); + + try { + cpSync(PROJECT, tmpDir, { recursive: true }); + const gitEnv = getGitEnv(); + + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); + execSync('git config user.name copytree-bot', { + cwd: tmpDir, + stdio: 'pipe', + }); + execSync('git config user.email bot@example.com', { + cwd: tmpDir, + stdio: 'pipe', + }); + + execSync('git add .', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + execSync('git commit -m "baseline"', { + cwd: tmpDir, + stdio: 'pipe', + env: { ...process.env, ...gitEnv }, + }); + + appendFileSync(path.join(tmpDir, 'README.md'), '\nModified for git status.'); + + const { code, stdout, stderr } = await runCli([ + tmpDir, + '--format', + 'markdown', + '--only-tree', + '--with-git-status', + '--display', + ]); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: tmpDir }); + + // Both flags active + expect(normalized).toMatch(/only_tree:\s*true/); + expect(normalized).toMatch(/include_git_status:\s*true/); + // No file content sections + expect(normalized).not.toContain('copytree:file-begin'); + + expect(normalized).toMatchGolden('markdown/only-tree-git-status.md.golden'); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + }, 30000); + + test('fence language detection for various file types', async () => { + // Create a temp project with multiple file extensions + const tmpDir = path.join(os.tmpdir(), `copytree-md-lang-${randomUUID()}`); + mkdirSync(tmpDir, { recursive: true }); + + try { + const files = { + 'app.js': 'const x = 1;', + 'style.css': 'body { margin: 0; }', + 'data.json': '{"key": "value"}', + 'config.yml': 'key: value', + 'script.py': 'print("hello")', + 'main.go': 'package main', + 'lib.rs': 'fn main() {}', + 'run.sh': '#!/bin/bash', + 'notes.txt': 'plain text notes', + 'unknown.xyz': 'unknown extension content', + }; + + for (const [name, content] of Object.entries(files)) { + writeFileSync(path.join(tmpDir, name), content); + } + + const { code, stdout, stderr } = await runCli([tmpDir, '--format', 'markdown', '--display']); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: tmpDir }); + + // Verify fence language tags for known extensions + expect(normalized).toMatch(/```js\n/); + expect(normalized).toMatch(/```css\n/); + expect(normalized).toMatch(/```json\n/); + expect(normalized).toMatch(/```yaml\n/); + expect(normalized).toMatch(/```python\n/); + expect(normalized).toMatch(/```go\n/); + expect(normalized).toMatch(/```rust\n/); + expect(normalized).toMatch(/```bash\n/); + expect(normalized).toMatch(/```text\n/); + // Unknown extension: no language tag (just ```) + expect(normalized).toMatch(/```\n(?:unknown extension content)/); + + expect(normalized).toMatchGolden('markdown/language-detection.md.golden'); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + } + }, 30000); + + test('begin/end markers are properly placed', async () => { + const { code, stdout, stderr } = await runCli([PROJECT, '--format', 'markdown', '--display']); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const normalized = normalize(stdout, { projectRoot: PROJECT }); + + // Count begin and end markers + const beginMarkers = normalized.match(/ +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @index.js + +```js +console.log('Hello'); + +``` + + + + + +### @README.md + +```markdown +# Test Project + +``` + + + + + +### @src/test.js + +```js +export function test() { + return 42; +} + +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/language-detection.md.golden b/tests/fixtures/goldens/markdown/language-detection.md.golden new file mode 100644 index 0000000..04ca531 --- /dev/null +++ b/tests/fixtures/goldens/markdown/language-detection.md.golden @@ -0,0 +1,144 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 10 +total_size_bytes: 147 +char_limit_applied: false +only_tree: false +include_git_status: false +include_line_numbers: false +instructions: + name: "default" + included: true +--- + +# CopyTree Export — copytree-md-lang- + +## Directory Tree +```text +├── app.js (12 B) +├── config.yml (10 B) +├── data.json (16 B) +├── lib.rs (12 B) +├── main.go (12 B) +├── notes.txt (16 B) +├── run.sh (11 B) +├── script.py (14 B) +├── style.css (19 B) +└── unknown.xyz (25 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @app.js + +```js +const x = 1; +``` + + + + + +### @config.yml + +```yaml +key: value +``` + + + + + +### @data.json + +```json +{"key": "value"} +``` + + + + + +### @lib.rs + +```rust +fn main() {} +``` + + + + + +### @main.go + +```go +package main +``` + + + + + +### @notes.txt + +```text +plain text notes +``` + + + + + +### @run.sh + +```bash +#!/bin/bash +``` + + + + + +### @script.py + +```python +print("hello") +``` + + + + + +### @style.css + +```css +body { margin: 0; } +``` + + + + + +### @unknown.xyz + +``` +unknown extension content +``` + + +🖥️ 10 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/line-numbers-git-status.md.golden b/tests/fixtures/goldens/markdown/line-numbers-git-status.md.golden new file mode 100644 index 0000000..c81ffaf --- /dev/null +++ b/tests/fixtures/goldens/markdown/line-numbers-git-status.md.golden @@ -0,0 +1,74 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 94 +char_limit_applied: false +only_tree: false +include_git_status: true +include_line_numbers: true +instructions: + name: "default" + included: true +--- + +# CopyTree Export — copytree-md-combined- + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (39 B) +└── README.md (15 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @index.js + +```js + 1: console.log('Hello'); + 2: + 3: // modified line +``` + + + + + +### @README.md + +```markdown + 1: # Test Project + 2: +``` + + + + + +### @src/test.js + +```js + 1: export function test() { + 2: return 42; + 3: } + 4: +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/only-tree-git-status.md.golden b/tests/fixtures/goldens/markdown/only-tree-git-status.md.golden new file mode 100644 index 0000000..adb25ad --- /dev/null +++ b/tests/fixtures/goldens/markdown/only-tree-git-status.md.golden @@ -0,0 +1,36 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 102 +char_limit_applied: false +only_tree: true +include_git_status: true +include_line_numbers: false +instructions: + name: "default" + included: true +--- + +# CopyTree Export — copytree-md-tree-git- + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (22 B) +└── README.md (40 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/only-tree.md.golden b/tests/fixtures/goldens/markdown/only-tree.md.golden new file mode 100644 index 0000000..ed522ca --- /dev/null +++ b/tests/fixtures/goldens/markdown/only-tree.md.golden @@ -0,0 +1,36 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 77 +char_limit_applied: false +only_tree: true +include_git_status: false +include_line_numbers: false +instructions: + name: "default" + included: true +--- + +# CopyTree Export — simple-project + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (22 B) +└── README.md (15 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/with-git-status.md.golden b/tests/fixtures/goldens/markdown/with-git-status.md.golden new file mode 100644 index 0000000..c2c8d89 --- /dev/null +++ b/tests/fixtures/goldens/markdown/with-git-status.md.golden @@ -0,0 +1,74 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 102 +char_limit_applied: false +only_tree: false +include_git_status: true +include_line_numbers: false +instructions: + name: "default" + included: true +--- + +# CopyTree Export — copytree-md-git- + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (22 B) +└── README.md (40 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @index.js + +```js +console.log('Hello'); + +``` + + + + + +### @README.md + +```markdown +# Test Project + +Temp change for testing. +``` + + + + + +### @src/test.js + +```js +export function test() { + return 42; +} + +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/with-line-numbers.md.golden b/tests/fixtures/goldens/markdown/with-line-numbers.md.golden new file mode 100644 index 0000000..1f5533b --- /dev/null +++ b/tests/fixtures/goldens/markdown/with-line-numbers.md.golden @@ -0,0 +1,73 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 77 +char_limit_applied: false +only_tree: false +include_git_status: false +include_line_numbers: true +instructions: + name: "default" + included: true +--- + +# CopyTree Export — simple-project + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (22 B) +└── README.md (15 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @index.js + +```js + 1: console.log('Hello'); + 2: +``` + + + + + +### @README.md + +```markdown + 1: # Test Project + 2: +``` + + + + + +### @src/test.js + +```js + 1: export function test() { + 2: return 42; + 3: } + 4: +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file diff --git a/tests/fixtures/goldens/markdown/with-show-size.md.golden b/tests/fixtures/goldens/markdown/with-show-size.md.golden new file mode 100644 index 0000000..faae72b --- /dev/null +++ b/tests/fixtures/goldens/markdown/with-show-size.md.golden @@ -0,0 +1,73 @@ +--- +format: copytree-md@1 +tool: copytree +generated: "" +base_path: "" +profile: "default" +file_count: 3 +total_size_bytes: 77 +char_limit_applied: false +only_tree: false +include_git_status: false +include_line_numbers: false +instructions: + name: "default" + included: true +--- + +# CopyTree Export — simple-project + +## Directory Tree +```text +├── src/ +│ └── test.js (40 B) +├── index.js (22 B) +└── README.md (15 B) +``` + +## Instructions + + +```text +This XML document represents the directory structure and contents of a project, which you will analyze to answer user questions. Reference files using @ followed by the relative path from the project root (e.g., @README.md, @src/app.js). The @ notation must not be enclosed in backticks. For example, always write "...in the file @app/page.tsx..." and never "...in the file `@app/page.tsx`...". You can also reference directories with @directory/path to instruct the agent to examine multiple files. +``` + + + +## Files + + + +### @index.js + +```js +console.log('Hello'); + +``` + + + + + +### @README.md + +```markdown +# Test Project + +``` + + + + + +### @src/test.js + +```js +export function test() { + return 42; +} + +``` + + +🖥️ 3 files [] displayed to terminal \ No newline at end of file