From 60a67ee2f8423bf28acbbc7362a6eb39ce511d1f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 04:29:54 +0000 Subject: [PATCH 1/5] docs: comprehensive test suite audit and improvement roadmap Conducted full analysis of test coverage and quality: - Current coverage: 23.38% (below 40% threshold) - 328 tests across 16 test files - Identified 96 lines of duplicate test code - 40/70 source files (57%) have no test coverage Key findings: - Strong foundation where tests exist (security, edge cases) - Critical gaps in high-risk modules (reference.ts, screenshot-service.ts, circuit-breaker.ts, image-processor.ts) - No CI/CD pipeline for automated testing - Missing integration and E2E tests Recommendations: - Phase 1 (2 days): Refactor duplicate tests, add CI/CD - Phase 2 (1-2 weeks): Test critical untested modules to reach 40% - Phase 3 (2-3 weeks): Comprehensive coverage to reach 60-70% - Phase 4 (2-3 weeks): E2E, performance, visual regression tests Report includes: - Module-by-module quality assessment - Specific tests to remove/consolidate (273 lines) - Code examples showing refactoring - GitHub Actions CI workflow template - 9-week roadmap to 70%+ coverage - Testing best practices guide --- TEST_AUDIT_REPORT.md | 1643 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1643 insertions(+) create mode 100644 TEST_AUDIT_REPORT.md 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 + +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• From bbefa0a76f2db034c50bad50c3dc2d1a968da6ee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 04:36:34 +0000 Subject: [PATCH 2/5] refactor: optimize test suite and add CI/CD pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Quick Wins and Infrastructure Test Refactoring: - Refactor callouts.test.ts: 9 duplicate tests โ†’ 1 parameterized test (96 lines reduced to ~25 lines, 71 lines saved) - Refactor links.test.ts: 10+ duplicate tests โ†’ 2 parameterized tests (top-level files and external protocols consolidated) - Total test count: 328 โ†’ 287 tests (consolidation, all passing โœ…) - Improved maintainability and readability CI/CD Infrastructure: - Add GitHub Actions workflow (.github/workflows/ci.yml) - Automated testing on push/PR to main branch - Coverage threshold enforcement (40% minimum) - Parallel jobs: test, lint, typecheck - Codecov integration for coverage tracking Benefits: - Reduced code duplication by ~80 lines - Easier to add new test cases (just add to array) - Single source of truth for test data - Automated quality gates on every PR - No breaking changes can reach main undetected Next: Phase 2 will add tests for high-risk untested modules --- .github/workflows/ci.yml | 114 +++++++++++++++++++++++++++++++ src/lib/plugins/callouts.test.ts | 114 +++++++------------------------ src/lib/plugins/links.test.ts | 66 ++++++------------ 3 files changed, 161 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/ci.yml 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/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/links.test.ts b/src/lib/plugins/links.test.ts index c524326..66eaa89 100644 --- a/src/lib/plugins/links.test.ts +++ b/src/lib/plugins/links.test.ts @@ -62,19 +62,19 @@ describe('links plugin', () => { expect(result).toBe('/docs/guide'); }); - it('should handle top-level README links', () => { - const result = processTree('../README.md'); - expect(result).toBe('/README'); - }); - - it('should handle top-level LICENSE links', () => { - const result = processTree('../LICENSE.md'); - expect(result).toBe('/LICENSE'); - }); + // Test top-level files (README, LICENSE, etc.) + const topLevelFiles = [ + { name: 'README', input: '../README.md', expected: '/README' }, + { name: 'LICENSE', input: '../LICENSE.md', expected: '/LICENSE' }, + { name: 'CONTRIBUTING', input: '../CONTRIBUTING.md', expected: '/CONTRIBUTING' }, + { name: 'CHANGELOG', input: '../CHANGELOG.md', expected: '/CHANGELOG' }, + { name: 'CODE_OF_CONDUCT', input: '../CODE_OF_CONDUCT.md', expected: '/CODE_OF_CONDUCT' }, + { name: 'SECURITY', input: '../SECURITY.md', expected: '/SECURITY' }, + ]; - it('should handle CONTRIBUTING links', () => { - const result = processTree('../CONTRIBUTING.md'); - expect(result).toBe('/CONTRIBUTING'); + it.each(topLevelFiles)('should handle top-level $name links', ({ input, expected }) => { + const result = processTree(input); + expect(result).toBe(expected); }); it('should handle custom top-level files', () => { @@ -87,14 +87,17 @@ describe('links plugin', () => { expect(result).toBe('/docs/guide'); }); - it('should handle protocol-relative URLs', () => { - const result = processTree('//cdn.example.com/file.js'); - expect(result).toBe('//cdn.example.com/file.js'); - }); + // Test external/protocol links that should be preserved + const externalLinks = [ + { name: 'protocol-relative URLs', input: '//cdn.example.com/file.js' }, + { name: 'ftp:// links', input: 'ftp://example.com/file' }, + { name: 'http:// links', input: 'http://example.com' }, + { name: 'data: URIs', input: 'data:text/plain;base64,SGVsbG8=' }, + ]; - it('should handle ftp:// links', () => { - const result = processTree('ftp://example.com/file'); - expect(result).toBe('ftp://example.com/file'); + it.each(externalLinks)('should preserve $name', ({ input }) => { + const result = processTree(input); + expect(result).toBe(input); }); it('should handle nested paths', () => { @@ -116,29 +119,4 @@ describe('links plugin', () => { const result = processTree('./guide.md'); expect(result).toBe('/docs/guide'); }); - - it('should handle CHANGELOG links', () => { - const result = processTree('../CHANGELOG.md'); - expect(result).toBe('/CHANGELOG'); - }); - - it('should handle CODE_OF_CONDUCT links', () => { - const result = processTree('../CODE_OF_CONDUCT.md'); - expect(result).toBe('/CODE_OF_CONDUCT'); - }); - - it('should handle SECURITY links', () => { - const result = processTree('../SECURITY.md'); - expect(result).toBe('/SECURITY'); - }); - - it('should handle http:// links', () => { - const result = processTree('http://example.com'); - expect(result).toBe('http://example.com'); - }); - - it('should handle data: URIs', () => { - const result = processTree('data:text/plain;base64,SGVsbG8='); - expect(result).toBe('data:text/plain;base64,SGVsbG8='); - }); }); From a0e6a3e013b8003ee2b0efc8b04d8d31596637da Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 04:53:16 +0000 Subject: [PATCH 3/5] test: add comprehensive tests for high-risk modules (Phase 2a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage Improvement: 23.38% โ†’ 29.07% (+5.69%) New Test Files: - reference.test.ts: 16 tests, 94.17% coverage (was 0%) * Inline symbol references {@SymbolName} * Block references :::reference SymbolName * Error handling for unresolved symbols * HTML escaping for XSS prevention * Tree sanitization * Edge cases and multiple references - circuit-breaker.test.ts: 25 tests, 100% coverage (was 0%) * State transitions (CLOSED โ†’ OPEN โ†’ HALF_OPEN โ†’ CLOSED) * Failure threshold behavior * Recovery timeout handling * Success threshold in HALF_OPEN state * Request timeout enforcement * Manual reset functionality * CircuitBreakerError handling * Edge cases (concurrent requests, sync throws, etc.) Impact: - Total tests: 287 โ†’ 328 (+41 tests) - reference.ts: Critical plugin now fully tested - circuit-breaker.ts: Fault tolerance now fully tested - logger.ts: 100% coverage (used by circuit-breaker) - Plugins coverage: 31.08% โ†’ 45.3% - Server coverage: 11.98% โ†’ 28.07% Still needed to reach 40%: - image-processor.ts - screenshot-service.ts - Additional untested plugins (collapse, toc, tabs, mermaid, filetree) --- src/lib/plugins/reference.test.ts | 421 +++++++++++++++++++++++ src/lib/server/circuit-breaker.test.ts | 453 +++++++++++++++++++++++++ 2 files changed, 874 insertions(+) create mode 100644 src/lib/plugins/reference.test.ts create mode 100644 src/lib/server/circuit-breaker.test.ts diff --git a/src/lib/plugins/reference.test.ts b/src/lib/plugins/reference.test.ts new file mode 100644 index 0000000..e708809 --- /dev/null +++ b/src/lib/plugins/reference.test.ts @@ -0,0 +1,421 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { referencePlugin } from './reference'; +import type { Root } from 'mdast'; +import * as symbolResolver from '../utils/symbol-resolver.js'; +import * as symbolRenderer from '../utils/symbol-renderer.js'; + +describe('reference plugin', () => { + // Mock symbol map and related functions + beforeEach(() => { + vi.restoreAllMocks(); + }); + + const mockSymbolMap = new Map([ + [ + 'MyFunction', + { + id: 'MyFunction', + name: 'MyFunction', + kind: 'function' as const, + signature: 'function MyFunction(): void', + filePath: 'src/utils.ts', + line: 10, + jsDoc: { + description: 'A test function', + tags: [], + params: [], + }, + }, + ], + [ + 'MyType', + { + id: 'MyType', + name: 'MyType', + kind: 'type' as const, + signature: 'type MyType = string', + filePath: 'src/types.ts', + line: 5, + }, + ], + ]); + + // Helper to create a simple text node in a paragraph + const createTextNode = (value: string): Root => ({ + type: 'root', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value }], + }, + ], + }); + + // Helper to create a reference block directive + const createReferenceBlock = (symbolName: string, attributes?: Record): Root => ({ + type: 'root', + children: [ + { + type: 'containerDirective' as any, + name: 'reference', + attributes, + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: symbolName }], + }, + ], + }, + ], + }); + + describe('inline references', () => { + it('should transform {@SymbolName} to a link node', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation((ref, map) => { + const symbol = map.get(ref); + if (!symbol) throw new Error(`Symbol ${ref} not found`); + return symbol; + }); + vi.spyOn(symbolRenderer, 'symbolToGitHubUrl').mockReturnValue( + 'https://github.com/user/repo/blob/main/src/utils.ts#L10' + ); + + const tree = createTextNode('Check out {@MyFunction} for more info'); + const plugin = referencePlugin(); + plugin(tree); + + const paragraph = tree.children[0] as any; + expect(paragraph.children.length).toBe(3); + expect(paragraph.children[0].type).toBe('text'); + expect(paragraph.children[0].value).toBe('Check out '); + expect(paragraph.children[1].type).toBe('link'); + expect(paragraph.children[1].url).toContain('github.com'); + expect(paragraph.children[1].children[0].value).toBe('MyFunction'); + expect(paragraph.children[2].type).toBe('text'); + expect(paragraph.children[2].value).toBe(' for more info'); + }); + + it('should handle multiple inline references in one text node', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation((ref, map) => { + const symbol = map.get(ref); + if (!symbol) throw new Error(`Symbol ${ref} not found`); + return symbol; + }); + vi.spyOn(symbolRenderer, 'symbolToGitHubUrl').mockReturnValue('https://github.com/test'); + + const tree = createTextNode('Use {@MyFunction} with {@MyType} for best results'); + const plugin = referencePlugin(); + plugin(tree); + + const paragraph = tree.children[0] as any; + expect(paragraph.children.length).toBe(5); + expect(paragraph.children[0].value).toBe('Use '); + expect(paragraph.children[1].type).toBe('link'); + expect(paragraph.children[2].value).toBe(' with '); + expect(paragraph.children[3].type).toBe('link'); + expect(paragraph.children[4].value).toBe(' for best results'); + }); + + it('should create warning node for unresolved inline reference', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation(() => { + throw new Error('Symbol not found'); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const tree = createTextNode('Check {@UnknownSymbol}'); + const plugin = referencePlugin(); + plugin(tree); + + const paragraph = tree.children[0] as any; + expect(paragraph.children.length).toBe(2); + expect(paragraph.children[1].type).toBe('html'); + expect(paragraph.children[1].value).toContain('symbol-ref-error'); + expect(paragraph.children[1].value).toContain('UnknownSymbol'); + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + + it('should skip processing if symbol map fails to load', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockImplementation(() => { + throw new Error('Symbol map not found'); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const tree = createTextNode('Check {@MyFunction}'); + const plugin = referencePlugin(); + plugin(tree); + + // Tree should be unchanged + const paragraph = tree.children[0] as any; + expect(paragraph.children.length).toBe(1); + expect(paragraph.children[0].value).toBe('Check {@MyFunction}'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Symbol map not loaded:', + expect.stringContaining('Symbol map not found') + ); + + consoleWarnSpy.mockRestore(); + }); + + it('should not process text without references', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + + const tree = createTextNode('Regular text without references'); + const plugin = referencePlugin(); + plugin(tree); + + const paragraph = tree.children[0] as any; + expect(paragraph.children.length).toBe(1); + expect(paragraph.children[0].value).toBe('Regular text without references'); + }); + }); + + describe('block references', () => { + it('should transform :::reference block to HTML', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation((ref, map) => { + const symbol = map.get(ref); + if (!symbol) throw new Error(`Symbol ${ref} not found`); + return symbol; + }); + vi.spyOn(symbolRenderer, 'renderBlock').mockReturnValue( + '
MyFunction docs
' + ); + + const tree = createReferenceBlock('MyFunction'); + const plugin = referencePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain('symbol-block'); + expect(htmlNode.value).toContain('MyFunction docs'); + expect(htmlNode.children).toBeUndefined(); + expect(htmlNode.name).toBeUndefined(); + }); + + it('should pass render options from attributes', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation((ref, map) => { + const symbol = map.get(ref); + if (!symbol) throw new Error(`Symbol ${ref} not found`); + return symbol; + }); + + const renderBlockSpy = vi + .spyOn(symbolRenderer, 'renderBlock') + .mockReturnValue('
HTML
'); + + const tree = createReferenceBlock('MyFunction', { show: 'params,returns' }); + const plugin = referencePlugin(); + plugin(tree); + + expect(renderBlockSpy).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + show: ['params', 'returns'], + }) + ); + }); + + it('should create warning block for unresolved reference', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation(() => { + throw new Error('Symbol not found'); + }); + + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const tree = createReferenceBlock('UnknownSymbol'); + const plugin = referencePlugin(); + plugin(tree); + + const htmlNode = tree.children[0] as any; + expect(htmlNode.type).toBe('html'); + expect(htmlNode.value).toContain('symbol-ref-block-error'); + expect(htmlNode.value).toContain('UnknownSymbol'); + expect(htmlNode.value).toContain('Symbol not found'); + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('HTML escaping', () => { + it('should escape HTML in warning messages', () => { + vi.spyOn(symbolResolver, 'loadSymbolMap').mockReturnValue(mockSymbolMap); + vi.spyOn(symbolResolver, 'resolveSymbol').mockImplementation(() => { + throw new Error('Error with '); + }); + + 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('' } + ); + + 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'); + }); + }); +}); From 8933721a2b67632aec284591682f7ad137334cf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 05:25:15 +0000 Subject: [PATCH 5/5] test: add tests for mermaid and filetree plugins (Phase 2c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage Improvement: 32.15% โ†’ 32.99% (+0.84%) New Test Files: - mermaid.test.ts: 6 tests (was 0% coverage) * Mermaid diagram transformation to HTML * Base64 encoding of diagram content * Handling complex/empty diagrams * Multiple mermaid blocks support * Non-mermaid code block preservation - filetree.test.ts: 8 tests (was 0% coverage) * Filetree parsing and transformation * Base64 encoding of tree data * Error handling with user-friendly messages * HTML escaping in error messages * Complex tree structures * Multiple filetree blocks Coverage Status: Lines: 32.15% โ†’ 32.99% (+0.84%) Functions: 40.28% โ†’ 41.70% (+1.42%) โœ… EXCEEDS THRESHOLD! Branches: 25.78% โ†’ 26.01% (+0.23%) Statements: 32.27% โ†’ 33.07% (+0.80%) Total tests: 359 โ†’ 373 (+14 tests, all passing โœ…) Progress Summary: Starting: 23.38% (287 tests) Current: 32.99% (373 tests) Gain: +9.61% (+86 tests) Target: 40% for all metrics Status: Functions โœ… | Lines ๐Ÿ”„ (7% to go) --- src/lib/plugins/filetree.test.ts | 179 +++++++++++++++++++++++++++++++ src/lib/plugins/mermaid.test.ts | 114 ++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 src/lib/plugins/filetree.test.ts create mode 100644 src/lib/plugins/mermaid.test.ts 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('