diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..db840f7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test & Coverage + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests with coverage + run: pnpm run test:coverage + + - name: Check coverage threshold + run: | + # Extract coverage percentages from coverage summary + LINES=$(jq '.total.lines.pct' coverage/coverage-summary.json | cut -d. -f1) + FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json | cut -d. -f1) + BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json | cut -d. -f1) + STATEMENTS=$(jq '.total.statements.pct' coverage/coverage-summary.json | cut -d. -f1) + + echo "Coverage: Lines=$LINES% Functions=$FUNCTIONS% Branches=$BRANCHES% Statements=$STATEMENTS%" + + if [ "$LINES" -lt 40 ]; then + echo "❌ Lines coverage ($LINES%) is below 40% threshold" + exit 1 + fi + if [ "$FUNCTIONS" -lt 40 ]; then + echo "❌ Functions coverage ($FUNCTIONS%) is below 40% threshold" + exit 1 + fi + if [ "$BRANCHES" -lt 40 ]; then + echo "❌ Branches coverage ($BRANCHES%) is below 40% threshold" + exit 1 + fi + if [ "$STATEMENTS" -lt 40 ]; then + echo "❌ Statements coverage ($STATEMENTS%) is below 40% threshold" + exit 1 + fi + + echo "✅ All coverage thresholds met!" + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + if: always() + with: + file: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run linter + run: pnpm run lint + + typecheck: + name: Type Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run type check + run: pnpm exec tsc --noEmit diff --git a/TEST_AUDIT_REPORT.md b/TEST_AUDIT_REPORT.md new file mode 100644 index 0000000..385536e --- /dev/null +++ b/TEST_AUDIT_REPORT.md @@ -0,0 +1,1643 @@ +# 🔍 Comprehensive Test Audit Report +**docs-engine** | Generated: 2025-11-11 + +═══════════════════════════════════════════════════════════════════════════════ + +## 📊 Executive Summary + +**Current Status:** 🔄 Below Coverage Threshold + +``` +Total Test Files: 16 +Total Test Cases: 328 +Test Lines of Code: 3,596 +Source Files: 70 +Coverage: 23.38% ❌ (Target: 40%) +All Tests Passing: ✅ 327/328 passing + +Coverage Breakdown: + Lines: 23.38% ❌ (Target: 40%) + Functions: 29.85% ❌ (Target: 40%) + Branches: 18.11% ❌ (Target: 40%) + Statements: 23.33% ❌ (Target: 40%) +``` + +**Key Findings:** +• 40/70 source files (57%) have NO test coverage +• 96 lines of duplicate test code identified (can be reduced to ~15 lines) +• Tests that exist are generally high quality with good edge case coverage +• No integration tests, E2E tests, or CI/CD pipeline detected +• Security tests present (XSS, sanitization) - strong foundation + +**Overall Grade:** C+ (Good foundation, needs expansion) + +═══════════════════════════════════════════════════════════════════════════════ + +## 🎯 Critical Issues Requiring Immediate Action + +### 1. Coverage Below Threshold ⚠️ +**Impact:** HIGH | **Effort:** HIGH +``` +Current: 23.38% +Target: 40% +Gap: -16.62% +``` +**Recommendation:** Prioritize adding tests for high-risk untested modules + +### 2. Massive Test Duplication 🔴 +**Impact:** MEDIUM | **Effort:** LOW +``` +Location: src/lib/plugins/callouts.test.ts (lines 49-145) +Issue: 9 nearly identical tests (96 lines) +Solution: Parameterize with test.each() (reduce to ~15 lines) +Savings: ~80 lines of code +``` + +### 3. Missing Critical Module Tests ⚠️ +**Impact:** HIGH | **Effort:** HIGH +``` +Untested High-Risk Modules: +• reference.ts (287 lines) - Symbol reference plugin +• screenshot-service.ts (514 lines) - Screenshot capture service +• image-processor.ts (318 lines) - Image processing pipeline +• circuit-breaker.ts (217 lines) - Fault tolerance +• collapse.ts (190 lines) - Collapsible sections +• toc.ts (184 lines) - Table of contents generation +``` + +### 4. No CI/CD Integration ⚠️ +**Impact:** MEDIUM | **Effort:** LOW +``` +Issue: No automated testing on push/PR +Risk: Breaking changes can reach main branch undetected +Action: Add GitHub Actions workflow (see recommendations) +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 📋 Test Quality Assessment by Module + +### ✅ EXCELLENT QUALITY (Keep as-is) + +**sanitize.test.ts** +``` +Rating: ⭐⭐⭐⭐⭐ (5/5) +Coverage: 100% +Tests: 24 +Strengths: • Comprehensive XSS attack scenarios + • Tests all sanitization contexts (HTML/SVG/Error) + • Good security edge cases (protocol injection, event handlers) + • Clear, focused tests +Gaps: None - exemplary test suite +``` + +**markdown.test.ts** +``` +Rating: ⭐⭐⭐⭐ (4/5) +Coverage: 100% +Tests: 38 +Strengths: • Thorough edge case coverage + • Tests all markdown utility functions + • Good special character handling +Issues: • Tests implementation details (timestamps, blank lines) + • Some redundant tests (empty headers + empty rows) +Action: Remove 5 redundant tests (lines 101-110, 112-115, 196-199, 220-224) +``` + +**frontmatter.test.ts** +``` +Rating: ⭐⭐⭐⭐ (4/5) +Coverage: 100% +Tests: 21 +Strengths: • Good YAML parsing edge cases + • Tests title extraction hierarchy + • Malformed input handling +Issues: • Weak assertions (toBeDefined() instead of exact values) + • Missing tests for multiple delimiters +Action: Strengthen assertions, add 3 edge case tests +``` + +### ⚠️ GOOD QUALITY (Minor improvements needed) + +**symbol-resolver.test.ts** +``` +Rating: ⭐⭐⭐⭐ (4/5) +Coverage: 84.94% +Tests: 21 +Strengths: • Tests all 6 symbol kinds + • Ambiguity detection with suggestions + • Path hint filtering +Gaps: • Missing case sensitivity tests + • Missing glob pattern tests +Action: Add 3-5 tests for missing edge cases +``` + +**rate-limiter.test.ts** +``` +Rating: ⭐⭐⭐ (3/5) +Coverage: 66.66% +Tests: 45 +Strengths: • Good use of fake timers + • Tests sliding windows + • Per-user isolation +Issues: • Redundant tests (concurrent + exceeding limit test same thing) + • Tests implementation details (counting mechanism) + • Unrealistic edge cases (100ms windows, empty identifiers) +Action: Remove 4 redundant tests, add input validation tests +``` + +**image-optimization.test.ts** +``` +Rating: ⭐⭐⭐⭐ (4/5) +Coverage: 100% +Tests: 8 +Strengths: • Tests external URL handling + • Configuration options tested + • Base64 encoding verified +Gaps: • Missing error handling tests (corrupt images) + • Missing performance tests (very large images) +Action: Add 5-7 error handling tests +``` + +### 🔴 NEEDS IMPROVEMENT (Refactoring required) + +**callouts.test.ts** +``` +Rating: ⭐⭐ (2/5) +Coverage: 32.53% +Tests: 13 +Critical: 🔴 MASSIVE CODE DUPLICATION +Issue: Lines 49-145 contain 9 identical tests (96 lines) + Only difference is callout type (NOTE, TIP, WARNING, etc.) + +Before: + test('should transform NOTE callout', () => { /* 10 lines */ }); + test('should transform TIP callout', () => { /* 10 lines */ }); + test('should transform WARNING callout', () => { /* 10 lines */ }); + ... 6 more identical tests ... + +After (recommended): + const types = [ + { type: 'NOTE', class: 'blue', title: 'Note' }, + { type: 'TIP', class: 'green', title: 'Tip' }, + // ... + ]; + test.each(types)('should transform $type callout', ({ type, class, title }) => { + // Single test implementation + }); + +Savings: 96 lines → ~15 lines (81 lines saved) +Gaps: • No tests for nested blockquotes + • No tests for markdown in callout content + • No case sensitivity tests +Action: Refactor to parameterized tests, add 5 edge case tests +``` + +**links.test.ts** +``` +Rating: ⭐⭐ (2/5) +Coverage: 89.79% +Tests: 23 +Critical: 🔴 DUPLICATE TOP-LEVEL FILE TESTS +Issue: Lines 65-133 contain 7 tests that follow identical pattern + (README, LICENSE, CONTRIBUTING, CHANGELOG, etc.) + +Action: Parameterize with test.each(), add query param tests +Savings: ~40 lines of code +Gaps: • No tests for URLs with query parameters + • No tests for .MD vs .md case sensitivity + • No tests for malformed URLs +``` + +**code-highlight.test.ts** +``` +Rating: ⭐⭐ (2/5) +Coverage: 83.33% +Tests: 13 +Issues: • Tests TypeScript interfaces at runtime (useless) + • Tests plugin API shape (TypeScript's job) + • Long justification comments (code smell) + • Type casting everywhere (unsafe) +Action: Remove 2 useless tests, improve assertions, add HTML injection tests +Savings: ~30 lines of code +``` + +**file-io.test.ts** +``` +Rating: ⭐⭐ (2/5) +Coverage: 90% +Tests: 32 +Issues: • Duplicate UTF-8 tests (lines 54-62 and 106-114) + • Circular dependencies (use writeFile to test readFile) + • Over-granular countLines testing (8 tests for simple utility) + • Tests implementation details (JSON indentation spaces) +Gaps: • No permission error tests + • No disk space error tests + • No large file tests + • No concurrent access tests +Action: Remove 6 redundant tests, add 4 error handling tests +Savings: ~80 lines of code +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 🚫 Tests to Remove or Consolidate + +### High Priority Removals (No Value) + +**1. TypeScript Type Tests (Remove entirely)** +``` +Location: src/lib/plugins/code-highlight.test.ts:256-265 +Reason: Tests TypeScript interface at runtime - TypeScript already validates this +Impact: No loss of test coverage +``` + +**2. API Shape Tests (Remove entirely)** +``` +Location: src/lib/plugins/code-highlight.test.ts:18-31 +Reason: Tests plugin API structure - no runtime value +Impact: No loss of test coverage +``` + +**3. Duplicate UTF-8 Tests (Consolidate to 1)** +``` +Location: src/lib/utils/file-io.test.ts:54-62 and 106-114 +Reason: Both test identical UTF-8 functionality +Action: Keep one, remove the other +Savings: 8 lines +``` + +**4. Circular Dependency Tests (Refactor)** +``` +Location: src/lib/utils/file-io.test.ts (multiple) +Reason: Use writeFile to test readFile and vice versa +Action: Use test fixtures instead +Risk: Current approach could hide bugs in both functions +``` + +### Medium Priority Consolidations + +**5. Callouts Type Tests (Parameterize)** +``` +Location: src/lib/plugins/callouts.test.ts:49-145 +Before: 9 identical tests (96 lines) +After: 1 parameterized test (~15 lines) +Savings: 81 lines (84% reduction) +``` + +**6. Top-Level File Links (Parameterize)** +``` +Location: src/lib/plugins/links.test.ts:65-133 +Before: 7 similar tests (~40 lines) +After: 1 parameterized test (~8 lines) +Savings: 32 lines (80% reduction) +``` + +**7. External Link Protocol Tests (Parameterize)** +``` +Location: src/lib/plugins/links.test.ts (multiple) +Before: 5 tests for different protocols (http, https, ftp, mailto, data) +After: 1 parameterized test +Savings: ~20 lines +``` + +**8. Redundant Rate Limiter Tests (Remove)** +``` +Location: src/lib/server/rate-limiter.test.ts +Remove: • Lines 89-101: "concurrent requests" (duplicate of "block exceeding") + • Lines 117-122: "empty identifier" (unrealistic edge case) + • Lines 124-132: "100ms windows" (unrealistic edge case) +Savings: ~30 lines +``` + +**9. Over-Granular countLines Tests (Consolidate)** +``` +Location: src/lib/utils/file-io.test.ts:266-318 +Before: 8 tests for simple line counting utility +After: 3-4 meaningful tests (empty, single, multiple, edge cases) +Savings: ~30 lines +``` + +**Total Potential Savings: ~273 lines of test code** +**Total Potential Reduction: ~7.6% of test suite size** + +═══════════════════════════════════════════════════════════════════════════════ + +## 🔴 Critical Coverage Gaps (Priority Order) + +### Tier 1: High-Risk Untested Modules (Add immediately) + +**1. reference.ts (287 lines) - 0% coverage** +``` +Risk Level: 🔴 CRITICAL +Functionality: Symbol reference resolution and transformation +Why Critical: • Core feature for documentation linking + • Complex path resolution logic + • Error-prone symbol matching +Tests Needed: ~15-20 tests +Estimated LOC: ~300 lines +Priority: 1 +``` + +**2. screenshot-service.ts (514 lines) - 0% coverage** +``` +Risk Level: 🔴 CRITICAL +Functionality: Screenshot capture with Playwright +Why Critical: • External process management + • File I/O operations + • Resource cleanup critical + • Timeout handling +Tests Needed: ~20-25 tests (mostly integration tests) +Estimated LOC: ~400 lines +Priority: 2 +Note: Consider separating unit tests from integration tests +``` + +**3. circuit-breaker.ts (217 lines) - 0% coverage** +``` +Risk Level: 🔴 CRITICAL +Functionality: Fault tolerance and service protection +Why Critical: • Complex state management + • Time-based logic + • Critical for production resilience +Tests Needed: ~15-18 tests +Estimated LOC: ~250 lines +Priority: 3 +``` + +**4. image-processor.ts (318 lines) - 0% coverage** +``` +Risk Level: 🔴 CRITICAL +Functionality: Image optimization pipeline (Sharp) +Why Critical: • File system operations + • External library integration (Sharp) + • Performance-sensitive + • Error handling for corrupt images +Tests Needed: ~18-22 tests +Estimated LOC: ~350 lines +Priority: 4 +``` + +### Tier 2: Important Feature Gaps (Add within 2 weeks) + +**5. collapse.ts (190 lines) - 0% coverage** +``` +Risk Level: 🟡 HIGH +Functionality: Collapsible section transformation +Tests Needed: ~12-15 tests +Estimated LOC: ~200 lines +Priority: 5 +``` + +**6. toc.ts (184 lines) - 0% coverage** +``` +Risk Level: 🟡 HIGH +Functionality: Table of contents generation +Tests Needed: ~12-15 tests +Estimated LOC: ~180 lines +Priority: 6 +``` + +**7. tabs.ts (135 lines) - 0% coverage** +``` +Risk Level: 🟡 HIGH +Functionality: Tab container transformation +Tests Needed: ~10-12 tests +Estimated LOC: ~150 lines +Priority: 7 +``` + +**8. mermaid.ts (33 lines) - 0% coverage** +``` +Risk Level: 🟡 MEDIUM +Functionality: Mermaid diagram integration +Tests Needed: ~6-8 tests +Estimated LOC: ~80 lines +Priority: 8 +``` + +**9. filetree.ts (45 lines) - 0% coverage** +``` +Risk Level: 🟡 MEDIUM +Functionality: File tree visualization +Tests Needed: ~6-8 tests +Estimated LOC: ~70 lines +Priority: 9 +``` + +### Tier 3: Utility and Support Functions + +**10. symbol-generation.ts (851 lines) - 0% coverage** +``` +Risk Level: 🟡 HIGH (due to size) +Functionality: Symbol generation from source code +Tests Needed: ~25-30 tests +Estimated LOC: ~500 lines +Priority: 10 +``` + +**11. tree-parser.ts (208 lines) - 0% coverage** +``` +Risk Level: 🟢 MEDIUM +Functionality: AST tree parsing utilities +Tests Needed: ~12-15 tests +Estimated LOC: ~180 lines +Priority: 11 +``` + +**12. navigation-builder.ts + navigation-scanner.ts (374 lines combined) - 0% coverage** +``` +Risk Level: 🟢 MEDIUM +Functionality: Navigation structure building +Tests Needed: ~15-18 tests combined +Estimated LOC: ~250 lines +Priority: 12 +``` + +**13. search.ts (163 lines) - 0% coverage** +``` +Risk Level: 🟢 MEDIUM +Functionality: Search implementation +Tests Needed: ~10-12 tests +Estimated LOC: ~150 lines +Priority: 13 +Note: search-index.ts has good coverage, but actual search.ts doesn't +``` + +### Tier 4: Low-Risk Utilities (Add as time permits) + +``` +• cli-executor.ts (80 lines) - 0% coverage +• logger.ts (64 lines) - 0% coverage +• highlighter.ts (74 lines) - 0% coverage +• base64.ts (95 lines) - 12% coverage (needs more) +• openapi-formatter.ts (363 lines) - 0% coverage +• symbol-renderer.ts (277 lines) - 0% coverage +• version.ts (20 lines) - 0% coverage +• html.ts (87 lines) - 42.85% coverage (needs more) +• date.ts - 100% coverage ✅ +``` + +**Estimated Total Effort:** +``` +New Tests Needed: ~220-270 tests +New Test LOC: ~4,000-5,000 lines +Time Estimate: 3-4 weeks (1 developer) +Coverage Increase: 23% → 60-70% +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 🧪 Missing Test Types + +### 1. Integration Tests ❌ +**Status:** Not present +``` +Current: Only unit tests exist +Need: • Plugin pipeline integration + • File I/O + Markdown processing flow + • Symbol resolution + generation flow + • Image optimization pipeline + • Search indexing + querying flow + +Example Integration Test: + test('full markdown processing pipeline', async () => { + const markdown = '# Title\n\n[!NOTE] Callout\n\n```js\ncode\n```'; + const result = await processMarkdown(markdown, allPlugins); + expect(result.html).toMatchSnapshot(); + expect(result.metadata).toEqual({ ... }); + }); + +Estimated: 15-20 integration tests needed +LOC: ~800-1000 lines +Priority: HIGH +``` + +### 2. End-to-End Tests ❌ +**Status:** Not present +``` +Current: No E2E tests +Need: • Full documentation site generation + • Search functionality E2E + • Navigation generation E2E + • Image optimization E2E + +Tools: Playwright (already in devDependencies ✅) +Example: Generate full site → verify pages → test navigation → test search + +Estimated: 10-15 E2E tests needed +LOC: ~600-800 lines +Priority: MEDIUM +Note: Playwright already installed, just needs test setup +``` + +### 3. Performance Tests ❌ +**Status:** Not present +``` +Current: No performance benchmarks +Need: • Large file processing (10MB+ markdown files) + • Thousands of symbols performance + • Search index with 10k+ documents + • Image batch processing (100+ images) + +Example: + test('should process large markdown file within 5s', async () => { + const largeMarkdown = generateMarkdown(10000); // 10k lines + const start = Date.now(); + await processMarkdown(largeMarkdown); + expect(Date.now() - start).toBeLessThan(5000); + }); + +Estimated: 8-12 performance tests +LOC: ~300-400 lines +Priority: LOW (add after coverage is above 60%) +``` + +### 4. Visual Regression Tests ❌ +**Status:** Not present +``` +Current: No visual testing +Need: • Callout rendering consistency + • Code block styling + • Table rendering + • Symbol reference rendering + +Tools: Percy, Chromatic, or Playwright screenshots +Priority: LOW (nice-to-have) +``` + +### 5. Component Tests ❌ +**Status:** Not present +``` +Current: No component tests (all Svelte components untested) +Files: • All files in lib/components/ have 0% coverage +Need: • Svelte component unit tests + • @testing-library/svelte already installed ✅ + +Priority: MEDIUM (if components contain complex logic) +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 🎯 Test Best Practices Issues + +### ❌ Current Issues + +**1. Testing Implementation Details** +``` +Location: Multiple files +Examples: • Checking for specific CSS class names + • Testing JSON indentation (2 spaces vs 4) + • Testing exact timestamp formats + • Testing blank lines in output + +Problem: Tests break when implementation changes, even if behavior is correct +Fix: Test behavior and outcomes, not internal implementation +``` + +**2. Weak Assertions** +``` +Location: Multiple files +Examples: • expect(result).toBeDefined() + • expect(result).toContain('shiki') + • expect(count).toBeGreaterThan(0) + +Problem: Assertions are too vague to catch real bugs +Fix: Use specific assertions: + • expect(result).toEqual({ specific: 'value' }) + • expect(html).toMatch(/
/)
+           • expect(count).toBe(expectedCount)
+```
+
+**3. Magic Numbers and Strings**
+```
+Location:  All test files
+Examples:  • 60000 (milliseconds - what is this?)
+           • 10 (rate limit - why 10?)
+           • '/docs/' (hardcoded path)
+           • 'shiki' (what if class name changes?)
+
+Fix:       Extract to named constants:
+           const ONE_MINUTE = 60_000;
+           const RATE_LIMIT = 10;
+           const DOCS_PATH_PREFIX = '/docs/';
+```
+
+**4. Circular Test Dependencies**
+```
+Location:  src/lib/utils/file-io.test.ts
+Example:   Using writeFile() to test readFile()
+           Using readFile() to test writeFile()
+
+Problem:   If both functions have the same bug, tests will pass
+Fix:       Use independent test fixtures or known good data
+```
+
+**5. No Test Helpers or Fixtures**
+```
+Current:   All mock data is inline in test files
+Problem:   • Code duplication
+           • Hard to maintain
+           • Test data scattered everywhere
+
+Fix:       Create test utilities:
+           // tests/helpers/fixtures.ts
+           export const mockMarkdownTree = () => ({ /* ... */ });
+           export const mockFrontmatter = () => ({ /* ... */ });
+```
+
+**6. Inconsistent Test Organization**
+```
+Current:   Tests co-located with source files (*.test.ts)
+Pros:      ✅ Easy to find related tests
+           ✅ Encourages testing
+Cons:      ❌ Harder to run only tests
+           ❌ Clutters source directories
+
+Recommendation: Keep current approach (co-location is good for this project size)
+```
+
+**7. No Test Coverage Enforcement on CI**
+```
+Current:   Coverage threshold set to 40% in config
+           But no CI to enforce it ❌
+
+Problem:   Coverage can drop without anyone noticing
+Fix:       Add CI workflow with coverage checks (see recommendations)
+```
+
+### ✅ Things Done Well
+
+**1. Good Error Handling Tests**
+```
+Examples:  • Malformed YAML gracefully handled
+           • Invalid language in code blocks
+           • Missing files in file-io
+           • XSS attack prevention in sanitize
+
+Keep:      Continue this pattern for all new tests
+```
+
+**2. Security Testing**
+```
+Tests:     • XSS prevention (sanitize.test.ts)
+           • HTML injection
+           • Protocol injection (javascript:, data:)
+           • SVG attack vectors
+
+Excellent: This is exemplary - expand to other modules
+```
+
+**3. Good Use of Test Lifecycle Hooks**
+```
+Pattern:   beforeEach() for setup
+           afterEach() for cleanup
+           Temporary directories properly cleaned up
+
+Keep:      Continue this pattern consistently
+```
+
+**4. Good Test Naming**
+```
+Pattern:   "should [expected behavior]" convention
+Examples:  • "should transform NOTE callout"
+           • "should handle malformed YAML gracefully"
+           • "should escape special characters"
+
+Keep:      Maintain this clear, descriptive naming
+```
+
+═══════════════════════════════════════════════════════════════════════════════
+
+## 🚀 CI/CD Recommendations
+
+### Current State: No CI/CD ❌
+
+**Missing:**
+• No GitHub Actions workflows
+• No automated test runs on PR/push
+• No coverage reporting
+• No lint checks
+• No type checking in CI
+
+### Recommended GitHub Actions Workflow
+
+**Create:** `.github/workflows/ci.yml`
+
+```yaml
+name: CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  test:
+    name: Test & Coverage
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: pnpm/action-setup@v2
+        with:
+          version: 10
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'pnpm'
+
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile
+
+      - name: Run tests with coverage
+        run: pnpm run test:coverage
+
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v4
+        with:
+          file: ./coverage/lcov.info
+          fail_ci_if_error: true
+
+      - name: Check coverage threshold
+        run: |
+          if [ $(jq '.total.lines.pct' coverage/coverage-summary.json | cut -d. -f1) -lt 40 ]; then
+            echo "Coverage is below 40% threshold"
+            exit 1
+          fi
+
+  lint:
+    name: Lint
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pnpm/action-setup@v2
+        with:
+          version: 10
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'pnpm'
+
+      - run: pnpm install --frozen-lockfile
+      - run: pnpm run lint
+
+  typecheck:
+    name: Type Check
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pnpm/action-setup@v2
+        with:
+          version: 10
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          cache: 'pnpm'
+
+      - run: pnpm install --frozen-lockfile
+      - run: pnpm exec tsc --noEmit
+```
+
+**Additional Workflows:**
+
+```yaml
+# .github/workflows/nightly.yml
+# Run comprehensive tests nightly (including slow E2E tests)
+
+name: Nightly Tests
+on:
+  schedule:
+    - cron: '0 0 * * *'  # Midnight UTC
+  workflow_dispatch:
+
+jobs:
+  e2e:
+    name: E2E Tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pnpm/action-setup@v2
+      - uses: actions/setup-node@v4
+      - run: pnpm install --frozen-lockfile
+      - run: pnpm exec playwright install --with-deps
+      - run: pnpm run test:e2e  # When E2E tests exist
+
+  performance:
+    name: Performance Tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pnpm/action-setup@v2
+      - uses: actions/setup-node@v4
+      - run: pnpm install --frozen-lockfile
+      - run: pnpm run test:perf  # When perf tests exist
+```
+
+═══════════════════════════════════════════════════════════════════════════════
+
+## 🔧 Test Configuration Optimization
+
+### Current Vitest Config Analysis
+
+**File:** `vitest.config.ts`
+
+**Current Settings:**
+```typescript
+{
+  globals: true,
+  environment: 'happy-dom',
+  include: ['src/**/*.{test,spec}.{js,ts}'],
+  coverage: {
+    provider: 'v8',
+    reporter: ['text', 'json', 'html', 'lcov'],
+    include: ['src/lib/**/*.ts'],
+    exclude: [/* standard exclusions */],
+    thresholds: {
+      lines: 40,
+      functions: 40,
+      branches: 40,
+      statements: 40
+    },
+    all: true
+  }
+}
+```
+
+**✅ Good Configurations:**
+• Environment: happy-dom (lightweight, fast) ✅
+• Coverage provider: v8 (fast, accurate) ✅
+• Multiple reporters including lcov for CI ✅
+• Threshold enforcement ✅
+• `all: true` (measures all files, not just tested ones) ✅
+
+**🔧 Recommended Improvements:**
+
+```typescript
+// vitest.config.ts
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'happy-dom',
+    include: ['src/**/*.{test,spec}.{js,ts}'],
+
+    // NEW: Separate test types
+    exclude: [
+      '**/node_modules/**',
+      '**/dist/**',
+      '**/.{idea,git,cache,output,temp}/**',
+      '**/{e2e,perf}/**'  // Exclude from default run
+    ],
+
+    // NEW: Faster test runs
+    pool: 'threads',
+    poolOptions: {
+      threads: {
+        singleThread: false,
+        useAtomics: true
+      }
+    },
+
+    // NEW: Better error messages
+    reporters: ['default', 'html'],
+    outputFile: {
+      html: './coverage/test-report.html'
+    },
+
+    // NEW: Fail fast for CI
+    bail: process.env.CI ? 1 : undefined,
+
+    // NEW: Timeouts
+    testTimeout: 10000,
+    hookTimeout: 10000,
+
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html', 'lcov'],
+      include: ['src/lib/**/*.ts'],
+      exclude: [
+        '**/node_modules/**',
+        '**/dist/**',
+        '**/*.d.ts',
+        '**/*.config.*',
+        '**/types.ts',
+        '**/*.test.ts',
+        '**/*.spec.ts',
+        '**/*.svelte'  // Keep for now, may change later
+      ],
+
+      // CHANGED: Increase thresholds gradually
+      thresholds: {
+        lines: 40,      // Current: 23.38% → Target: 40%
+        functions: 40,  // Current: 29.85% → Target: 40%
+        branches: 40,   // Current: 18.11% → Target: 40%
+        statements: 40  // Current: 23.33% → Target: 40%
+
+        // Phase 2 targets (after adding high-risk tests):
+        // lines: 60,
+        // functions: 60,
+        // branches: 50,
+        // statements: 60
+      },
+
+      all: true,
+
+      // NEW: Per-directory thresholds (optional)
+      perFile: true,
+      watermarks: {
+        lines: [50, 80],
+        functions: [50, 80],
+        branches: [50, 80],
+        statements: [50, 80]
+      }
+    }
+  }
+});
+```
+
+**Add New Test Scripts to package.json:**
+
+```json
+{
+  "scripts": {
+    "test": "vitest",
+    "test:ui": "vitest --ui",
+    "test:run": "vitest run",
+    "test:coverage": "vitest run --coverage",
+    "test:watch": "vitest watch",
+
+    // NEW: Separate test types
+    "test:unit": "vitest run --config vitest.config.ts",
+    "test:integration": "vitest run --config vitest.integration.config.ts",
+    "test:e2e": "playwright test",
+    "test:perf": "vitest run --config vitest.perf.config.ts",
+
+    // NEW: CI-specific
+    "test:ci": "vitest run --coverage --reporter=default --reporter=json",
+
+    // NEW: Coverage helpers
+    "coverage": "vitest run --coverage",
+    "coverage:open": "open coverage/index.html",
+
+    // NEW: Watch specific files
+    "test:related": "vitest related"
+  }
+}
+```
+
+═══════════════════════════════════════════════════════════════════════════════
+
+## 📊 Actionable Improvement Roadmap
+
+### Phase 1: Quick Wins (1-2 days)
+
+**Goal:** Reduce technical debt, improve maintainability
+
+```
+✅ Tasks:
+  ⬜ Refactor callouts.test.ts to use test.each() (1 hour)
+     Impact: -81 lines, better maintainability
+
+  ⬜ Refactor links.test.ts to parameterize tests (1 hour)
+     Impact: -52 lines
+
+  ⬜ Remove useless tests (TypeScript types, API shapes) (30 min)
+     Impact: -20 lines, cleaner test suite
+
+  ⬜ Add GitHub Actions CI workflow (2 hours)
+     Impact: Automated testing on every PR
+
+  ⬜ Set up Codecov or similar for coverage tracking (1 hour)
+     Impact: Visualize coverage trends
+
+Total Time: 1-2 days
+Coverage Change: 0% (just cleanup)
+Maintainability: ↑↑↑ High improvement
+```
+
+### Phase 2: Critical Coverage (1-2 weeks)
+
+**Goal:** Test high-risk untested modules
+
+```
+✅ Tasks:
+  ⬜ Add tests for reference.ts (2 days)
+     • 15-20 tests for symbol reference resolution
+     • Edge cases for ambiguous references
+     • Error handling tests
+     Impact: +300 LOC, +~5% coverage
+
+  ⬜ Add tests for circuit-breaker.ts (1 day)
+     • State transition tests
+     • Timeout handling
+     • Failure threshold tests
+     Impact: +250 LOC, +~3% coverage
+
+  ⬜ Add tests for collapse.ts + toc.ts + tabs.ts (2 days)
+     • Plugin transformation tests
+     • Edge cases for each
+     Impact: +530 LOC, +~6% coverage
+
+  ⬜ Add tests for mermaid.ts + filetree.ts (1 day)
+     • Basic transformation tests
+     • Error handling
+     Impact: +150 LOC, +~1% coverage
+
+  ⬜ Add integration tests for plugins (2 days)
+     • Full pipeline integration
+     • Plugin interaction tests
+     Impact: +800 LOC, +~2% coverage
+
+Total Time: 1-2 weeks
+Coverage Change: 23% → ~40% ✅
+Risk Reduction: ↑↑↑ High
+```
+
+### Phase 3: Comprehensive Coverage (2-3 weeks)
+
+**Goal:** Reach 60%+ coverage
+
+```
+✅ Tasks:
+  ⬜ Add tests for screenshot-service.ts (3 days)
+     • Integration tests with Playwright
+     • Resource cleanup tests
+     • Timeout handling
+     Impact: +400 LOC, +~6% coverage
+     Note: Requires Playwright setup
+
+  ⬜ Add tests for image-processor.ts (2 days)
+     • Sharp integration tests
+     • Error handling (corrupt images)
+     • Performance tests
+     Impact: +350 LOC, +~4% coverage
+
+  ⬜ Add tests for symbol-generation.ts (3 days)
+     • Source code parsing tests
+     • Symbol extraction tests
+     • Edge cases for complex types
+     Impact: +500 LOC, +~10% coverage
+
+  ⬜ Add tests for navigation modules (2 days)
+     • navigation-builder.ts
+     • navigation-scanner.ts
+     • Integration tests
+     Impact: +250 LOC, +~4% coverage
+
+  ⬜ Add tests for search.ts (2 days)
+     • Search algorithm tests
+     • Ranking tests
+     • Performance tests
+     Impact: +150 LOC, +~2% coverage
+
+Total Time: 2-3 weeks
+Coverage Change: 40% → ~66% ✅
+Risk Reduction: ↑↑↑ Very High
+```
+
+### Phase 4: Advanced Testing (2-3 weeks)
+
+**Goal:** E2E, performance, visual regression
+
+```
+✅ Tasks:
+  ⬜ Set up Playwright E2E tests (3 days)
+     • Full site generation E2E
+     • Navigation E2E
+     • Search E2E
+     Impact: +800 LOC E2E tests
+
+  ⬜ Add performance benchmarks (2 days)
+     • Large file processing
+     • Batch image optimization
+     • Search indexing performance
+     Impact: +400 LOC perf tests
+
+  ⬜ Add visual regression tests (2 days)
+     • Snapshot tests for rendering
+     • CSS regression detection
+     Impact: +300 LOC visual tests
+
+  ⬜ Add component tests (1 week)
+     • Test all Svelte components
+     • Interaction tests
+     Impact: +600 LOC, +~3% coverage
+
+Total Time: 2-3 weeks
+Coverage Change: 66% → ~70%+
+Quality: ↑↑↑ Production-ready
+```
+
+### Total Effort Summary
+
+```
+Phase 1 (Quick Wins):        1-2 days
+Phase 2 (Critical):          1-2 weeks
+Phase 3 (Comprehensive):     2-3 weeks
+Phase 4 (Advanced):          2-3 weeks
+───────────────────────────────────────
+Total:                       6-9 weeks (1 developer)
+
+Coverage Progression:
+  Start:   23.38%
+  Phase 1: 23%      (cleanup, no change)
+  Phase 2: ~40%     (+16.62%) ✅ Threshold met
+  Phase 3: ~66%     (+26%)
+  Phase 4: ~70%+    (+4%)
+
+Risk Reduction:
+  Phase 1: ↑ Low (maintainability)
+  Phase 2: ↑↑↑ Very High (critical modules)
+  Phase 3: ↑↑ High (comprehensive coverage)
+  Phase 4: ↑ Medium (quality of life)
+```
+
+═══════════════════════════════════════════════════════════════════════════════
+
+## 💡 Specific Code Examples
+
+### Example 1: Refactoring Callouts Tests
+
+**Before (96 lines):**
+```typescript
+test('should transform NOTE callout', () => {
+  const tree = createTree('[!NOTE] Note content');
+  const result = transformCallout(tree);
+  expect(result.html).toContain('callout-blue');
+  expect(result.html).toContain('Note');
+});
+
+test('should transform TIP callout', () => {
+  const tree = createTree('[!TIP] Tip content');
+  const result = transformCallout(tree);
+  expect(result.html).toContain('callout-green');
+  expect(result.html).toContain('Tip');
+});
+
+// ... 7 more identical tests
+```
+
+**After (15 lines):**
+```typescript
+const calloutTypes = [
+  { type: 'NOTE', class: 'blue', title: 'Note', role: 'note' },
+  { type: 'TIP', class: 'green', title: 'Tip', role: 'note' },
+  { type: 'WARNING', class: 'yellow', title: 'Warning', role: 'alert' },
+  { type: 'IMPORTANT', class: 'purple', title: 'Important', role: 'alert' },
+  { type: 'CAUTION', class: 'red', title: 'Caution', role: 'alert' },
+  { type: 'SUCCESS', class: 'success', title: 'Success', role: 'status' },
+  { type: 'DANGER', class: 'danger', title: 'Danger', role: 'alert' },
+  { type: 'INFO', class: 'info', title: 'Info', role: 'note' },
+  { type: 'QUESTION', class: 'question', title: 'Question', role: 'note' }
+];
+
+test.each(calloutTypes)(
+  'should transform $type callout',
+  ({ type, class: cssClass, title, role }) => {
+    const tree = createTree(`[!${type}] ${type} content`);
+    const result = transformCallout(tree);
+
+    expect(result.html).toContain(`callout-${cssClass}`);
+    expect(result.html).toContain(title);
+    expect(result.html).toContain(`role="${role}"`);
+    expect(result.html).toContain(`aria-label="${title}"`);
+  }
+);
+```
+
+**Benefits:**
+• 81 lines removed (84% reduction)
+• Easier to add new callout types (add one line to array)
+• Single place to update test logic
+• Clear data-driven approach
+
+### Example 2: Adding Error Handling Tests
+
+**Missing (should add):**
+```typescript
+// src/lib/utils/file-io.test.ts
+
+describe('error handling', () => {
+  test('should throw on permission denied', async () => {
+    const readOnlyFile = path.join(tmpDir, 'readonly.txt');
+    await writeFile(readOnlyFile, 'content');
+    fs.chmodSync(readOnlyFile, 0o000); // Remove all permissions
+
+    await expect(readFile(readOnlyFile)).rejects.toThrow(/permission denied/i);
+
+    // Cleanup
+    fs.chmodSync(readOnlyFile, 0o644);
+  });
+
+  test('should throw on disk space exhausted', async () => {
+    // Mock fs.writeFile to simulate ENOSPC error
+    const mockWrite = vi.spyOn(fs.promises, 'writeFile');
+    mockWrite.mockRejectedValueOnce(
+      Object.assign(new Error('ENOSPC: no space left on device'), {
+        code: 'ENOSPC'
+      })
+    );
+
+    const file = path.join(tmpDir, 'test.txt');
+    await expect(writeFile(file, 'content')).rejects.toThrow(/no space left/i);
+
+    mockWrite.mockRestore();
+  });
+
+  test('should handle very large files', async () => {
+    const largeContent = 'x'.repeat(10 * 1024 * 1024); // 10MB
+    const file = path.join(tmpDir, 'large.txt');
+
+    await writeFile(file, largeContent);
+    const result = await readFile(file);
+
+    expect(result.length).toBe(largeContent.length);
+  }, 30000); // 30s timeout for large file
+});
+```
+
+### Example 3: Integration Test Example
+
+**Should add:**
+```typescript
+// src/lib/integration/markdown-pipeline.test.ts
+
+import { describe, it, expect } from 'vitest';
+import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import remarkDirective from 'remark-directive';
+import { calloutsPlugin } from '../plugins/callouts';
+import { codeHighlightPlugin } from '../plugins/code-highlight';
+import { linksPlugin } from '../plugins/links';
+import { katexPlugin } from '../plugins/katex';
+
+describe('markdown processing pipeline', () => {
+  const processor = unified()
+    .use(remarkParse)
+    .use(remarkDirective)
+    .use(calloutsPlugin)
+    .use(codeHighlightPlugin)
+    .use(linksPlugin)
+    .use(katexPlugin);
+
+  it('should process complex markdown with all plugins', async () => {
+    const markdown = `
+# Documentation
+
+[!NOTE] This is a note callout
+
+Check out [guide.md](./guide.md) for more info.
+
+\`\`\`typescript title="example.ts" showLineNumbers
+function hello() {
+  console.log('Hello World');
+}
+\`\`\`
+
+Inline math: $E = mc^2$
+
+Display math:
+
+$$
+\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
+$$
+    `.trim();
+
+    const result = await processor.process(markdown);
+    const html = result.toString();
+
+    // Verify all plugins ran
+    expect(html).toContain('callout-blue');        // Callouts
+    expect(html).toContain('/docs/guide');         // Links
+    expect(html).toContain('class="shiki"');       // Code highlight
+    expect(html).toContain('code-block-title');    // Code metadata
+    expect(html).toContain('katex');               // Math rendering
+
+    // Verify no plugin corrupted another's output
+    expect(html).not.toContain('[!NOTE]');         // Callout processed
+    expect(html).not.toContain('./guide.md');      // Link processed
+    expect(html).not.toContain('```typescript');   // Code fence processed
+    expect(html).not.toContain('$E = mc^2$');      // Inline math processed
+  });
+
+  it('should handle plugin errors gracefully', async () => {
+    const markdown = `
+[!NOTE] Valid callout
+
+\`\`\`invalidlang123
+code
+\`\`\`
+
+$\\invalid{latex}$
+    `.trim();
+
+    // Should not throw, but handle errors gracefully
+    await expect(processor.process(markdown)).resolves.toBeDefined();
+  });
+
+  it('should maintain correct order of transformations', async () => {
+    // Links should be processed before callouts
+    // So callouts can contain links
+    const markdown = `
+[!NOTE] Check [guide.md](./guide.md)
+    `.trim();
+
+    const result = await processor.process(markdown);
+    const html = result.toString();
+
+    expect(html).toContain('callout');
+    expect(html).toContain('/docs/guide');
+  });
+});
+```
+
+═══════════════════════════════════════════════════════════════════════════════
+
+## 🎓 Testing Best Practices Guide
+
+### Test Naming Convention
+
+**❌ Bad:**
+```typescript
+test('test1', () => { ... });
+test('file-io', () => { ... });
+test('check read', () => { ... });
+```
+
+**✅ Good:**
+```typescript
+test('should read UTF-8 file with special characters', () => { ... });
+test('should throw error when file does not exist', () => { ... });
+test('should handle empty files gracefully', () => { ... });
+```
+
+**Pattern:** `should [expected behavior] [given context]`
+
+### Assertion Quality
+
+**❌ Weak Assertions:**
+```typescript
+expect(result).toBeDefined();
+expect(html).toContain('class');
+expect(count).toBeGreaterThan(0);
+```
+
+**✅ Strong Assertions:**
+```typescript
+expect(result).toEqual({ title: 'Title', content: 'Content' });
+expect(html).toMatch(/
/); +expect(count).toBe(5); +``` + +### Test Independence + +**❌ Dependent Tests:** +```typescript +let sharedState; + +test('test 1', () => { + sharedState = someFunction(); +}); + +test('test 2', () => { + // Depends on test 1 running first + expect(sharedState).toBe(expected); +}); +``` + +**✅ Independent Tests:** +```typescript +test('test 1', () => { + const state = someFunction(); + expect(state).toBe(expected); +}); + +test('test 2', () => { + const state = someFunction(); + expect(state).toBe(expected); +}); +``` + +### Avoid Magic Values + +**❌ Magic Values:** +```typescript +test('rate limiter', () => { + for (let i = 0; i < 10; i++) { + checkRateLimit('user'); + } + vi.advanceTimersByTime(60000); +}); +``` + +**✅ Named Constants:** +```typescript +const RATE_LIMIT = 10; +const ONE_MINUTE_MS = 60_000; + +test('rate limiter should allow up to limit', () => { + for (let i = 0; i < RATE_LIMIT; i++) { + checkRateLimit('user'); + } + vi.advanceTimersByTime(ONE_MINUTE_MS); +}); +``` + +### Test Helpers and Fixtures + +**❌ Inline Mock Data Everywhere:** +```typescript +test('test 1', () => { + const tree = { type: 'root', children: [{ type: 'paragraph', ... }] }; + // ... +}); + +test('test 2', () => { + const tree = { type: 'root', children: [{ type: 'paragraph', ... }] }; + // ... +}); +``` + +**✅ Shared Test Helpers:** +```typescript +// tests/helpers/fixtures.ts +export function createMockTree(content: string) { + return { type: 'root', children: [{ type: 'paragraph', ... }] }; +} + +// test file +import { createMockTree } from '../helpers/fixtures'; + +test('test 1', () => { + const tree = createMockTree('content'); + // ... +}); +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 📈 Coverage Improvement Tracking + +### Recommended Coverage Targets + +``` +Phase Timeline Lines Functions Branches Statements Status +─────── ────────── ─────── ───────── ──────── ────────── ────────── +Current Today 23.38% 29.85% 18.11% 23.33% 🔴 Below +Phase 1 Week 1 23% 30% 18% 23% 🔄 Cleanup +Phase 2 Week 3 40% 45% 35% 40% ✅ Threshold +Phase 3 Week 6 60% 65% 50% 60% ✅ Good +Phase 4 Week 9 70%+ 75%+ 60%+ 70%+ ✅ Excellent +``` + +### Module Priority for Coverage + +**Tier 1 - Critical (Add First):** +``` +Module Current Target Priority +───────────────────── ─────── ────── ──────── +reference.ts 0% 90% ⭐⭐⭐⭐⭐ +circuit-breaker.ts 0% 85% ⭐⭐⭐⭐⭐ +screenshot-service.ts 0% 60% ⭐⭐⭐⭐⭐ +image-processor.ts 0% 80% ⭐⭐⭐⭐⭐ +``` + +**Tier 2 - Important (Add Second):** +``` +Module Current Target Priority +───────────────────── ─────── ────── ──────── +collapse.ts 0% 85% ⭐⭐⭐⭐ +toc.ts 0% 90% ⭐⭐⭐⭐ +tabs.ts 0% 85% ⭐⭐⭐⭐ +symbol-generation.ts 0% 75% ⭐⭐⭐⭐ +``` + +**Tier 3 - Utilities (Add Third):** +``` +Module Current Target Priority +───────────────────── ─────── ────── ──────── +navigation-builder.ts 0% 80% ⭐⭐⭐ +tree-parser.ts 0% 85% ⭐⭐⭐ +search.ts 0% 85% ⭐⭐⭐ +base64.ts 12% 70% ⭐⭐⭐ +``` + +### Weekly Progress Tracking Template + +``` +Week of: [DATE] + +Coverage: + Lines: [%] (target: [%]) [↑↓] [delta] + Functions: [%] (target: [%]) [↑↓] [delta] + Branches: [%] (target: [%]) [↑↓] [delta] + Statements: [%] (target: [%]) [↑↓] [delta] + +Tests Added: [N] tests ([M] LOC) +Modules Covered: + • [module] - [N] tests - [%] coverage + • [module] - [N] tests - [%] coverage + +Issues Found: + • [issue description] + • [issue description] + +Next Week Goals: + • [goal] + • [goal] +``` + +═══════════════════════════════════════════════════════════════════════════════ + +## 🚀 Conclusion + +### Summary of Findings + +**Strengths:** +✅ Solid test foundation where tests exist +✅ Good edge case coverage in tested modules +✅ Excellent security testing (XSS, sanitization) +✅ Good test organization and naming conventions +✅ Proper cleanup and lifecycle management + +**Critical Issues:** +❌ Coverage well below 40% threshold (currently 23.38%) +❌ 57% of source files completely untested +❌ 96 lines of duplicate test code identified +❌ No CI/CD pipeline for automated testing +❌ Missing integration and E2E tests + +**Risk Level:** 🟡 MEDIUM-HIGH +``` +High-risk untested modules exist (reference.ts, screenshot-service.ts, +circuit-breaker.ts, image-processor.ts) but core functionality has tests. +``` + +### Immediate Action Items (This Week) + +1. **Refactor duplicate tests** (callouts, links) - 2 hours +2. **Set up GitHub Actions CI** - 2 hours +3. **Remove useless tests** (TypeScript types) - 30 minutes +4. **Start Phase 2: Add reference.ts tests** - Ongoing + +### Long-term Roadmap (9 weeks) + +``` +Weeks 1-2: Quick wins + cleanup +Weeks 3-4: Critical coverage (reach 40%) +Weeks 5-7: Comprehensive coverage (reach 60-70%) +Weeks 8-9: Advanced testing (E2E, performance) +``` + +### Success Metrics + +**Phase 2 Complete (Week 4):** +• Coverage ≥ 40% ✅ +• All Tier 1 modules have tests +• CI/CD running on all PRs + +**Phase 3 Complete (Week 7):** +• Coverage ≥ 60% +• All high and medium risk modules tested +• Integration tests in place + +**Phase 4 Complete (Week 9):** +• Coverage ≥ 70% +• E2E tests running nightly +• Performance benchmarks established +• Visual regression testing (optional) + +═══════════════════════════════════════════════════════════════════════════════ + +## 📚 Additional Resources + +**Vitest Documentation:** +• https://vitest.dev/guide/ +• https://vitest.dev/guide/coverage.html +• https://vitest.dev/api/ + +**Testing Best Practices:** +• Kent C. Dodds - Testing Library principles +• Martin Fowler - Test Pyramid +• Google Testing Blog - Effective testing strategies + +**Tools to Consider:** +• Codecov - Coverage tracking and visualization +• Playwright - E2E testing (already installed ✅) +• Percy/Chromatic - Visual regression testing +• Benchmarkjs - Performance benchmarking + +═══════════════════════════════════════════════════════════════════════════════ + +**Report Generated:** 2025-11-11 +**docs-engine version:** 2.0.0 +**Vitest version:** 4.0.7 + +═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/lib/plugins/callouts.test.ts b/src/lib/plugins/callouts.test.ts index 75b749a..127ee1c 100644 --- a/src/lib/plugins/callouts.test.ts +++ b/src/lib/plugins/callouts.test.ts @@ -46,55 +46,31 @@ describe('callouts plugin', () => { return tree; }; - it('should transform NOTE callout', () => { - const blockquote = createBlockquote('NOTE'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--blue'); - expect(html).toContain('Note'); - }); - - it('should transform TIP callout', () => { - const blockquote = createBlockquote('TIP'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--green'); - expect(html).toContain('Tip'); - }); - - it('should transform WARNING callout', () => { - const blockquote = createBlockquote('WARNING'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--yellow'); - expect(html).toContain('Warning'); - }); - - it('should transform IMPORTANT callout', () => { - const blockquote = createBlockquote('IMPORTANT'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--purple'); - expect(html).toContain('Important'); - }); - - it('should transform CAUTION callout', () => { - const blockquote = createBlockquote('CAUTION'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--red'); - expect(html).toContain('Caution'); - }); + // Test all callout types with parameterized tests + const calloutTypes = [ + { type: 'NOTE', cssClass: 'blue', title: 'Note' }, + { type: 'TIP', cssClass: 'green', title: 'Tip' }, + { type: 'WARNING', cssClass: 'yellow', title: 'Warning' }, + { type: 'IMPORTANT', cssClass: 'purple', title: 'Important' }, + { type: 'CAUTION', cssClass: 'red', title: 'Caution' }, + { type: 'SUCCESS', cssClass: 'success', title: 'Success' }, + { type: 'DANGER', cssClass: 'danger', title: 'Danger' }, + { type: 'INFO', cssClass: 'info', title: 'Info' }, + { type: 'QUESTION', cssClass: 'question', title: 'Question' }, + ]; + + it.each(calloutTypes)( + 'should transform $type callout with correct class and title', + ({ type, cssClass, title }) => { + const blockquote = createBlockquote(type); + const tree = processTree(blockquote); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const html = (tree.children[0] as any).value; + expect(html).toContain(`md-callout--${cssClass}`); + expect(html).toContain(title); + } + ); it('should not transform regular blockquotes', () => { const blockquote = createBlockquote(); @@ -104,46 +80,6 @@ describe('callouts plugin', () => { expect(tree.children[0].type).toBe('blockquote'); }); - it('should handle SUCCESS callout', () => { - const blockquote = createBlockquote('SUCCESS'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--success'); - expect(html).toContain('Success'); - }); - - it('should handle DANGER callout', () => { - const blockquote = createBlockquote('DANGER'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--danger'); - expect(html).toContain('Danger'); - }); - - it('should handle INFO callout', () => { - const blockquote = createBlockquote('INFO'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--info'); - expect(html).toContain('Info'); - }); - - it('should handle QUESTION callout', () => { - const blockquote = createBlockquote('QUESTION'); - const tree = processTree(blockquote); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = (tree.children[0] as any).value; - expect(html).toContain('md-callout--question'); - expect(html).toContain('Question'); - }); - it('should preserve ARIA attributes', () => { const blockquote = createBlockquote('NOTE'); const tree = processTree(blockquote); diff --git a/src/lib/plugins/collapse.test.ts b/src/lib/plugins/collapse.test.ts new file mode 100644 index 0000000..7bc518b --- /dev/null +++ b/src/lib/plugins/collapse.test.ts @@ -0,0 +1,648 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect } from 'vitest'; +import { collapsePlugin } from './collapse'; +import type { Root } from 'mdast'; + +describe('collapse plugin', () => { + const createCollapseDirective = (content: any[], attributes?: Record): Root => ({ + type: 'root', + children: [ + { + type: 'containerDirective' as any, + name: 'collapse', + attributes, + children: content, + }, + ], + }); + + describe('basic transformation', () => { + it('should transform collapse directive to details element', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content here' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain('
'); + expect(htmlNode.value).toContain('

Content here

'); + expect(htmlNode.children).toBeUndefined(); + }); + + it('should use custom title from attributes', () => { + const tree = createCollapseDirective( + [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ], + { title: 'Custom Title' } + ); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Custom Title'); + }); + + it('should use default title when not specified', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Details'); + }); + + it('should be open by default', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
'); + }); + + it('should be closed when open=false', () => { + const tree = createCollapseDirective( + [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ], + { open: 'false' } + ); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
'); + expect(htmlNode.value).not.toContain('open'); + }); + + it('should include chevron icon', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain(' { + it('should render paragraph content', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [{ type: 'text', value: 'First paragraph' }], + }, + { + type: 'paragraph', + children: [{ type: 'text', value: 'Second paragraph' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('

First paragraph

'); + expect(htmlNode.value).toContain('

Second paragraph

'); + }); + + it('should render code blocks', () => { + const tree = createCollapseDirective([ + { + type: 'code', + lang: 'javascript', + value: 'console.log("test");', + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
');
+      expect(htmlNode.value).toContain('console.log("test");');
+    });
+
+    it('should render code blocks without language', () => {
+      const tree = createCollapseDirective([
+        {
+          type: 'code',
+          value: 'plain code',
+        },
+      ]);
+
+      const plugin = collapsePlugin();
+      plugin(tree);
+
+      const htmlNode = tree.children[0] as any;
+      expect(htmlNode.value).toContain('
plain code
'); + }); + + it('should render blockquotes', () => { + const tree = createCollapseDirective([ + { + type: 'blockquote', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Quote text' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
'); + expect(htmlNode.value).toContain('

Quote text

'); + expect(htmlNode.value).toContain('
'); + }); + + it('should render headings', () => { + const tree = createCollapseDirective([ + { + type: 'heading', + depth: 2, + children: [{ type: 'text', value: 'Section Title' }], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('

Section Title

'); + }); + + it('should render unordered lists', () => { + const tree = createCollapseDirective([ + { + type: 'list', + ordered: false, + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Item 1' }], + }, + ], + }, + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Item 2' }], + }, + ], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
    '); + expect(htmlNode.value).toContain('
  • Item 1
  • '); + expect(htmlNode.value).toContain('
  • Item 2
  • '); + expect(htmlNode.value).toContain('
'); + }); + + it('should render ordered lists', () => { + const tree = createCollapseDirective([ + { + type: 'list', + ordered: true, + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'First' }], + }, + ], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
    '); + expect(htmlNode.value).toContain('
  1. First
  2. '); + expect(htmlNode.value).toContain('
'); + }); + + it('should render nested lists', () => { + const tree = createCollapseDirective([ + { + type: 'list', + ordered: false, + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Parent item' }], + }, + { + type: 'list', + ordered: false, + children: [ + { + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Nested item' }], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('
    '); + expect(htmlNode.value).toContain('Parent item'); + expect(htmlNode.value).toContain('Nested item'); + }); + }); + + describe('inline content rendering', () => { + it('should render emphasis', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { type: 'text', value: 'Normal ' }, + { + type: 'emphasis', + children: [{ type: 'text', value: 'italic' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Normal italic'); + }); + + it('should render strong', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'strong', + children: [{ type: 'text', value: 'bold' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('bold'); + }); + + it('should render inline code', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { type: 'text', value: 'Use ' }, + { type: 'inlineCode', value: 'console.log()' }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Use console.log()'); + }); + + it('should render links', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'link', + url: 'https://example.com', + children: [{ type: 'text', value: 'Link text' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Link text'); + }); + + it('should render links with title', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'link', + url: 'https://example.com', + title: 'Example Site', + children: [{ type: 'text', value: 'Link' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('title="Example Site"'); + }); + + it('should render strikethrough', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'delete', + children: [{ type: 'text', value: 'deleted' }], + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('deleted'); + }); + + it('should render images', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'image', + url: 'image.png', + alt: 'Alt text', + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Alt text'); + }); + + it('should render images with title', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { + type: 'image', + url: 'image.png', + alt: 'Alt', + title: 'Image title', + }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('title="Image title"'); + }); + + it('should render line breaks', () => { + const tree = createCollapseDirective([ + { + type: 'paragraph', + children: [ + { type: 'text', value: 'Line 1' }, + { type: 'break' }, + { type: 'text', value: 'Line 2' }, + ], + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Line 1
    Line 2'); + }); + }); + + describe('HTML escaping', () => { + it('should escape HTML in title', () => { + const tree = createCollapseDirective( + [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ], + { title: '' } + ); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).not.toContain('', + }, + ]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('<script>'); + }); + }); + + describe('edge cases', () => { + it('should handle empty collapse directive', () => { + const tree = createCollapseDirective([]); + + const plugin = collapsePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain(' { + const tree: Root = { + type: 'root', + children: null as any, + }; + + const plugin = collapsePlugin(); + expect(() => plugin(tree)).not.toThrow(); + }); + + it('should handle node without name', () => { + const tree: Root = { + type: 'root', + children: [ + { + type: 'containerDirective' as any, + children: [], + }, + ], + }; + + const plugin = collapsePlugin(); + expect(() => plugin(tree)).not.toThrow(); + }); + + it('should skip non-collapse directives', () => { + const tree: Root = { + type: 'root', + children: [ + { + type: 'containerDirective' as any, + name: 'other', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Content' }], + }, + ], + }, + ], + }; + + const plugin = collapsePlugin(); + plugin(tree); + + const node = tree.children[0] as any; + expect(node.type).toBe('containerDirective'); + expect(node.name).toBe('other'); + }); + + it('should sanitize null/undefined children', () => { + const tree: Root = { + type: 'root', + children: [ + { + type: 'containerDirective' as any, + name: 'collapse', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Valid' }, null as any, undefined as any], + }, + ], + }, + ], + }; + + const plugin = collapsePlugin(); + expect(() => plugin(tree)).not.toThrow(); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).toContain('Valid'); + }); + }); +}); diff --git a/src/lib/plugins/filetree.test.ts b/src/lib/plugins/filetree.test.ts new file mode 100644 index 0000000..67093ef --- /dev/null +++ b/src/lib/plugins/filetree.test.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi } from 'vitest'; +import { filetreePlugin } from './filetree'; +import type { Root } from 'mdast'; +import * as treeParser from '../utils/tree-parser.js'; +import * as base64 from '../utils/base64.js'; + +describe('filetree plugin', () => { + const createCodeBlock = (lang: string, value: string): Root => ({ + type: 'root', + children: [ + { + type: 'code', + lang, + value, + }, + ], + }); + + it('should transform filetree code blocks to HTML', () => { + const mockTreeData = { + name: 'src', + type: 'directory', + children: [{ name: 'main.ts', type: 'file' }], + }; + + vi.spyOn(treeParser, 'parseTree').mockReturnValue(mockTreeData); + vi.spyOn(base64, 'encodeJsonBase64').mockReturnValue('encoded'); + + const tree = createCodeBlock('filetree', 'src/\n└── main.ts'); + const plugin = filetreePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain('
    { + const treeString = 'src/\n└── main.ts'; + const mockTreeData = { name: 'src', type: 'directory', children: [] }; + + const parseTreeSpy = vi.spyOn(treeParser, 'parseTree').mockReturnValue(mockTreeData); + vi.spyOn(base64, 'encodeJsonBase64').mockReturnValue('encoded'); + + const tree = createCodeBlock('filetree', treeString); + const plugin = filetreePlugin(); + plugin(tree); + + expect(parseTreeSpy).toHaveBeenCalledWith(treeString); + }); + + it('should encode tree data as base64', () => { + const mockTreeData = { name: 'root', type: 'directory', children: [] }; + + vi.spyOn(treeParser, 'parseTree').mockReturnValue(mockTreeData); + const encodeJsonBase64Spy = vi.spyOn(base64, 'encodeJsonBase64').mockReturnValue('encoded'); + + const tree = createCodeBlock('filetree', 'root/'); + const plugin = filetreePlugin(); + plugin(tree); + + expect(encodeJsonBase64Spy).toHaveBeenCalledWith(mockTreeData); + }); + + it('should not transform non-filetree code blocks', () => { + const tree = createCodeBlock('javascript', 'console.log("test")'); + const plugin = filetreePlugin(); + plugin(tree); + + const codeNode = tree.children[0] as any; + expect(codeNode.type).toBe('code'); + expect(codeNode.lang).toBe('javascript'); + }); + + it('should render error message on parse failure', () => { + vi.spyOn(treeParser, 'parseTree').mockImplementation(() => { + throw new Error('Parse error'); + }); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const tree = createCodeBlock('filetree', 'invalid tree'); + const plugin = filetreePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain('md-filetree--error'); + expect(htmlNode.value).toContain('Invalid File Tree'); + expect(htmlNode.value).toContain('Failed to parse file tree structure'); + expect(htmlNode.value).toContain('invalid tree'); + expect(consoleErrorSpy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + + it('should escape HTML in error message', () => { + vi.spyOn(treeParser, 'parseTree').mockImplementation(() => { + throw new Error('Parse error'); + }); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const maliciousContent = ''; + const tree = createCodeBlock('filetree', maliciousContent); + const plugin = filetreePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.value).not.toContain(''); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const tree = createTextNode('Check {@BadSymbol}'); + const plugin = referencePlugin(); + plugin(tree); + + const paragraph = tree.children[0] as any; + expect(paragraph.children[1].value).not.toContain('