This is page 26 of 33. Use http://codebase.md/tosin2013/documcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
│ ├── agents
│ │ ├── documcp-ast.md
│ │ ├── documcp-deploy.md
│ │ ├── documcp-memory.md
│ │ ├── documcp-test.md
│ │ └── documcp-tool.md
│ ├── copilot-instructions.md
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── automated-changelog.md
│ │ ├── bug_report.md
│ │ ├── bug_report.yml
│ │ ├── documentation_issue.md
│ │ ├── feature_request.md
│ │ ├── feature_request.yml
│ │ ├── npm-publishing-fix.md
│ │ └── release_improvements.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── release-drafter.yml
│ └── workflows
│ ├── auto-merge.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── deploy-docs.yml
│ ├── README.md
│ ├── release-drafter.yml
│ └── release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .linkcheck.config.json
├── .markdown-link-check.json
├── .nvmrc
├── .pre-commit-config.yaml
├── .versionrc.json
├── ARCHITECTURAL_CHANGES_SUMMARY.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│ ├── .docusaurus
│ │ ├── docusaurus-plugin-content-docs
│ │ │ └── default
│ │ │ └── __mdx-loader-dependency.json
│ │ └── docusaurus-plugin-content-pages
│ │ └── default
│ │ └── __plugin.json
│ ├── adrs
│ │ ├── adr-0001-mcp-server-architecture.md
│ │ ├── adr-0002-repository-analysis-engine.md
│ │ ├── adr-0003-static-site-generator-recommendation-engine.md
│ │ ├── adr-0004-diataxis-framework-integration.md
│ │ ├── adr-0005-github-pages-deployment-automation.md
│ │ ├── adr-0006-mcp-tools-api-design.md
│ │ ├── adr-0007-mcp-prompts-and-resources-integration.md
│ │ ├── adr-0008-intelligent-content-population-engine.md
│ │ ├── adr-0009-content-accuracy-validation-framework.md
│ │ ├── adr-0010-mcp-resource-pattern-redesign.md
│ │ ├── adr-0011-ce-mcp-compatibility.md
│ │ ├── adr-0012-priority-scoring-system-for-documentation-drift.md
│ │ ├── adr-0013-release-pipeline-and-package-distribution.md
│ │ └── README.md
│ ├── api
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── hierarchy.js
│ │ │ ├── highlight.css
│ │ │ ├── icons.js
│ │ │ ├── icons.svg
│ │ │ ├── main.js
│ │ │ ├── navigation.js
│ │ │ ├── search.js
│ │ │ └── style.css
│ │ ├── hierarchy.html
│ │ ├── index.html
│ │ ├── modules.html
│ │ └── variables
│ │ └── TOOLS.html
│ ├── assets
│ │ └── logo.svg
│ ├── CE-MCP-FINDINGS.md
│ ├── development
│ │ └── MCP_INSPECTOR_TESTING.md
│ ├── docusaurus.config.js
│ ├── explanation
│ │ ├── architecture.md
│ │ └── index.md
│ ├── guides
│ │ ├── link-validation.md
│ │ ├── playwright-integration.md
│ │ └── playwright-testing-workflow.md
│ ├── how-to
│ │ ├── analytics-setup.md
│ │ ├── change-watcher.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── drift-priority-scoring.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── llm-integration.md
│ │ ├── local-testing.md
│ │ ├── performance-optimization.md
│ │ ├── prompting-guide.md
│ │ ├── repository-analysis.md
│ │ ├── seo-optimization.md
│ │ ├── site-monitoring.md
│ │ ├── troubleshooting.md
│ │ └── usage-examples.md
│ ├── index.md
│ ├── knowledge-graph.md
│ ├── package-lock.json
│ ├── package.json
│ ├── phase-2-intelligence.md
│ ├── reference
│ │ ├── api-overview.md
│ │ ├── cli.md
│ │ ├── configuration.md
│ │ ├── deploy-pages.md
│ │ ├── index.md
│ │ ├── mcp-tools.md
│ │ └── prompt-templates.md
│ ├── research
│ │ ├── cross-domain-integration
│ │ │ └── README.md
│ │ ├── domain-1-mcp-architecture
│ │ │ ├── index.md
│ │ │ └── mcp-performance-research.md
│ │ ├── domain-2-repository-analysis
│ │ │ └── README.md
│ │ ├── domain-3-ssg-recommendation
│ │ │ ├── index.md
│ │ │ └── ssg-performance-analysis.md
│ │ ├── domain-4-diataxis-integration
│ │ │ └── README.md
│ │ ├── domain-5-github-deployment
│ │ │ ├── github-pages-security-analysis.md
│ │ │ └── index.md
│ │ ├── domain-6-api-design
│ │ │ └── README.md
│ │ ├── README.md
│ │ ├── research-integration-summary-2025-01-14.md
│ │ ├── research-progress-template.md
│ │ └── research-questions-2025-01-14.md
│ ├── robots.txt
│ ├── sidebars.js
│ ├── sitemap.xml
│ ├── src
│ │ └── css
│ │ └── custom.css
│ └── tutorials
│ ├── development-setup.md
│ ├── environment-setup.md
│ ├── first-deployment.md
│ ├── getting-started.md
│ ├── index.md
│ ├── memory-workflows.md
│ └── user-onboarding.md
├── ISSUE_IMPLEMENTATION_SUMMARY.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│ └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│ ├── benchmarks
│ │ └── performance.ts
│ ├── index.ts
│ ├── memory
│ │ ├── contextual-retrieval.ts
│ │ ├── deployment-analytics.ts
│ │ ├── enhanced-manager.ts
│ │ ├── export-import.ts
│ │ ├── freshness-kg-integration.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kg-code-integration.ts
│ │ ├── kg-health.ts
│ │ ├── kg-integration.ts
│ │ ├── kg-link-validator.ts
│ │ ├── kg-storage.ts
│ │ ├── knowledge-graph.ts
│ │ ├── learning.ts
│ │ ├── manager.ts
│ │ ├── multi-agent-sharing.ts
│ │ ├── pruning.ts
│ │ ├── schemas.ts
│ │ ├── storage.ts
│ │ ├── temporal-analysis.ts
│ │ ├── user-preferences.ts
│ │ └── visualization.ts
│ ├── prompts
│ │ └── technical-writer-prompts.ts
│ ├── scripts
│ │ └── benchmark.ts
│ ├── templates
│ │ └── playwright
│ │ ├── accessibility.spec.template.ts
│ │ ├── Dockerfile.template
│ │ ├── docs-e2e.workflow.template.yml
│ │ ├── link-validation.spec.template.ts
│ │ └── playwright.config.template.ts
│ ├── tools
│ │ ├── analyze-deployments.ts
│ │ ├── analyze-readme.ts
│ │ ├── analyze-repository.ts
│ │ ├── change-watcher.ts
│ │ ├── check-documentation-links.ts
│ │ ├── cleanup-agent-artifacts.ts
│ │ ├── deploy-pages.ts
│ │ ├── detect-gaps.ts
│ │ ├── evaluate-readme-health.ts
│ │ ├── generate-config.ts
│ │ ├── generate-contextual-content.ts
│ │ ├── generate-llm-context.ts
│ │ ├── generate-readme-template.ts
│ │ ├── generate-technical-writer-prompts.ts
│ │ ├── kg-health-check.ts
│ │ ├── manage-preferences.ts
│ │ ├── manage-sitemap.ts
│ │ ├── optimize-readme.ts
│ │ ├── populate-content.ts
│ │ ├── readme-best-practices.ts
│ │ ├── recommend-ssg.ts
│ │ ├── setup-playwright-tests.ts
│ │ ├── setup-structure.ts
│ │ ├── simulate-execution.ts
│ │ ├── sync-code-to-docs.ts
│ │ ├── test-local-deployment.ts
│ │ ├── track-documentation-freshness.ts
│ │ ├── update-existing-documentation.ts
│ │ ├── validate-content.ts
│ │ ├── validate-documentation-freshness.ts
│ │ ├── validate-readme-checklist.ts
│ │ └── verify-deployment.ts
│ ├── types
│ │ └── api.ts
│ ├── utils
│ │ ├── artifact-detector.ts
│ │ ├── ast-analyzer.ts
│ │ ├── change-watcher.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── execution-simulator.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── llm-client.ts
│ │ ├── permission-checker.ts
│ │ ├── semantic-analyzer.ts
│ │ ├── sitemap-generator.ts
│ │ ├── usage-metadata.ts
│ │ └── user-feedback-integration.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── call-graph-builder.test.ts
│ ├── change-watcher-priority.integration.test.ts
│ ├── change-watcher.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── execution-simulator.test.ts
│ ├── functional
│ │ └── tools.test.ts
│ ├── integration
│ │ ├── kg-documentation-workflow.test.ts
│ │ ├── knowledge-graph-workflow.test.ts
│ │ ├── mcp-readme-tools.test.ts
│ │ ├── memory-mcp-tools.test.ts
│ │ ├── readme-technical-writer.test.ts
│ │ └── workflow.test.ts
│ ├── memory
│ │ ├── contextual-retrieval.test.ts
│ │ ├── enhanced-manager.test.ts
│ │ ├── export-import.test.ts
│ │ ├── freshness-kg-integration.test.ts
│ │ ├── kg-code-integration.test.ts
│ │ ├── kg-health.test.ts
│ │ ├── kg-link-validator.test.ts
│ │ ├── kg-storage-validation.test.ts
│ │ ├── kg-storage.test.ts
│ │ ├── knowledge-graph-documentation-examples.test.ts
│ │ ├── knowledge-graph-enhanced.test.ts
│ │ ├── knowledge-graph.test.ts
│ │ ├── learning.test.ts
│ │ ├── manager-advanced.test.ts
│ │ ├── manager.test.ts
│ │ ├── mcp-resource-integration.test.ts
│ │ ├── mcp-tool-persistence.test.ts
│ │ ├── schemas-documentation-examples.test.ts
│ │ ├── schemas.test.ts
│ │ ├── storage.test.ts
│ │ ├── temporal-analysis.test.ts
│ │ └── user-preferences.test.ts
│ ├── performance
│ │ ├── memory-load-testing.test.ts
│ │ └── memory-stress-testing.test.ts
│ ├── prompts
│ │ ├── guided-workflow-prompts.test.ts
│ │ └── technical-writer-prompts.test.ts
│ ├── server.test.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── all-tools.test.ts
│ │ ├── analyze-coverage.test.ts
│ │ ├── analyze-deployments.test.ts
│ │ ├── analyze-readme.test.ts
│ │ ├── analyze-repository.test.ts
│ │ ├── check-documentation-links.test.ts
│ │ ├── cleanup-agent-artifacts.test.ts
│ │ ├── deploy-pages-kg-retrieval.test.ts
│ │ ├── deploy-pages-tracking.test.ts
│ │ ├── deploy-pages.test.ts
│ │ ├── detect-gaps.test.ts
│ │ ├── evaluate-readme-health.test.ts
│ │ ├── generate-contextual-content.test.ts
│ │ ├── generate-llm-context.test.ts
│ │ ├── generate-readme-template.test.ts
│ │ ├── generate-technical-writer-prompts.test.ts
│ │ ├── kg-health-check.test.ts
│ │ ├── manage-sitemap.test.ts
│ │ ├── optimize-readme.test.ts
│ │ ├── readme-best-practices.test.ts
│ │ ├── recommend-ssg-historical.test.ts
│ │ ├── recommend-ssg-preferences.test.ts
│ │ ├── recommend-ssg.test.ts
│ │ ├── simple-coverage.test.ts
│ │ ├── sync-code-to-docs.test.ts
│ │ ├── test-local-deployment.test.ts
│ │ ├── tool-error-handling.test.ts
│ │ ├── track-documentation-freshness.test.ts
│ │ ├── validate-content.test.ts
│ │ ├── validate-documentation-freshness.test.ts
│ │ └── validate-readme-checklist.test.ts
│ ├── types
│ │ └── type-safety.test.ts
│ └── utils
│ ├── artifact-detector.test.ts
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector-diataxis.test.ts
│ ├── drift-detector-priority.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ ├── llm-client.test.ts
│ ├── semantic-analyzer.test.ts
│ ├── sitemap-generator.test.ts
│ ├── usage-metadata.test.ts
│ └── user-feedback-integration.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/tests/tools/validate-content.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2 | import * as fs from "fs/promises";
3 | import * as path from "path";
4 | import {
5 | handleValidateDiataxisContent,
6 | validateGeneralContent,
7 | } from "../../src/tools/validate-content.js";
8 | import { ValidationResult } from "../../src/tools/validate-content.js";
9 |
10 | describe("Content Validation Tool", () => {
11 | const testTempDir = path.join(__dirname, "../../.tmp/test-validation");
12 |
13 | beforeEach(async () => {
14 | // Create test directory
15 | await fs.mkdir(testTempDir, { recursive: true });
16 | });
17 |
18 | afterEach(async () => {
19 | // Clean up test directory
20 | try {
21 | await fs.rm(testTempDir, { recursive: true });
22 | } catch {
23 | // Ignore cleanup errors
24 | }
25 | });
26 |
27 | describe("Application Code Validation", () => {
28 | it("should detect application code path correctly", async () => {
29 | // Create mock application structure
30 | const appDir = path.join(testTempDir, "mock-app");
31 | await fs.mkdir(appDir, { recursive: true });
32 | await fs.mkdir(path.join(appDir, "src"), { recursive: true });
33 | await fs.writeFile(
34 | path.join(appDir, "package.json"),
35 | '{"name": "test-app"}',
36 | );
37 |
38 | // Create TypeScript file without documentation
39 | const tsFile = path.join(appDir, "src", "index.ts");
40 | await fs.writeFile(
41 | tsFile,
42 | `
43 | export function undocumentedFunction(param: string): string {
44 | return param.toUpperCase();
45 | }
46 |
47 | export const anotherFunction = (value: number) => {
48 | if (value < 0) {
49 | throw new Error('Invalid value');
50 | }
51 | return value * 2;
52 | };
53 | `.trim(),
54 | );
55 |
56 | const result = await handleValidateDiataxisContent({
57 | contentPath: appDir,
58 | validationType: "compliance",
59 | includeCodeValidation: true,
60 | });
61 |
62 | expect(result).toBeDefined();
63 | expect(result.issues).toBeDefined();
64 |
65 | // Should find issues with undocumented exported functions
66 | const undocumentedIssues = result.issues.filter((issue) =>
67 | issue.description.includes("lacks documentation"),
68 | );
69 | expect(undocumentedIssues.length).toBeGreaterThan(0);
70 |
71 | // Should find issues with undocumented error throwing
72 | const errorDocIssues = result.issues.filter((issue) =>
73 | issue.description.includes(
74 | "Error throwing code found without error documentation",
75 | ),
76 | );
77 | expect(errorDocIssues.length).toBeGreaterThan(0);
78 | });
79 |
80 | it("should validate application architecture structure", async () => {
81 | // Create mock application with missing directories
82 | const appDir = path.join(testTempDir, "incomplete-app");
83 | await fs.mkdir(appDir, { recursive: true });
84 | await fs.writeFile(
85 | path.join(appDir, "package.json"),
86 | '{"name": "incomplete-app"}',
87 | );
88 |
89 | // Missing tools and types directories
90 | await fs.mkdir(path.join(appDir, "src"), { recursive: true });
91 | await fs.writeFile(
92 | path.join(appDir, "src", "index.ts"),
93 | 'export const app = "test";',
94 | );
95 |
96 | const result = await handleValidateDiataxisContent({
97 | contentPath: appDir,
98 | validationType: "compliance",
99 | includeCodeValidation: false,
100 | });
101 |
102 | const structureIssues = result.issues.filter(
103 | (issue) => issue.location.file === "application structure",
104 | );
105 | expect(structureIssues.length).toBeGreaterThan(0);
106 |
107 | // Should suggest missing tools directory
108 | const toolsIssue = structureIssues.find((issue) =>
109 | issue.description.includes("tools directory"),
110 | );
111 | expect(toolsIssue).toBeDefined();
112 | });
113 |
114 | it("should validate README structure", async () => {
115 | const appDir = path.join(testTempDir, "readme-test");
116 | await fs.mkdir(appDir, { recursive: true });
117 | await fs.mkdir(path.join(appDir, "src"), { recursive: true });
118 | await fs.writeFile(
119 | path.join(appDir, "package.json"),
120 | '{"name": "readme-test"}',
121 | );
122 | await fs.writeFile(
123 | path.join(appDir, "src", "index.ts"),
124 | 'export const app = "test";',
125 | );
126 |
127 | // Create README with missing sections
128 | await fs.writeFile(
129 | path.join(appDir, "README.md"),
130 | `
131 | This is a project without proper structure.
132 | Some description here.
133 | `.trim(),
134 | );
135 |
136 | const result = await handleValidateDiataxisContent({
137 | contentPath: appDir,
138 | validationType: "compliance",
139 | includeCodeValidation: false,
140 | });
141 |
142 | // Application validation should find issues
143 |
144 | const readmeIssues = result.issues.filter(
145 | (issue) => issue.location.file === "README.md",
146 | );
147 | expect(readmeIssues.length).toBeGreaterThan(0);
148 |
149 | // Should find issues with README structure
150 | const structureIssue = readmeIssues.find((issue) =>
151 | issue.description.includes("lacks essential sections"),
152 | );
153 | expect(structureIssue).toBeDefined();
154 | });
155 |
156 | it("should detect properly documented functions", async () => {
157 | const appDir = path.join(testTempDir, "documented-app");
158 | await fs.mkdir(appDir, { recursive: true });
159 | await fs.mkdir(path.join(appDir, "src"), { recursive: true });
160 | await fs.writeFile(
161 | path.join(appDir, "package.json"),
162 | '{"name": "documented-app"}',
163 | );
164 |
165 | // Create well-documented TypeScript file
166 | const tsFile = path.join(appDir, "src", "documented.ts");
167 | await fs.writeFile(
168 | tsFile,
169 | `
170 | /**
171 | * Converts a string to uppercase
172 | * @param param - The input string
173 | * @returns The uppercase string
174 | */
175 | export function documentedFunction(param: string): string {
176 | return param.toUpperCase();
177 | }
178 |
179 | /**
180 | * Doubles a positive number
181 | * @param value - The input number (must be positive)
182 | * @returns The doubled value
183 | * @throws {Error} When value is negative
184 | */
185 | export const wellDocumentedFunction = (value: number) => {
186 | if (value < 0) {
187 | throw new Error('Invalid value');
188 | }
189 | return value * 2;
190 | };
191 | `.trim(),
192 | );
193 |
194 | const result = await handleValidateDiataxisContent({
195 | contentPath: appDir,
196 | validationType: "compliance",
197 | includeCodeValidation: true,
198 | });
199 |
200 | // Should have no undocumented issues since functions are properly documented
201 | const undocumentedIssues = result.issues.filter((issue) =>
202 | issue.description.includes("lacks documentation"),
203 | );
204 | expect(undocumentedIssues.length).toBe(0);
205 |
206 | // Should not complain about error documentation
207 | const errorDocIssues = result.issues.filter((issue) =>
208 | issue.description.includes(
209 | "Error throwing code found without error documentation",
210 | ),
211 | );
212 | expect(errorDocIssues.length).toBe(0);
213 | });
214 | });
215 |
216 | describe("Documentation Validation", () => {
217 | it("should detect documentation directory correctly", async () => {
218 | // Create mock documentation structure
219 | const docsDir = path.join(testTempDir, "docs");
220 | await fs.mkdir(docsDir, { recursive: true });
221 | await fs.mkdir(path.join(docsDir, "tutorials"), { recursive: true });
222 |
223 | await fs.writeFile(
224 | path.join(docsDir, "tutorials", "tutorial1.md"),
225 | `
226 | # Tutorial 1
227 |
228 | This is a tutorial without prerequisites section.
229 |
230 | \`\`\`javascript
231 | console.log("hello")
232 | \`\`\`
233 | `.trim(),
234 | );
235 |
236 | const result = await handleValidateDiataxisContent({
237 | contentPath: docsDir,
238 | validationType: "compliance",
239 | includeCodeValidation: true,
240 | });
241 |
242 | expect(result).toBeDefined();
243 |
244 | // Should find Diataxis compliance issues
245 | const complianceIssues = result.issues.filter(
246 | (issue) => issue.category === "compliance",
247 | );
248 | expect(complianceIssues.length).toBeGreaterThan(0);
249 |
250 | // Should find missing prerequisites in tutorial
251 | const prereqIssue = complianceIssues.find((issue) =>
252 | issue.description.includes("prerequisites"),
253 | );
254 | expect(prereqIssue).toBeDefined();
255 | });
256 |
257 | it("should validate link integrity", async () => {
258 | const docsDir = path.join(testTempDir, "docs-links");
259 | await fs.mkdir(docsDir, { recursive: true });
260 |
261 | // Create file with broken internal link
262 | await fs.writeFile(
263 | path.join(docsDir, "index.md"),
264 | `
265 | # Documentation
266 |
267 | [Broken Link](./nonexistent.md)
268 | [Another Link](./other.md)
269 | `.trim(),
270 | );
271 |
272 | // Create the referenced file
273 | await fs.writeFile(path.join(docsDir, "other.md"), "# Other Page");
274 |
275 | const result = await handleValidateDiataxisContent({
276 | contentPath: docsDir,
277 | validationType: "accuracy",
278 | includeCodeValidation: false,
279 | });
280 |
281 | const linkIssues = result.issues.filter((issue) =>
282 | issue.description.includes("Broken internal link"),
283 | );
284 | expect(linkIssues.length).toBe(1);
285 |
286 | const brokenLink = linkIssues[0];
287 | expect(brokenLink.description).toContain("nonexistent.md");
288 | });
289 |
290 | it("should validate code blocks in documentation", async () => {
291 | const docsDir = path.join(testTempDir, "docs-code");
292 | await fs.mkdir(docsDir, { recursive: true });
293 |
294 | await fs.writeFile(
295 | path.join(docsDir, "guide.md"),
296 | `
297 | # Code Examples
298 |
299 | \`\`\`javascript
300 | // Missing semicolon
301 | console.log("test")
302 | \`\`\`
303 |
304 | \`\`\`json
305 | { "valid": "json" }
306 | \`\`\`
307 |
308 | \`\`\`json
309 | { "invalid": json }
310 | \`\`\`
311 | `.trim(),
312 | );
313 |
314 | const result = await handleValidateDiataxisContent({
315 | contentPath: docsDir,
316 | validationType: "all",
317 | includeCodeValidation: true,
318 | });
319 |
320 | expect(result.codeValidation).toBeDefined();
321 | expect(result.codeValidation!.exampleResults.length).toBeGreaterThan(0);
322 |
323 | // Should find JSON syntax error
324 | const jsonErrors = result.codeValidation!.exampleResults.filter((ex) =>
325 | ex.issues.some((issue) => issue.description.includes("Invalid JSON")),
326 | );
327 | expect(jsonErrors.length).toBeGreaterThan(0);
328 | });
329 | });
330 |
331 | describe("General Content Validation", () => {
332 | it("should validate general content with link checking", async () => {
333 | const contentDir = path.join(testTempDir, "general-content");
334 | await fs.mkdir(contentDir, { recursive: true });
335 |
336 | await fs.writeFile(
337 | path.join(contentDir, "page.md"),
338 | `
339 | # Test Page
340 |
341 | [Good Link](./existing.md)
342 | [Bad Link](./missing.md)
343 |
344 | \`\`\`js
345 | console.log("missing semicolon")
346 | \`\`\`
347 | `.trim(),
348 | );
349 |
350 | await fs.writeFile(
351 | path.join(contentDir, "existing.md"),
352 | "# Existing Page",
353 | );
354 |
355 | const result = await validateGeneralContent({
356 | contentPath: contentDir,
357 | validationType: "all",
358 | includeCodeValidation: true,
359 | });
360 |
361 | expect(result.success).toBe(false);
362 | expect(result.brokenLinks.length).toBe(1);
363 | expect(result.brokenLinks[0]).toContain("missing.md");
364 | expect(result.codeBlocksValidated).toBeGreaterThan(0);
365 | expect(result.codeErrors.length).toBeGreaterThan(0);
366 | expect(result.recommendations.length).toBeGreaterThan(0);
367 | });
368 |
369 | it("should pass validation for clean content", async () => {
370 | const contentDir = path.join(testTempDir, "clean-content");
371 | await fs.mkdir(contentDir, { recursive: true });
372 |
373 | await fs.writeFile(
374 | path.join(contentDir, "clean.md"),
375 | `
376 | # Clean Page
377 |
378 | [Good Link](./other.md)
379 |
380 | \`\`\`json
381 | { "valid": "json" }
382 | \`\`\`
383 | `.trim(),
384 | );
385 |
386 | await fs.writeFile(path.join(contentDir, "other.md"), "# Other Page");
387 |
388 | const result = await validateGeneralContent({
389 | contentPath: contentDir,
390 | validationType: "all",
391 | includeCodeValidation: true,
392 | });
393 |
394 | expect(result.success).toBe(true);
395 | expect(result.brokenLinks.length).toBe(0);
396 | expect(result.recommendations).toContain(
397 | "Content validation passed - no critical issues found",
398 | );
399 | });
400 | });
401 |
402 | describe("Confidence Metrics", () => {
403 | it("should calculate confidence metrics correctly", async () => {
404 | const appDir = path.join(testTempDir, "confidence-test");
405 | await fs.mkdir(appDir, { recursive: true });
406 | await fs.mkdir(path.join(appDir, "src"), { recursive: true });
407 | await fs.writeFile(
408 | path.join(appDir, "package.json"),
409 | '{"name": "confidence-test"}',
410 | );
411 | await fs.writeFile(
412 | path.join(appDir, "src", "index.ts"),
413 | 'export const test = "value";',
414 | );
415 |
416 | const result = await handleValidateDiataxisContent({
417 | contentPath: appDir,
418 | validationType: "all",
419 | includeCodeValidation: true,
420 | });
421 |
422 | expect(result.confidence).toBeDefined();
423 | expect(result.confidence.overall).toBeGreaterThan(0);
424 | expect(result.confidence.overall).toBeLessThanOrEqual(100);
425 |
426 | expect(result.confidence.breakdown).toBeDefined();
427 | expect(result.confidence.breakdown.technologyDetection).toBeDefined();
428 | expect(result.confidence.breakdown.codeExampleRelevance).toBeDefined();
429 | expect(
430 | result.confidence.breakdown.architecturalAssumptions,
431 | ).toBeDefined();
432 | });
433 |
434 | it("should provide recommendations based on confidence", async () => {
435 | const appDir = path.join(testTempDir, "recommendations-test");
436 | await fs.mkdir(appDir, { recursive: true });
437 | await fs.writeFile(
438 | path.join(appDir, "package.json"),
439 | '{"name": "recommendations-test"}',
440 | );
441 |
442 | // Create content that will generate issues
443 | await fs.writeFile(path.join(appDir, "README.md"), "No proper structure");
444 |
445 | const result = await handleValidateDiataxisContent({
446 | contentPath: appDir,
447 | validationType: "all",
448 | includeCodeValidation: false,
449 | });
450 |
451 | expect(result.recommendations).toBeDefined();
452 | expect(result.recommendations.length).toBeGreaterThan(0);
453 | expect(result.nextSteps).toBeDefined();
454 | expect(result.nextSteps.length).toBeGreaterThan(0);
455 |
456 | if (result.confidence.overall < 70) {
457 | expect(
458 | result.recommendations.some((rec) =>
459 | rec.includes("comprehensive review"),
460 | ),
461 | ).toBe(true);
462 | }
463 | });
464 | });
465 |
466 | describe("Error Handling and Edge Cases", () => {
467 | it("should handle non-existent content path gracefully", async () => {
468 | const nonExistentPath = path.join(testTempDir, "does-not-exist");
469 |
470 | const result = await handleValidateDiataxisContent({
471 | contentPath: nonExistentPath,
472 | validationType: "all",
473 | includeCodeValidation: false,
474 | });
475 |
476 | expect(result).toBeDefined();
477 | // The function handles non-existent paths gracefully but may still succeed
478 | expect(result.confidence).toBeDefined();
479 | });
480 |
481 | it("should handle empty directory", async () => {
482 | const emptyDir = path.join(testTempDir, "empty-dir");
483 | await fs.mkdir(emptyDir, { recursive: true });
484 |
485 | const result = await handleValidateDiataxisContent({
486 | contentPath: emptyDir,
487 | validationType: "all",
488 | includeCodeValidation: true,
489 | });
490 |
491 | expect(result).toBeDefined();
492 | expect(result.confidence.breakdown.architecturalAssumptions).toBeLessThan(
493 | 80,
494 | );
495 | });
496 |
497 | it("should handle project context loading with analysis ID", async () => {
498 | const appDir = path.join(testTempDir, "context-test");
499 | await fs.mkdir(appDir, { recursive: true });
500 | await fs.writeFile(
501 | path.join(appDir, "package.json"),
502 | '{"name": "context-test"}',
503 | );
504 |
505 | // Create .documcp directory with analysis
506 | const docucmpDir = path.join(appDir, ".documcp", "analyses");
507 | await fs.mkdir(docucmpDir, { recursive: true });
508 | await fs.writeFile(
509 | path.join(docucmpDir, "test-analysis.json"),
510 | JSON.stringify({
511 | metadata: {
512 | projectName: "test-project",
513 | primaryLanguage: "TypeScript",
514 | },
515 | technologies: { framework: "React" },
516 | dependencies: { packages: ["react", "typescript"] },
517 | }),
518 | );
519 |
520 | const result = await handleValidateDiataxisContent({
521 | contentPath: appDir,
522 | analysisId: "test-analysis",
523 | validationType: "accuracy",
524 | includeCodeValidation: false,
525 | });
526 |
527 | expect(result).toBeDefined();
528 | expect(result.confidence).toBeDefined();
529 | });
530 |
531 | it("should handle missing analysis ID gracefully", async () => {
532 | const appDir = path.join(testTempDir, "missing-analysis");
533 | await fs.mkdir(appDir, { recursive: true });
534 | await fs.writeFile(
535 | path.join(appDir, "package.json"),
536 | '{"name": "missing-analysis"}',
537 | );
538 |
539 | const result = await handleValidateDiataxisContent({
540 | contentPath: appDir,
541 | analysisId: "non-existent-analysis",
542 | validationType: "accuracy",
543 | includeCodeValidation: false,
544 | });
545 |
546 | expect(result).toBeDefined();
547 | expect(result.confidence).toBeDefined();
548 | });
549 |
550 | it("should detect documentation directory correctly", async () => {
551 | const docsPath = path.join(testTempDir, "project", "docs");
552 | await fs.mkdir(docsPath, { recursive: true });
553 | await fs.writeFile(path.join(docsPath, "index.md"), "# Documentation");
554 |
555 | const result = await handleValidateDiataxisContent({
556 | contentPath: docsPath,
557 | validationType: "compliance",
558 | includeCodeValidation: false,
559 | });
560 |
561 | expect(result).toBeDefined();
562 | expect(result.confidence).toBeDefined();
563 | // Documentation directory should be processed
564 | expect(
565 | result.confidence.breakdown.architecturalAssumptions,
566 | ).toBeGreaterThan(0);
567 | });
568 |
569 | it("should handle different validation types", async () => {
570 | const appDir = path.join(testTempDir, "validation-types");
571 | await fs.mkdir(appDir, { recursive: true });
572 | await fs.writeFile(
573 | path.join(appDir, "test.md"),
574 | "# Test\n[broken link](./missing.md)",
575 | );
576 |
577 | // Test accuracy only
578 | const accuracyResult = await handleValidateDiataxisContent({
579 | contentPath: appDir,
580 | validationType: "accuracy",
581 | includeCodeValidation: false,
582 | });
583 | expect(accuracyResult).toBeDefined();
584 |
585 | // Test completeness only
586 | const completenessResult = await handleValidateDiataxisContent({
587 | contentPath: appDir,
588 | validationType: "completeness",
589 | includeCodeValidation: false,
590 | });
591 | expect(completenessResult).toBeDefined();
592 |
593 | // Test compliance only
594 | const complianceResult = await handleValidateDiataxisContent({
595 | contentPath: appDir,
596 | validationType: "compliance",
597 | includeCodeValidation: false,
598 | });
599 | expect(complianceResult).toBeDefined();
600 | });
601 |
602 | it("should handle code validation failure scenarios", async () => {
603 | const appDir = path.join(testTempDir, "code-validation-fail");
604 | await fs.mkdir(appDir, { recursive: true });
605 |
606 | // Create markdown with broken code examples
607 | await fs.writeFile(
608 | path.join(appDir, "broken-code.md"),
609 | `
610 | # Broken Code Examples
611 |
612 | \`\`\`javascript
613 | // Syntax error
614 | console.log("missing quote);
615 | \`\`\`
616 |
617 | \`\`\`json
618 | { "invalid": json }
619 | \`\`\`
620 | `.trim(),
621 | );
622 |
623 | const result = await handleValidateDiataxisContent({
624 | contentPath: appDir,
625 | validationType: "all",
626 | includeCodeValidation: true,
627 | });
628 |
629 | expect(result.codeValidation).toBeDefined();
630 | expect(result.codeValidation!.overallSuccess).toBe(false);
631 | expect(
632 | result.recommendations.some((rec) => rec.includes("Fix code examples")),
633 | ).toBe(true);
634 | });
635 |
636 | it("should generate risk factors for critical issues", async () => {
637 | const appDir = path.join(testTempDir, "risk-factors");
638 | await fs.mkdir(appDir, { recursive: true });
639 |
640 | // Create content with multiple critical issues
641 | await fs.writeFile(
642 | path.join(appDir, "critical-issues.md"),
643 | `
644 | # Critical Issues
645 |
646 | [Broken Link 1](./missing1.md)
647 | [Broken Link 2](./missing2.md)
648 | [Broken Link 3](./missing3.md)
649 | `.trim(),
650 | );
651 |
652 | const result = await handleValidateDiataxisContent({
653 | contentPath: appDir,
654 | validationType: "all",
655 | includeCodeValidation: false,
656 | });
657 |
658 | expect(result.confidence.riskFactors).toBeDefined();
659 | expect(result.confidence.riskFactors.length).toBeGreaterThan(0);
660 |
661 | const highRiskFactors = result.confidence.riskFactors.filter(
662 | (rf) => rf.type === "high",
663 | );
664 | expect(highRiskFactors.length).toBeGreaterThan(0);
665 | });
666 |
667 | it("should handle uncertainty flags and medium risk factors", async () => {
668 | const appDir = path.join(testTempDir, "uncertainty-test");
669 | await fs.mkdir(appDir, { recursive: true });
670 |
671 | // Create content that generates uncertainties
672 | await fs.writeFile(
673 | path.join(appDir, "uncertain.md"),
674 | `
675 | # Uncertain Content
676 |
677 | This content has many ambiguous references and unclear instructions.
678 | Multiple areas need clarification for proper understanding.
679 | `.trim(),
680 | );
681 |
682 | const result = await handleValidateDiataxisContent({
683 | contentPath: appDir,
684 | validationType: "all",
685 | includeCodeValidation: false,
686 | });
687 |
688 | // Manually add uncertainties to test the risk factor generation
689 | result.uncertainties = [
690 | {
691 | area: "test1",
692 | severity: "high",
693 | description: "test",
694 | potentialImpact: "test",
695 | clarificationNeeded: "test",
696 | fallbackStrategy: "test",
697 | },
698 | {
699 | area: "test2",
700 | severity: "high",
701 | description: "test",
702 | potentialImpact: "test",
703 | clarificationNeeded: "test",
704 | fallbackStrategy: "test",
705 | },
706 | {
707 | area: "test3",
708 | severity: "high",
709 | description: "test",
710 | potentialImpact: "test",
711 | clarificationNeeded: "test",
712 | fallbackStrategy: "test",
713 | },
714 | {
715 | area: "test4",
716 | severity: "high",
717 | description: "test",
718 | potentialImpact: "test",
719 | clarificationNeeded: "test",
720 | fallbackStrategy: "test",
721 | },
722 | {
723 | area: "test5",
724 | severity: "high",
725 | description: "test",
726 | potentialImpact: "test",
727 | clarificationNeeded: "test",
728 | fallbackStrategy: "test",
729 | },
730 | {
731 | area: "test6",
732 | severity: "high",
733 | description: "test",
734 | potentialImpact: "test",
735 | clarificationNeeded: "test",
736 | fallbackStrategy: "test",
737 | },
738 | ];
739 |
740 | expect(result.uncertainties.length).toBeGreaterThan(5);
741 |
742 | const highUncertainties = result.uncertainties.filter(
743 | (u) => u.severity === "high" || u.severity === "critical",
744 | );
745 | expect(highUncertainties.length).toBeGreaterThan(0);
746 | });
747 |
748 | it("should handle Diataxis structure analysis", async () => {
749 | const docsDir = path.join(testTempDir, "diataxis-structure");
750 | await fs.mkdir(docsDir, { recursive: true });
751 |
752 | // Create Diataxis structure
753 | await fs.mkdir(path.join(docsDir, "tutorials"), { recursive: true });
754 | await fs.mkdir(path.join(docsDir, "how-to"), { recursive: true });
755 | await fs.mkdir(path.join(docsDir, "reference"), { recursive: true });
756 | await fs.mkdir(path.join(docsDir, "explanation"), { recursive: true });
757 |
758 | await fs.writeFile(
759 | path.join(docsDir, "tutorials", "tutorial.md"),
760 | "# Tutorial",
761 | );
762 | await fs.writeFile(
763 | path.join(docsDir, "how-to", "guide.md"),
764 | "# How-to Guide",
765 | );
766 | await fs.writeFile(
767 | path.join(docsDir, "reference", "api.md"),
768 | "# API Reference",
769 | );
770 | await fs.writeFile(
771 | path.join(docsDir, "explanation", "concept.md"),
772 | "# Explanation",
773 | );
774 |
775 | const result = await handleValidateDiataxisContent({
776 | contentPath: docsDir,
777 | validationType: "compliance",
778 | includeCodeValidation: false,
779 | });
780 |
781 | expect(result).toBeDefined();
782 | expect(
783 | result.confidence.breakdown.architecturalAssumptions,
784 | ).toBeGreaterThan(60);
785 | });
786 |
787 | it("should handle successful validation with no issues", async () => {
788 | const cleanDir = path.join(testTempDir, "clean-validation");
789 | await fs.mkdir(cleanDir, { recursive: true });
790 |
791 | // Create clean content with no issues
792 | await fs.writeFile(
793 | path.join(cleanDir, "clean.md"),
794 | `
795 | # Clean Documentation
796 |
797 | This is well-structured documentation with no issues.
798 |
799 | \`\`\`json
800 | { "valid": "json" }
801 | \`\`\`
802 | `.trim(),
803 | );
804 |
805 | const result = await handleValidateDiataxisContent({
806 | contentPath: cleanDir,
807 | validationType: "all",
808 | includeCodeValidation: true,
809 | });
810 |
811 | // Should have minimal issues and good confidence
812 | expect(result.confidence.overall).toBeGreaterThan(0);
813 | expect(result.recommendations).toBeDefined();
814 | expect(result.recommendations.length).toBeGreaterThan(0);
815 | });
816 |
817 | it("should handle timeout scenarios", async () => {
818 | // Test timeout handling by creating a scenario that might take time
819 | const largeDir = path.join(testTempDir, "timeout-test");
820 | await fs.mkdir(largeDir, { recursive: true });
821 |
822 | // Create multiple markdown files to simulate processing time
823 | for (let i = 0; i < 5; i++) {
824 | await fs.writeFile(
825 | path.join(largeDir, `file${i}.md`),
826 | `
827 | # File ${i}
828 |
829 | Content for file ${i} with some text.
830 |
831 | \`\`\`javascript
832 | console.log("File ${i}");
833 | \`\`\`
834 | `.trim(),
835 | );
836 | }
837 |
838 | const result = await handleValidateDiataxisContent({
839 | contentPath: largeDir,
840 | validationType: "all",
841 | includeCodeValidation: true,
842 | });
843 |
844 | expect(result).toBeDefined();
845 | expect(result.confidence).toBeDefined();
846 | });
847 |
848 | it("should handle confidence levels and validation modes", async () => {
849 | const testDir = path.join(testTempDir, "confidence-levels");
850 | await fs.mkdir(testDir, { recursive: true });
851 | await fs.writeFile(path.join(testDir, "test.md"), "# Test Content");
852 |
853 | // Test different confidence levels
854 | const strictResult = await handleValidateDiataxisContent({
855 | contentPath: testDir,
856 | validationType: "all",
857 | includeCodeValidation: false,
858 | confidence: "strict",
859 | });
860 | expect(strictResult).toBeDefined();
861 |
862 | const moderateResult = await handleValidateDiataxisContent({
863 | contentPath: testDir,
864 | validationType: "all",
865 | includeCodeValidation: false,
866 | confidence: "moderate",
867 | });
868 | expect(moderateResult).toBeDefined();
869 |
870 | const permissiveResult = await handleValidateDiataxisContent({
871 | contentPath: testDir,
872 | validationType: "all",
873 | includeCodeValidation: false,
874 | confidence: "permissive",
875 | });
876 | expect(permissiveResult).toBeDefined();
877 | });
878 |
879 | it("should handle TypeScript files without package.json", async () => {
880 | const tsDir = path.join(testTempDir, "typescript-only");
881 | await fs.mkdir(tsDir, { recursive: true });
882 | await fs.mkdir(path.join(tsDir, "src"), { recursive: true });
883 |
884 | // Create TypeScript files without package.json
885 | await fs.writeFile(
886 | path.join(tsDir, "src", "app.ts"),
887 | `
888 | export class TestClass {
889 | public method(): void {
890 | console.log('test');
891 | }
892 | }
893 | `.trim(),
894 | );
895 |
896 | const result = await handleValidateDiataxisContent({
897 | contentPath: tsDir,
898 | validationType: "compliance",
899 | includeCodeValidation: false,
900 | });
901 |
902 | expect(result).toBeDefined();
903 | expect(result.confidence).toBeDefined();
904 | });
905 |
906 | it("should handle mixed content scenarios", async () => {
907 | const mixedDir = path.join(testTempDir, "mixed-content");
908 | await fs.mkdir(mixedDir, { recursive: true });
909 | await fs.mkdir(path.join(mixedDir, "src"), { recursive: true });
910 |
911 | // Create both application and documentation content
912 | await fs.writeFile(
913 | path.join(mixedDir, "package.json"),
914 | '{"name": "mixed-app"}',
915 | );
916 | await fs.writeFile(
917 | path.join(mixedDir, "src", "index.ts"),
918 | 'export const app = "test";',
919 | );
920 | await fs.writeFile(
921 | path.join(mixedDir, "README.md"),
922 | `
923 | # Mixed Content App
924 |
925 | ## Installation
926 |
927 | Run \`npm install\`
928 |
929 | ## Usage
930 |
931 | See the documentation.
932 | `.trim(),
933 | );
934 |
935 | const result = await handleValidateDiataxisContent({
936 | contentPath: mixedDir,
937 | validationType: "all",
938 | includeCodeValidation: true,
939 | });
940 |
941 | expect(result).toBeDefined();
942 | expect(
943 | result.confidence.breakdown.architecturalAssumptions,
944 | ).toBeGreaterThanOrEqual(60);
945 | });
946 |
947 | it("should handle business context alignment scoring", async () => {
948 | const businessDir = path.join(testTempDir, "business-context");
949 | await fs.mkdir(businessDir, { recursive: true });
950 |
951 | // Create content with business context
952 | await fs.writeFile(
953 | path.join(businessDir, "business.md"),
954 | `
955 | # Business Requirements
956 |
957 | This application serves enterprise customers with specific needs.
958 | The solution addresses market requirements and business objectives.
959 | `.trim(),
960 | );
961 |
962 | const result = await handleValidateDiataxisContent({
963 | contentPath: businessDir,
964 | validationType: "all",
965 | includeCodeValidation: false,
966 | });
967 |
968 | expect(result).toBeDefined();
969 | expect(
970 | result.confidence.breakdown.businessContextAlignment,
971 | ).toBeGreaterThanOrEqual(0);
972 | });
973 |
974 | it("should handle deprecated patterns in technical accuracy checks", async () => {
975 | const deprecatedDir = path.join(testTempDir, "deprecated-patterns");
976 | await fs.mkdir(deprecatedDir, { recursive: true });
977 |
978 | await fs.writeFile(
979 | path.join(deprecatedDir, "deprecated.md"),
980 | `
981 | # Deprecated Patterns
982 |
983 | \`\`\`bash
984 | npm install -g some-package
985 | \`\`\`
986 |
987 | \`\`\`javascript
988 | var oldVariable = "test";
989 | function() {
990 | console.log("old style");
991 | }
992 | \`\`\`
993 |
994 | Visit http://example.com for more info.
995 | `.trim(),
996 | );
997 |
998 | const result = await handleValidateDiataxisContent({
999 | contentPath: deprecatedDir,
1000 | validationType: "accuracy",
1001 | includeCodeValidation: false,
1002 | });
1003 |
1004 | const deprecatedIssues = result.issues.filter((issue) =>
1005 | issue.description.includes("Potentially outdated pattern"),
1006 | );
1007 | expect(deprecatedIssues.length).toBeGreaterThan(0);
1008 | });
1009 |
1010 | it("should handle async code without error handling", async () => {
1011 | const asyncDir = path.join(testTempDir, "async-code");
1012 | await fs.mkdir(asyncDir, { recursive: true });
1013 |
1014 | await fs.writeFile(
1015 | path.join(asyncDir, "async.md"),
1016 | `
1017 | # Async Code Examples
1018 |
1019 | \`\`\`javascript
1020 | async function fetchData() {
1021 | const response = await fetch('/api/data');
1022 | return response.json();
1023 | }
1024 | \`\`\`
1025 |
1026 | \`\`\`typescript
1027 | const getData = async (): Promise<any> => {
1028 | const result = await someAsyncOperation();
1029 | return result;
1030 | };
1031 | \`\`\`
1032 | `.trim(),
1033 | );
1034 |
1035 | const result = await handleValidateDiataxisContent({
1036 | contentPath: asyncDir,
1037 | validationType: "accuracy",
1038 | includeCodeValidation: false,
1039 | });
1040 |
1041 | const asyncIssues = result.issues.filter((issue) =>
1042 | issue.description.includes("Async code without error handling"),
1043 | );
1044 | expect(asyncIssues.length).toBeGreaterThan(0);
1045 | });
1046 |
1047 | it("should handle version compatibility checks with project context", async () => {
1048 | const versionDir = path.join(testTempDir, "version-compat");
1049 | await fs.mkdir(versionDir, { recursive: true });
1050 |
1051 | // Create .documcp directory with analysis
1052 | const docucmpDir = path.join(versionDir, ".documcp", "analyses");
1053 | await fs.mkdir(docucmpDir, { recursive: true });
1054 | await fs.writeFile(
1055 | path.join(docucmpDir, "version-analysis.json"),
1056 | JSON.stringify({
1057 | metadata: {
1058 | projectName: "version-test",
1059 | primaryLanguage: "TypeScript",
1060 | },
1061 | technologies: { framework: "React" },
1062 | dependencies: { packages: ["[email protected]", "[email protected]"] },
1063 | }),
1064 | );
1065 |
1066 | await fs.writeFile(
1067 | path.join(versionDir, "versions.md"),
1068 | `
1069 | # Version Information
1070 |
1071 | This project uses React @18.2.0 and TypeScript @4.9.0.
1072 | Also compatible with Node.js @16.14.0.
1073 | `.trim(),
1074 | );
1075 |
1076 | const result = await handleValidateDiataxisContent({
1077 | contentPath: versionDir,
1078 | analysisId: "version-analysis",
1079 | validationType: "accuracy",
1080 | includeCodeValidation: false,
1081 | });
1082 |
1083 | const versionUncertainties = result.uncertainties.filter(
1084 | (u) => u.area === "version-compatibility",
1085 | );
1086 | expect(versionUncertainties.length).toBeGreaterThan(0);
1087 | });
1088 |
1089 | it("should handle dangerous bash commands", async () => {
1090 | const bashDir = path.join(testTempDir, "dangerous-bash");
1091 | await fs.mkdir(bashDir, { recursive: true });
1092 |
1093 | await fs.writeFile(
1094 | path.join(bashDir, "dangerous.md"),
1095 | `
1096 | # Dangerous Commands
1097 |
1098 | \`\`\`bash
1099 | rm -rf /
1100 | sudo rm -rf /tmp/important
1101 | chmod 777 /etc/passwd
1102 | command > /dev/null 2>&1
1103 | \`\`\`
1104 | `.trim(),
1105 | );
1106 |
1107 | const result = await handleValidateDiataxisContent({
1108 | contentPath: bashDir,
1109 | validationType: "accuracy",
1110 | includeCodeValidation: false,
1111 | });
1112 |
1113 | const dangerousIssues = result.issues.filter((issue) =>
1114 | issue.description.includes("Potentially dangerous command"),
1115 | );
1116 | expect(dangerousIssues.length).toBeGreaterThan(0);
1117 | });
1118 |
1119 | it("should handle mixed path separators in commands", async () => {
1120 | const pathDir = path.join(testTempDir, "mixed-paths");
1121 | await fs.mkdir(pathDir, { recursive: true });
1122 |
1123 | await fs.writeFile(
1124 | path.join(pathDir, "paths.md"),
1125 | `
1126 | # Mixed Path Examples
1127 |
1128 | \`\`\`bash
1129 | cp /unix/path\\windows\\mixed /destination/path
1130 | \`\`\`
1131 | `.trim(),
1132 | );
1133 |
1134 | const result = await handleValidateDiataxisContent({
1135 | contentPath: pathDir,
1136 | validationType: "accuracy",
1137 | includeCodeValidation: false,
1138 | });
1139 |
1140 | const pathIssues = result.issues.filter((issue) =>
1141 | issue.description.includes("Mixed path separators"),
1142 | );
1143 | expect(pathIssues.length).toBeGreaterThan(0);
1144 | });
1145 |
1146 | it("should handle external links in accuracy validation", async () => {
1147 | const linksDir = path.join(testTempDir, "external-links");
1148 | await fs.mkdir(linksDir, { recursive: true });
1149 |
1150 | await fs.writeFile(
1151 | path.join(linksDir, "external.md"),
1152 | `
1153 | # External Links
1154 |
1155 | [GitHub](https://github.com)
1156 | [Documentation](https://docs.example.com)
1157 | `.trim(),
1158 | );
1159 |
1160 | const result = await handleValidateDiataxisContent({
1161 | contentPath: linksDir,
1162 | validationType: "accuracy",
1163 | includeCodeValidation: false,
1164 | });
1165 |
1166 | const linkUncertainties = result.uncertainties.filter(
1167 | (u) => u.area === "external-links",
1168 | );
1169 | expect(linkUncertainties.length).toBeGreaterThan(0);
1170 | });
1171 |
1172 | it("should handle Diataxis compliance rules for different sections", async () => {
1173 | const complianceDir = path.join(testTempDir, "diataxis-compliance");
1174 | await fs.mkdir(complianceDir, { recursive: true });
1175 |
1176 | // Create directories for each Diataxis section
1177 | await fs.mkdir(path.join(complianceDir, "tutorials"), {
1178 | recursive: true,
1179 | });
1180 | await fs.mkdir(path.join(complianceDir, "how-to"), { recursive: true });
1181 | await fs.mkdir(path.join(complianceDir, "reference"), {
1182 | recursive: true,
1183 | });
1184 | await fs.mkdir(path.join(complianceDir, "explanation"), {
1185 | recursive: true,
1186 | });
1187 |
1188 | // Tutorial without prerequisites
1189 | await fs.writeFile(
1190 | path.join(complianceDir, "tutorials", "bad-tutorial.md"),
1191 | `
1192 | # Bad Tutorial
1193 |
1194 | This tutorial doesn't have prerequisites or clear steps.
1195 | `.trim(),
1196 | );
1197 |
1198 | // How-to without task focus
1199 | await fs.writeFile(
1200 | path.join(complianceDir, "how-to", "bad-howto.md"),
1201 | `
1202 | # Bad Guide
1203 |
1204 | Short guide.
1205 | `.trim(),
1206 | );
1207 |
1208 | // Reference without structure
1209 | await fs.writeFile(
1210 | path.join(complianceDir, "reference", "bad-reference.md"),
1211 | `
1212 | Bad reference without headings or tables.
1213 | `.trim(),
1214 | );
1215 |
1216 | // Explanation without "why"
1217 | await fs.writeFile(
1218 | path.join(complianceDir, "explanation", "bad-explanation.md"),
1219 | `
1220 | # Bad Explanation
1221 |
1222 | Short explanation.
1223 | `.trim(),
1224 | );
1225 |
1226 | const result = await handleValidateDiataxisContent({
1227 | contentPath: complianceDir,
1228 | validationType: "compliance",
1229 | includeCodeValidation: false,
1230 | });
1231 |
1232 | const complianceIssues = result.issues.filter(
1233 | (issue) => issue.category === "compliance",
1234 | );
1235 | expect(complianceIssues.length).toBeGreaterThan(4); // Should find issues in each section
1236 | });
1237 |
1238 | it("should handle TypeScript code validation with compilation errors", async () => {
1239 | const tsDir = path.join(testTempDir, "typescript-validation");
1240 | await fs.mkdir(tsDir, { recursive: true });
1241 |
1242 | await fs.writeFile(
1243 | path.join(tsDir, "typescript.md"),
1244 | `
1245 | # TypeScript Examples
1246 |
1247 | \`\`\`typescript
1248 | // This has type errors
1249 | let x: string = 123;
1250 | function badFunction(param: number): string {
1251 | return param; // Type error
1252 | }
1253 | \`\`\`
1254 | `.trim(),
1255 | );
1256 |
1257 | const result = await handleValidateDiataxisContent({
1258 | contentPath: tsDir,
1259 | validationType: "all",
1260 | includeCodeValidation: true,
1261 | });
1262 |
1263 | expect(result.codeValidation).toBeDefined();
1264 | expect(result.codeValidation!.overallSuccess).toBe(false);
1265 | });
1266 |
1267 | it("should handle bash code validation with complex chaining", async () => {
1268 | const bashComplexDir = path.join(testTempDir, "bash-complex");
1269 | await fs.mkdir(bashComplexDir, { recursive: true });
1270 |
1271 | await fs.writeFile(
1272 | path.join(bashComplexDir, "complex-bash.md"),
1273 | `
1274 | # Complex Bash
1275 |
1276 | \`\`\`bash
1277 | # Complex command chaining
1278 | command1 && command2 || command3
1279 | rm $VARIABLE
1280 | \`\`\`
1281 | `.trim(),
1282 | );
1283 |
1284 | const result = await handleValidateDiataxisContent({
1285 | contentPath: bashComplexDir,
1286 | validationType: "all",
1287 | includeCodeValidation: true,
1288 | });
1289 |
1290 | expect(result.codeValidation).toBeDefined();
1291 | const bashIssues = result.codeValidation!.exampleResults.flatMap(
1292 | (ex) => ex.issues,
1293 | );
1294 | expect(bashIssues.length).toBeGreaterThan(0);
1295 | });
1296 |
1297 | it("should handle file limit reached scenario", async () => {
1298 | const largeDir = path.join(testTempDir, "large-directory");
1299 | await fs.mkdir(largeDir, { recursive: true });
1300 |
1301 | // Create many markdown files to test file limit
1302 | for (let i = 0; i < 10; i++) {
1303 | await fs.writeFile(
1304 | path.join(largeDir, `file${i}.md`),
1305 | `# File ${i}\nContent for file ${i}.`,
1306 | );
1307 | }
1308 |
1309 | const result = await handleValidateDiataxisContent({
1310 | contentPath: largeDir,
1311 | validationType: "all",
1312 | includeCodeValidation: false,
1313 | });
1314 |
1315 | expect(result).toBeDefined();
1316 | expect(
1317 | result.confidence.breakdown.architecturalAssumptions,
1318 | ).toBeGreaterThan(60);
1319 | });
1320 |
1321 | it("should handle symlink detection in file scanning", async () => {
1322 | const symlinkDir = path.join(testTempDir, "symlink-test");
1323 | await fs.mkdir(symlinkDir, { recursive: true });
1324 |
1325 | // Create a regular file
1326 | await fs.writeFile(path.join(symlinkDir, "regular.md"), "# Regular File");
1327 |
1328 | // Create a subdirectory
1329 | await fs.mkdir(path.join(symlinkDir, "subdir"), { recursive: true });
1330 | await fs.writeFile(
1331 | path.join(symlinkDir, "subdir", "nested.md"),
1332 | "# Nested File",
1333 | );
1334 |
1335 | const result = await handleValidateDiataxisContent({
1336 | contentPath: symlinkDir,
1337 | validationType: "all",
1338 | includeCodeValidation: false,
1339 | });
1340 |
1341 | expect(result).toBeDefined();
1342 | expect(
1343 | result.confidence.breakdown.architecturalAssumptions,
1344 | ).toBeGreaterThanOrEqual(60);
1345 | });
1346 |
1347 | it("should handle timeout scenario", async () => {
1348 | const timeoutDir = path.join(testTempDir, "timeout-scenario");
1349 | await fs.mkdir(timeoutDir, { recursive: true });
1350 | await fs.writeFile(path.join(timeoutDir, "test.md"), "# Test");
1351 |
1352 | // Mock a timeout by creating a very short timeout
1353 | const originalTimeout = 120000;
1354 |
1355 | const result = await handleValidateDiataxisContent({
1356 | contentPath: timeoutDir,
1357 | validationType: "all",
1358 | includeCodeValidation: false,
1359 | });
1360 |
1361 | expect(result).toBeDefined();
1362 | });
1363 |
1364 | it("should handle general content validation with external links", async () => {
1365 | const generalDir = path.join(testTempDir, "general-external");
1366 | await fs.mkdir(generalDir, { recursive: true });
1367 |
1368 | await fs.writeFile(
1369 | path.join(generalDir, "external.md"),
1370 | `
1371 | # External Links Test
1372 |
1373 | [GitHub](https://github.com)
1374 | [Local](./local.md)
1375 | `.trim(),
1376 | );
1377 |
1378 | await fs.writeFile(path.join(generalDir, "local.md"), "# Local File");
1379 |
1380 | const result = await validateGeneralContent({
1381 | contentPath: generalDir,
1382 | validationType: "all",
1383 | includeCodeValidation: true,
1384 | followExternalLinks: false,
1385 | });
1386 |
1387 | expect(result.linksChecked).toBeGreaterThan(0);
1388 | expect(result.success).toBe(true);
1389 | });
1390 |
1391 | it("should handle general content validation with code validation", async () => {
1392 | const codeDir = path.join(testTempDir, "general-code");
1393 | await fs.mkdir(codeDir, { recursive: true });
1394 |
1395 | await fs.writeFile(
1396 | path.join(codeDir, "code.md"),
1397 | `
1398 | # Code Test
1399 |
1400 | \`\`\`javascript
1401 | console.log("test")
1402 | \`\`\`
1403 |
1404 | \`\`\`js
1405 | console.log("another test");
1406 | \`\`\`
1407 | `.trim(),
1408 | );
1409 |
1410 | const result = await validateGeneralContent({
1411 | contentPath: codeDir,
1412 | validationType: "code",
1413 | includeCodeValidation: true,
1414 | });
1415 |
1416 | expect(result.codeBlocksValidated).toBeGreaterThan(0);
1417 | expect(result.codeErrors.length).toBeGreaterThan(0); // Missing semicolon
1418 | });
1419 |
1420 | it("should handle validation with no code blocks", async () => {
1421 | const noCodeDir = path.join(testTempDir, "no-code");
1422 | await fs.mkdir(noCodeDir, { recursive: true });
1423 |
1424 | await fs.writeFile(
1425 | path.join(noCodeDir, "text.md"),
1426 | `
1427 | # Text Only
1428 |
1429 | This is just text with no code blocks.
1430 | `.trim(),
1431 | );
1432 |
1433 | const result = await validateGeneralContent({
1434 | contentPath: noCodeDir,
1435 | validationType: "all",
1436 | includeCodeValidation: true,
1437 | });
1438 |
1439 | expect(result.codeBlocksValidated).toBe(0);
1440 | expect(result.success).toBe(true);
1441 | });
1442 | });
1443 | });
1444 |
```
--------------------------------------------------------------------------------
/tests/tools/sync-code-to-docs.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Sync Code to Docs Tool Tests (Phase 3)
3 | */
4 |
5 | import { handleSyncCodeToDocs } from "../../src/tools/sync-code-to-docs.js";
6 | import { promises as fs } from "fs";
7 | import { tmpdir } from "os";
8 | import { join } from "path";
9 | import { mkdtemp, rm } from "fs/promises";
10 | import { DriftDetector } from "../../src/utils/drift-detector.js";
11 |
12 | describe("sync_code_to_docs tool", () => {
13 | let tempDir: string;
14 | let projectPath: string;
15 | let docsPath: string;
16 |
17 | beforeEach(async () => {
18 | tempDir = await mkdtemp(join(tmpdir(), "sync-test-"));
19 | projectPath = join(tempDir, "project");
20 | docsPath = join(tempDir, "docs");
21 |
22 | await fs.mkdir(join(projectPath, "src"), { recursive: true });
23 | await fs.mkdir(docsPath, { recursive: true });
24 | });
25 |
26 | afterEach(async () => {
27 | await rm(tempDir, { recursive: true, force: true });
28 | });
29 |
30 | describe("Detect Mode", () => {
31 | test("should detect drift without making changes", async () => {
32 | // Create source file
33 | const sourceCode = `
34 | export function calculate(x: number): number {
35 | return x * 2;
36 | }
37 | `.trim();
38 |
39 | await fs.writeFile(join(projectPath, "src", "calc.ts"), sourceCode);
40 |
41 | // Create documentation
42 | const docContent = `
43 | # Calculator
44 |
45 | ## calculate(x: number): number
46 |
47 | Doubles the input.
48 | `.trim();
49 |
50 | await fs.writeFile(join(docsPath, "calc.md"), docContent);
51 |
52 | // Run in detect mode
53 | const result = await handleSyncCodeToDocs({
54 | projectPath,
55 | docsPath,
56 | mode: "detect",
57 | createSnapshot: true,
58 | });
59 |
60 | expect(result).toBeDefined();
61 | expect(result.content).toBeDefined();
62 | expect(result.content[0]).toBeDefined();
63 |
64 | const data = JSON.parse(result.content[0].text);
65 | expect(data.success).toBe(true);
66 | expect(data.data.mode).toBe("detect");
67 |
68 | // Verify no changes were made
69 | const docAfter = await fs.readFile(join(docsPath, "calc.md"), "utf-8");
70 | expect(docAfter).toBe(docContent);
71 | });
72 |
73 | test("should create baseline snapshot on first run", async () => {
74 | const sourceCode = `export function test(): void {}`;
75 | await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode);
76 |
77 | const result = await handleSyncCodeToDocs({
78 | projectPath,
79 | docsPath,
80 | mode: "detect",
81 | createSnapshot: true,
82 | });
83 |
84 | expect(result).toBeDefined();
85 |
86 | const data = JSON.parse(result.content[0].text);
87 | expect(data.success).toBe(true);
88 | expect(data.data.snapshotId).toBeTruthy();
89 |
90 | // Check snapshot was created
91 | const snapshotDir = join(tempDir, "project", ".documcp", "snapshots");
92 | const files = await fs.readdir(snapshotDir);
93 | expect(files.length).toBeGreaterThan(0);
94 | });
95 |
96 | test("should report drift statistics", async () => {
97 | // Create initial snapshot
98 | const oldCode = `
99 | export function oldFunction(): void {}
100 | `.trim();
101 |
102 | await fs.writeFile(join(projectPath, "src", "changes.ts"), oldCode);
103 |
104 | await handleSyncCodeToDocs({
105 | projectPath,
106 | docsPath,
107 | mode: "detect",
108 | createSnapshot: true,
109 | });
110 |
111 | // Make changes
112 | const newCode = `
113 | export function newFunction(): void {}
114 | `.trim();
115 |
116 | await fs.writeFile(join(projectPath, "src", "changes.ts"), newCode);
117 |
118 | // Detect drift
119 | const result = await handleSyncCodeToDocs({
120 | projectPath,
121 | docsPath,
122 | mode: "detect",
123 | createSnapshot: true,
124 | });
125 |
126 | const data = JSON.parse(result.content[0].text);
127 | expect(data.success).toBe(true);
128 | expect(data.data.stats).toBeDefined();
129 | expect(data.data.stats.filesAnalyzed).toBeGreaterThanOrEqual(0);
130 | });
131 | });
132 |
133 | describe("Apply Mode", () => {
134 | test("should apply high-confidence changes automatically", async () => {
135 | // Create code with JSDoc
136 | const sourceCode = `
137 | /**
138 | * Calculates the sum of two numbers
139 | * @param a First number
140 | * @param b Second number
141 | * @returns The sum
142 | */
143 | export function add(a: number, b: number): number {
144 | return a + b;
145 | }
146 | `.trim();
147 |
148 | await fs.writeFile(join(projectPath, "src", "math.ts"), sourceCode);
149 |
150 | // Create minimal documentation
151 | const docContent = `
152 | # Math Module
153 |
154 | Documentation needed.
155 | `.trim();
156 |
157 | await fs.writeFile(join(docsPath, "math.md"), docContent);
158 |
159 | // Create baseline
160 | await handleSyncCodeToDocs({
161 | projectPath,
162 | docsPath,
163 | mode: "detect",
164 | createSnapshot: true,
165 | });
166 |
167 | // Run in apply mode with high threshold
168 | const result = await handleSyncCodeToDocs({
169 | projectPath,
170 | docsPath,
171 | mode: "apply",
172 | autoApplyThreshold: 0.9,
173 | createSnapshot: true,
174 | });
175 |
176 | const data = JSON.parse(result.content[0].text);
177 | expect(data.success).toBe(true);
178 | expect(data.data.mode).toBe("apply");
179 |
180 | // Stats should show applied or pending changes
181 | const stats = data.data.stats;
182 | expect(
183 | stats.changesApplied + stats.changesPending,
184 | ).toBeGreaterThanOrEqual(0);
185 | });
186 |
187 | test("should respect confidence threshold", async () => {
188 | // Setup code and docs
189 | const sourceCode = `export function test(): void {}`;
190 | await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode);
191 |
192 | const docContent = `# Test`;
193 | await fs.writeFile(join(docsPath, "test.md"), docContent);
194 |
195 | // Create baseline
196 | await handleSyncCodeToDocs({
197 | projectPath,
198 | docsPath,
199 | mode: "detect",
200 | });
201 |
202 | // Apply with very high threshold (most changes won't meet it)
203 | const result = await handleSyncCodeToDocs({
204 | projectPath,
205 | docsPath,
206 | mode: "apply",
207 | autoApplyThreshold: 0.99,
208 | });
209 |
210 | const data = JSON.parse(result.content[0].text);
211 | expect(data.success).toBe(true);
212 |
213 | // With high threshold, most changes should be pending
214 | if (data.data.stats.driftsDetected > 0) {
215 | expect(data.data.pendingChanges.length).toBeGreaterThanOrEqual(0);
216 | }
217 | });
218 |
219 | test("should create snapshot before applying changes", async () => {
220 | const sourceCode = `export function test(): void {}`;
221 | await fs.writeFile(
222 | join(projectPath, "src", "snapshot-test.ts"),
223 | sourceCode,
224 | );
225 |
226 | await handleSyncCodeToDocs({
227 | projectPath,
228 | docsPath,
229 | mode: "apply",
230 | createSnapshot: true,
231 | });
232 |
233 | // Verify snapshot exists
234 | const snapshotDir = join(projectPath, ".documcp", "snapshots");
235 | const files = await fs.readdir(snapshotDir);
236 | expect(files.length).toBeGreaterThan(0);
237 | });
238 | });
239 |
240 | describe("Auto Mode", () => {
241 | test("should apply all changes in auto mode", async () => {
242 | const sourceCode = `
243 | export function autoFunction(param: string): string {
244 | return param.toUpperCase();
245 | }
246 | `.trim();
247 |
248 | await fs.writeFile(join(projectPath, "src", "auto.ts"), sourceCode);
249 |
250 | const result = await handleSyncCodeToDocs({
251 | projectPath,
252 | docsPath,
253 | mode: "auto",
254 | createSnapshot: true,
255 | });
256 |
257 | const data = JSON.parse(result.content[0].text);
258 | expect(data.success).toBe(true);
259 | expect(data.data.mode).toBe("auto");
260 | });
261 | });
262 |
263 | describe("Error Handling", () => {
264 | test("should handle invalid project path", async () => {
265 | const result = await handleSyncCodeToDocs({
266 | projectPath: "/nonexistent/path",
267 | docsPath,
268 | mode: "detect",
269 | });
270 |
271 | expect(result).toBeDefined();
272 | expect(result.content).toBeDefined();
273 |
274 | const data = JSON.parse(result.content[0].text);
275 | // Should either fail gracefully or handle missing path
276 | expect(data).toBeDefined();
277 | });
278 |
279 | test("should handle invalid docs path", async () => {
280 | const sourceCode = `export function test(): void {}`;
281 | await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode);
282 |
283 | const result = await handleSyncCodeToDocs({
284 | projectPath,
285 | docsPath: "/nonexistent/docs",
286 | mode: "detect",
287 | });
288 |
289 | expect(result).toBeDefined();
290 | const data = JSON.parse(result.content[0].text);
291 | expect(data).toBeDefined();
292 | });
293 |
294 | test("should handle empty project", async () => {
295 | // Empty project directory
296 | const result = await handleSyncCodeToDocs({
297 | projectPath,
298 | docsPath,
299 | mode: "detect",
300 | });
301 |
302 | expect(result).toBeDefined();
303 | const data = JSON.parse(result.content[0].text);
304 | expect(data.success).toBe(true);
305 | expect(data.data.stats.filesAnalyzed).toBe(0);
306 | });
307 | });
308 |
309 | describe("Recommendations and Next Steps", () => {
310 | test("should provide recommendations based on results", async () => {
311 | const sourceCode = `
312 | export function critical(param: number): void {}
313 | `.trim();
314 |
315 | await fs.writeFile(join(projectPath, "src", "critical.ts"), sourceCode);
316 |
317 | // Create baseline
318 | await handleSyncCodeToDocs({
319 | projectPath,
320 | docsPath,
321 | mode: "detect",
322 | });
323 |
324 | // Make breaking change
325 | const newCode = `
326 | export function critical(param: string, extra: boolean): void {}
327 | `.trim();
328 |
329 | await fs.writeFile(join(projectPath, "src", "critical.ts"), newCode);
330 |
331 | // Detect changes
332 | const result = await handleSyncCodeToDocs({
333 | projectPath,
334 | docsPath,
335 | mode: "detect",
336 | });
337 |
338 | const data = JSON.parse(result.content[0].text);
339 | expect(data.success).toBe(true);
340 | expect(data.recommendations).toBeDefined();
341 | expect(Array.isArray(data.recommendations)).toBe(true);
342 | });
343 |
344 | test("should provide next steps", async () => {
345 | const result = await handleSyncCodeToDocs({
346 | projectPath,
347 | docsPath,
348 | mode: "detect",
349 | });
350 |
351 | const data = JSON.parse(result.content[0].text);
352 | expect(data.success).toBe(true);
353 | expect(data.nextSteps).toBeDefined();
354 | expect(Array.isArray(data.nextSteps)).toBe(true);
355 | });
356 | });
357 |
358 | describe("Integration with Knowledge Graph", () => {
359 | test("should store sync events", async () => {
360 | const sourceCode = `export function kgTest(): void {}`;
361 | await fs.writeFile(join(projectPath, "src", "kg-test.ts"), sourceCode);
362 |
363 | const result = await handleSyncCodeToDocs({
364 | projectPath,
365 | docsPath,
366 | mode: "detect",
367 | });
368 |
369 | const data = JSON.parse(result.content[0].text);
370 | expect(data.success).toBe(true);
371 |
372 | // Sync event should be created (even if storage fails, shouldn't error)
373 | expect(data.data).toBeDefined();
374 | });
375 | });
376 |
377 | describe("Preview Mode", () => {
378 | test("should show changes in preview mode without applying", async () => {
379 | const sourceCode = `
380 | export function previewFunc(x: number): number {
381 | return x * 3;
382 | }
383 | `.trim();
384 |
385 | await fs.writeFile(join(projectPath, "src", "preview.ts"), sourceCode);
386 |
387 | const docContent = `
388 | # Preview
389 |
390 | Old documentation.
391 | `.trim();
392 |
393 | await fs.writeFile(join(docsPath, "preview.md"), docContent);
394 |
395 | // Create baseline
396 | await handleSyncCodeToDocs({
397 | projectPath,
398 | docsPath,
399 | mode: "detect",
400 | });
401 |
402 | // Change code
403 | const newCode = `
404 | export function previewFunc(x: number, y: number): number {
405 | return x * y;
406 | }
407 | `.trim();
408 |
409 | await fs.writeFile(join(projectPath, "src", "preview.ts"), newCode);
410 |
411 | // Preview changes
412 | const result = await handleSyncCodeToDocs({
413 | projectPath,
414 | docsPath,
415 | mode: "preview",
416 | });
417 |
418 | const data = JSON.parse(result.content[0].text);
419 | expect(data.success).toBe(true);
420 | expect(data.data.mode).toBe("preview");
421 |
422 | // Verify documentation wasn't changed
423 | const docAfter = await fs.readFile(join(docsPath, "preview.md"), "utf-8");
424 | expect(docAfter).toBe(docContent);
425 | });
426 | });
427 |
428 | describe("Documentation Change Application", () => {
429 | test("should apply changes when low-confidence changes exist in auto mode", async () => {
430 | // Create a source file with documentation
431 | const sourceCode = `
432 | /**
433 | * Multiplies two numbers together
434 | * @param x First number
435 | * @param y Second number
436 | */
437 | export function multiply(x: number, y: number): number {
438 | return x * y;
439 | }
440 | `.trim();
441 |
442 | await fs.writeFile(join(projectPath, "src", "math.ts"), sourceCode);
443 |
444 | // Create outdated documentation
445 | const docContent = `
446 | # Math Module
447 |
448 | ## multiply
449 |
450 | Adds two numbers.
451 | `.trim();
452 |
453 | await fs.writeFile(join(docsPath, "math.md"), docContent);
454 |
455 | // Create baseline
456 | await handleSyncCodeToDocs({
457 | projectPath,
458 | docsPath,
459 | mode: "detect",
460 | });
461 |
462 | // Run in auto mode (applies all changes)
463 | const result = await handleSyncCodeToDocs({
464 | projectPath,
465 | docsPath,
466 | mode: "auto",
467 | });
468 |
469 | const data = JSON.parse(result.content[0].text);
470 | expect(data.success).toBe(true);
471 | expect(data.data.mode).toBe("auto");
472 | });
473 |
474 | test("should handle apply errors gracefully", async () => {
475 | // Create source file
476 | const sourceCode = `export function testFunc(): void {}`;
477 | await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode);
478 |
479 | // Create documentation in a read-only parent directory would fail
480 | // But for this test, we'll just verify the error handling path exists
481 | const docContent = `# Test`;
482 | await fs.writeFile(join(docsPath, "test.md"), docContent);
483 |
484 | // Create baseline
485 | await handleSyncCodeToDocs({
486 | projectPath,
487 | docsPath,
488 | mode: "detect",
489 | });
490 |
491 | // Modify code
492 | const newCode = `export function testFunc(param: string): void {}`;
493 | await fs.writeFile(join(projectPath, "src", "test.ts"), newCode);
494 |
495 | // Try to apply changes
496 | const result = await handleSyncCodeToDocs({
497 | projectPath,
498 | docsPath,
499 | mode: "apply",
500 | autoApplyThreshold: 0.0, // Very low threshold
501 | });
502 |
503 | // Should complete without crashing
504 | expect(result).toBeDefined();
505 | const data = JSON.parse(result.content[0].text);
506 | expect(data.success).toBe(true);
507 | });
508 | });
509 |
510 | describe("Recommendation Edge Cases", () => {
511 | test("should recommend review for breaking changes", async () => {
512 | // Create initial code
513 | const oldCode = `
514 | export function oldApi(x: number): string {
515 | return x.toString();
516 | }
517 | `.trim();
518 |
519 | await fs.writeFile(join(projectPath, "src", "api.ts"), oldCode);
520 |
521 | // Create baseline
522 | await handleSyncCodeToDocs({
523 | projectPath,
524 | docsPath,
525 | mode: "detect",
526 | });
527 |
528 | // Make breaking change
529 | const newCode = `
530 | export function newApi(x: number, y: string): boolean {
531 | return x > 0;
532 | }
533 | `.trim();
534 |
535 | await fs.writeFile(join(projectPath, "src", "api.ts"), newCode);
536 |
537 | // Detect changes
538 | const result = await handleSyncCodeToDocs({
539 | projectPath,
540 | docsPath,
541 | mode: "detect",
542 | });
543 |
544 | const data = JSON.parse(result.content[0].text);
545 | expect(data.success).toBe(true);
546 |
547 | // Should have recommendations
548 | expect(data.recommendations).toBeDefined();
549 | expect(Array.isArray(data.recommendations)).toBe(true);
550 | });
551 |
552 | test("should show info when no drift detected", async () => {
553 | // Create code
554 | const sourceCode = `export function stable(): void {}`;
555 | await fs.writeFile(join(projectPath, "src", "stable.ts"), sourceCode);
556 |
557 | // Create baseline
558 | await handleSyncCodeToDocs({
559 | projectPath,
560 | docsPath,
561 | mode: "detect",
562 | });
563 |
564 | // Run again without changes
565 | const result = await handleSyncCodeToDocs({
566 | projectPath,
567 | docsPath,
568 | mode: "detect",
569 | });
570 |
571 | const data = JSON.parse(result.content[0].text);
572 | expect(data.success).toBe(true);
573 | expect(data.recommendations).toBeDefined();
574 |
575 | // Should have "No Drift Detected" recommendation
576 | const noDriftRec = data.recommendations.find(
577 | (r: any) => r.title?.includes("No Drift"),
578 | );
579 | expect(noDriftRec).toBeDefined();
580 | });
581 |
582 | test("should recommend validation after applying changes", async () => {
583 | const sourceCode = `
584 | /**
585 | * Test function
586 | */
587 | export function test(): void {}
588 | `.trim();
589 |
590 | await fs.writeFile(join(projectPath, "src", "validated.ts"), sourceCode);
591 |
592 | // Create baseline
593 | await handleSyncCodeToDocs({
594 | projectPath,
595 | docsPath,
596 | mode: "detect",
597 | });
598 |
599 | // Modify code
600 | const newCode = `
601 | /**
602 | * Modified test function
603 | */
604 | export function test(param: string): void {}
605 | `.trim();
606 |
607 | await fs.writeFile(join(projectPath, "src", "validated.ts"), newCode);
608 |
609 | // Apply changes
610 | const result = await handleSyncCodeToDocs({
611 | projectPath,
612 | docsPath,
613 | mode: "auto",
614 | });
615 |
616 | const data = JSON.parse(result.content[0].text);
617 | expect(data.success).toBe(true);
618 |
619 | // Should have next steps
620 | expect(data.nextSteps).toBeDefined();
621 | expect(Array.isArray(data.nextSteps)).toBe(true);
622 | });
623 | });
624 |
625 | describe("Next Steps Generation", () => {
626 | test("should suggest apply mode when in detect mode with pending changes", async () => {
627 | const sourceCode = `export function needsSync(): void {}`;
628 | await fs.writeFile(join(projectPath, "src", "sync.ts"), sourceCode);
629 |
630 | // Create baseline
631 | await handleSyncCodeToDocs({
632 | projectPath,
633 | docsPath,
634 | mode: "detect",
635 | });
636 |
637 | // Change code
638 | const newCode = `export function needsSync(param: number): void {}`;
639 | await fs.writeFile(join(projectPath, "src", "sync.ts"), newCode);
640 |
641 | // Detect in detect mode
642 | const result = await handleSyncCodeToDocs({
643 | projectPath,
644 | docsPath,
645 | mode: "detect",
646 | });
647 |
648 | const data = JSON.parse(result.content[0].text);
649 | expect(data.success).toBe(true);
650 | expect(data.nextSteps).toBeDefined();
651 |
652 | // If there are pending changes, should suggest apply mode
653 | if (data.data.pendingChanges?.length > 0) {
654 | const applyStep = data.nextSteps.find(
655 | (s: any) => s.action?.includes("Apply"),
656 | );
657 | expect(applyStep).toBeDefined();
658 | }
659 | });
660 |
661 | test("should suggest review for pending manual changes", async () => {
662 | const sourceCode = `export function complex(): void {}`;
663 | await fs.writeFile(join(projectPath, "src", "complex.ts"), sourceCode);
664 |
665 | // Create baseline
666 | await handleSyncCodeToDocs({
667 | projectPath,
668 | docsPath,
669 | mode: "detect",
670 | });
671 |
672 | // Change code
673 | const newCode = `export function complex(a: number, b: string): boolean { return true; }`;
674 | await fs.writeFile(join(projectPath, "src", "complex.ts"), newCode);
675 |
676 | // Detect with very high threshold (forces manual review)
677 | const result = await handleSyncCodeToDocs({
678 | projectPath,
679 | docsPath,
680 | mode: "apply",
681 | autoApplyThreshold: 0.99,
682 | });
683 |
684 | const data = JSON.parse(result.content[0].text);
685 | expect(data.success).toBe(true);
686 | expect(data.nextSteps).toBeDefined();
687 | });
688 | });
689 |
690 | describe("Snapshot Management", () => {
691 | test("should not create snapshot when createSnapshot is false in detect mode", async () => {
692 | const sourceCode = `export function noSnapshot(): void {}`;
693 | await fs.writeFile(join(projectPath, "src", "nosnapshot.ts"), sourceCode);
694 |
695 | await handleSyncCodeToDocs({
696 | projectPath,
697 | docsPath,
698 | mode: "detect",
699 | createSnapshot: false,
700 | });
701 |
702 | // Should still work even without snapshot
703 | const result = await handleSyncCodeToDocs({
704 | projectPath,
705 | docsPath,
706 | mode: "detect",
707 | createSnapshot: false,
708 | });
709 |
710 | const data = JSON.parse(result.content[0].text);
711 | expect(data.success).toBe(true);
712 | });
713 | });
714 |
715 | describe("Error Path Coverage", () => {
716 | test("should handle KG storage failures gracefully", async () => {
717 | const sourceCode = `export function kgError(): void {}`;
718 | await fs.writeFile(join(projectPath, "src", "kg-error.ts"), sourceCode);
719 |
720 | // The tool should complete even if KG storage fails
721 | const result = await handleSyncCodeToDocs({
722 | projectPath,
723 | docsPath,
724 | mode: "detect",
725 | });
726 |
727 | const data = JSON.parse(result.content[0].text);
728 | expect(data.success).toBe(true);
729 | });
730 |
731 | test("should handle zero drift detections", async () => {
732 | const sourceCode = `export function stable(): void {}`;
733 | await fs.writeFile(join(projectPath, "src", "stable.ts"), sourceCode);
734 |
735 | // Create baseline
736 | await handleSyncCodeToDocs({
737 | projectPath,
738 | docsPath,
739 | mode: "detect",
740 | });
741 |
742 | // Run again with no changes
743 | const result = await handleSyncCodeToDocs({
744 | projectPath,
745 | docsPath,
746 | mode: "detect",
747 | });
748 |
749 | const data = JSON.parse(result.content[0].text);
750 | expect(data.success).toBe(true);
751 | expect(data.data.stats.driftsDetected).toBe(0);
752 | });
753 |
754 | test("should handle files with no drift suggestions", async () => {
755 | const sourceCode = `export function noDrift(): void {}`;
756 | await fs.writeFile(join(projectPath, "src", "nodrift.ts"), sourceCode);
757 |
758 | const result = await handleSyncCodeToDocs({
759 | projectPath,
760 | docsPath,
761 | mode: "apply",
762 | autoApplyThreshold: 0.5,
763 | });
764 |
765 | const data = JSON.parse(result.content[0].text);
766 | expect(data.success).toBe(true);
767 | // Should handle case with no drift gracefully
768 | expect(data.data.appliedChanges).toBeDefined();
769 | expect(data.data.pendingChanges).toBeDefined();
770 | });
771 |
772 | test("should handle recommendations with zero breaking changes", async () => {
773 | const sourceCode = `export function minor(): void {}`;
774 | await fs.writeFile(join(projectPath, "src", "minor.ts"), sourceCode);
775 |
776 | const result = await handleSyncCodeToDocs({
777 | projectPath,
778 | docsPath,
779 | mode: "detect",
780 | });
781 |
782 | const data = JSON.parse(result.content[0].text);
783 | expect(data.success).toBe(true);
784 | // Should not have critical recommendations for no breaking changes
785 | const criticalRecs = data.recommendations?.filter(
786 | (r: any) => r.type === "critical",
787 | );
788 | expect(criticalRecs || []).toHaveLength(0);
789 | });
790 |
791 | test("should handle pending changes without manual review", async () => {
792 | const sourceCode = `export function autoApply(): void {}`;
793 | await fs.writeFile(join(projectPath, "src", "autoapply.ts"), sourceCode);
794 |
795 | // Create baseline
796 | await handleSyncCodeToDocs({
797 | projectPath,
798 | docsPath,
799 | mode: "detect",
800 | });
801 |
802 | // Modify
803 | const newCode = `export function autoApply(x: number): number { return x; }`;
804 | await fs.writeFile(join(projectPath, "src", "autoapply.ts"), newCode);
805 |
806 | const result = await handleSyncCodeToDocs({
807 | projectPath,
808 | docsPath,
809 | mode: "auto", // Auto applies all
810 | });
811 |
812 | const data = JSON.parse(result.content[0].text);
813 | expect(data.success).toBe(true);
814 | });
815 |
816 | test("should handle next steps when no breaking changes exist", async () => {
817 | const sourceCode = `export function noBreaking(): void {}`;
818 | await fs.writeFile(join(projectPath, "src", "nobreaking.ts"), sourceCode);
819 |
820 | const result = await handleSyncCodeToDocs({
821 | projectPath,
822 | docsPath,
823 | mode: "detect",
824 | });
825 |
826 | const data = JSON.parse(result.content[0].text);
827 | expect(data.success).toBe(true);
828 | // Should not suggest reviewing breaking changes when there are none
829 | const breakingStep = data.nextSteps?.find(
830 | (s: any) => s.action?.toLowerCase().includes("breaking"),
831 | );
832 | expect(breakingStep).toBeUndefined();
833 | });
834 |
835 | test("should handle next steps when no changes were applied", async () => {
836 | const sourceCode = `export function noApplied(): void {}`;
837 | await fs.writeFile(join(projectPath, "src", "noapplied.ts"), sourceCode);
838 |
839 | const result = await handleSyncCodeToDocs({
840 | projectPath,
841 | docsPath,
842 | mode: "detect", // Detect mode doesn't apply
843 | });
844 |
845 | const data = JSON.parse(result.content[0].text);
846 | expect(data.success).toBe(true);
847 | expect(data.data.appliedChanges).toHaveLength(0);
848 | });
849 |
850 | test("should handle next steps when no pending changes require review", async () => {
851 | const sourceCode = `export function noPending(): void {}`;
852 | await fs.writeFile(join(projectPath, "src", "nopending.ts"), sourceCode);
853 |
854 | const result = await handleSyncCodeToDocs({
855 | projectPath,
856 | docsPath,
857 | mode: "auto", // Auto applies everything
858 | });
859 |
860 | const data = JSON.parse(result.content[0].text);
861 | expect(data.success).toBe(true);
862 | });
863 |
864 | test("should handle apply mode with suggestions below threshold", async () => {
865 | const sourceCode = `export function lowConfidence(): void {}`;
866 | await fs.writeFile(
867 | join(projectPath, "src", "lowconfidence.ts"),
868 | sourceCode,
869 | );
870 |
871 | // Create baseline
872 | await handleSyncCodeToDocs({
873 | projectPath,
874 | docsPath,
875 | mode: "detect",
876 | });
877 |
878 | // Modify
879 | const newCode = `export function lowConfidence(param: string): void {}`;
880 | await fs.writeFile(join(projectPath, "src", "lowconfidence.ts"), newCode);
881 |
882 | // Very high threshold - suggestions won't meet it
883 | const result = await handleSyncCodeToDocs({
884 | projectPath,
885 | docsPath,
886 | mode: "apply",
887 | autoApplyThreshold: 1.0,
888 | });
889 |
890 | const data = JSON.parse(result.content[0].text);
891 | expect(data.success).toBe(true);
892 | });
893 |
894 | test("should handle context parameter with info logging", async () => {
895 | const sourceCode = `export function withContext(): void {}`;
896 | await fs.writeFile(join(projectPath, "src", "context.ts"), sourceCode);
897 |
898 | const mockContext = {
899 | info: jest.fn(),
900 | warn: jest.fn(),
901 | };
902 |
903 | const result = await handleSyncCodeToDocs(
904 | {
905 | projectPath,
906 | docsPath,
907 | mode: "detect",
908 | },
909 | mockContext,
910 | );
911 |
912 | const data = JSON.parse(result.content[0].text);
913 | expect(data.success).toBe(true);
914 | // Context info should have been called
915 | expect(mockContext.info).toHaveBeenCalled();
916 | });
917 |
918 | test("should handle snapshot creation in non-detect modes", async () => {
919 | const sourceCode = `export function modeSnapshot(): void {}`;
920 | await fs.writeFile(
921 | join(projectPath, "src", "modesnapshot.ts"),
922 | sourceCode,
923 | );
924 |
925 | // Apply mode should create snapshot even if createSnapshot not specified
926 | const result = await handleSyncCodeToDocs({
927 | projectPath,
928 | docsPath,
929 | mode: "apply",
930 | createSnapshot: false, // But mode !== "detect" overrides this
931 | });
932 |
933 | const data = JSON.parse(result.content[0].text);
934 | expect(data.success).toBe(true);
935 | });
936 | });
937 |
938 | describe("Mocked Drift Detector Tests", () => {
939 | let mockDetector: jest.Mocked<DriftDetector>;
940 |
941 | beforeEach(() => {
942 | // Create a real detector instance but spy on its methods
943 | mockDetector = new DriftDetector(
944 | projectPath,
945 | ) as jest.Mocked<DriftDetector>;
946 | });
947 |
948 | test("should apply high-confidence suggestions automatically", async () => {
949 | // Create real documentation file
950 | const docPath = join(docsPath, "api.md");
951 | const originalDoc = `# API
952 |
953 | ## oldFunction
954 |
955 | This is outdated.`;
956 | await fs.writeFile(docPath, originalDoc);
957 |
958 | // Mock drift detector to return suggestions
959 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
960 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
961 | timestamp: "2025-01-01T00:00:00.000Z",
962 | projectPath,
963 | files: new Map([
964 | [
965 | "src/api.ts",
966 | {
967 | filePath: "src/api.ts",
968 | language: "typescript",
969 | functions: [],
970 | classes: [],
971 | interfaces: [],
972 | types: [],
973 | imports: [],
974 | exports: [],
975 | contentHash: "abc123",
976 | lastModified: "2025-01-01T00:00:00.000Z",
977 | linesOfCode: 10,
978 | complexity: 1,
979 | },
980 | ],
981 | ]),
982 | documentation: new Map(),
983 | });
984 |
985 | jest
986 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
987 | .mockResolvedValue({
988 | timestamp: "2025-01-01T00:00:00.000Z",
989 | projectPath,
990 | files: new Map(),
991 | documentation: new Map(),
992 | });
993 |
994 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
995 | {
996 | filePath: "src/api.ts",
997 | hasDrift: true,
998 | severity: "medium",
999 | drifts: [
1000 | {
1001 | type: "outdated",
1002 | affectedDocs: [docPath],
1003 | codeChanges: [
1004 | {
1005 | type: "modified",
1006 | category: "function",
1007 | name: "oldFunction",
1008 | details: "Function renamed to newFunction",
1009 | impactLevel: "major",
1010 | },
1011 | ],
1012 | description: "Function signature changed",
1013 | detectedAt: "2025-01-01T00:00:00.000Z",
1014 | severity: "medium",
1015 | },
1016 | ],
1017 | impactAnalysis: {
1018 | breakingChanges: 0,
1019 | majorChanges: 1,
1020 | minorChanges: 0,
1021 | affectedDocFiles: [docPath],
1022 | estimatedUpdateEffort: "medium",
1023 | requiresManualReview: false,
1024 | },
1025 | suggestions: [
1026 | {
1027 | docFile: docPath,
1028 | section: "oldFunction",
1029 | currentContent: "This is outdated.",
1030 | suggestedContent: `## newFunction
1031 |
1032 | Updated documentation for new function.`,
1033 | reasoning: "Function was renamed from oldFunction to newFunction",
1034 | confidence: 0.95,
1035 | autoApplicable: true,
1036 | },
1037 | ],
1038 | },
1039 | ]);
1040 |
1041 | // Run in apply mode
1042 | const result = await handleSyncCodeToDocs({
1043 | projectPath,
1044 | docsPath,
1045 | mode: "apply",
1046 | autoApplyThreshold: 0.8,
1047 | });
1048 |
1049 | const data = JSON.parse(result.content[0].text);
1050 | expect(data.success).toBe(true);
1051 | expect(data.data.appliedChanges.length).toBeGreaterThan(0);
1052 |
1053 | // Verify the file was actually modified
1054 | const updatedDoc = await fs.readFile(docPath, "utf-8");
1055 | expect(updatedDoc).toContain("newFunction");
1056 | });
1057 |
1058 | test("should not apply low-confidence suggestions", async () => {
1059 | const docPath = join(docsPath, "lowconf.md");
1060 | const originalDoc = `# Low Confidence
1061 |
1062 | ## someFunction
1063 |
1064 | Original content.`;
1065 | await fs.writeFile(docPath, originalDoc);
1066 |
1067 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
1068 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
1069 | timestamp: "2025-01-01T00:00:00.000Z",
1070 | projectPath,
1071 | files: new Map(),
1072 | documentation: new Map(),
1073 | });
1074 |
1075 | jest
1076 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
1077 | .mockResolvedValue({
1078 | timestamp: "2025-01-01T00:00:00.000Z",
1079 | projectPath,
1080 | files: new Map(),
1081 | documentation: new Map(),
1082 | });
1083 |
1084 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
1085 | {
1086 | filePath: "src/lowconf.ts",
1087 | hasDrift: true,
1088 | severity: "low",
1089 | drifts: [
1090 | {
1091 | type: "outdated",
1092 | affectedDocs: [docPath],
1093 | codeChanges: [
1094 | {
1095 | type: "modified",
1096 | category: "function",
1097 | name: "someFunction",
1098 | details: "Minor change",
1099 | impactLevel: "minor",
1100 | },
1101 | ],
1102 | description: "Minor change",
1103 | detectedAt: "2025-01-01T00:00:00.000Z",
1104 | severity: "low",
1105 | },
1106 | ],
1107 | impactAnalysis: {
1108 | breakingChanges: 0,
1109 | majorChanges: 0,
1110 | minorChanges: 1,
1111 | affectedDocFiles: [docPath],
1112 | estimatedUpdateEffort: "low",
1113 | requiresManualReview: false,
1114 | },
1115 | suggestions: [
1116 | {
1117 | docFile: docPath,
1118 | section: "someFunction",
1119 | currentContent: "Original content.",
1120 | suggestedContent: "Suggested content.",
1121 | reasoning: "Minor update needed",
1122 | confidence: 0.5, // Low confidence
1123 | autoApplicable: true,
1124 | },
1125 | ],
1126 | },
1127 | ]);
1128 |
1129 | const result = await handleSyncCodeToDocs({
1130 | projectPath,
1131 | docsPath,
1132 | mode: "apply",
1133 | autoApplyThreshold: 0.8, // Higher than suggestion confidence
1134 | });
1135 |
1136 | const data = JSON.parse(result.content[0].text);
1137 | expect(data.success).toBe(true);
1138 | expect(data.data.pendingChanges.length).toBeGreaterThan(0);
1139 | expect(data.data.appliedChanges.length).toBe(0);
1140 |
1141 | // Verify file was NOT modified
1142 | const unchangedDoc = await fs.readFile(docPath, "utf-8");
1143 | expect(unchangedDoc).toBe(originalDoc);
1144 | });
1145 |
1146 | test("should apply all suggestions in auto mode regardless of confidence", async () => {
1147 | const docPath = join(docsPath, "auto.md");
1148 | const originalDoc = `# Auto Mode
1149 |
1150 | ## function1
1151 |
1152 | Old content.`;
1153 | await fs.writeFile(docPath, originalDoc);
1154 |
1155 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
1156 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
1157 | timestamp: "2025-01-01T00:00:00.000Z",
1158 | projectPath,
1159 | files: new Map(),
1160 | documentation: new Map(),
1161 | });
1162 |
1163 | jest
1164 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
1165 | .mockResolvedValue({
1166 | timestamp: "2025-01-01T00:00:00.000Z",
1167 | projectPath,
1168 | files: new Map(),
1169 | documentation: new Map(),
1170 | });
1171 |
1172 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
1173 | {
1174 | filePath: "src/auto.ts",
1175 | hasDrift: true,
1176 | severity: "low",
1177 | drifts: [
1178 | {
1179 | type: "outdated",
1180 | affectedDocs: [docPath],
1181 | codeChanges: [
1182 | {
1183 | type: "modified",
1184 | category: "function",
1185 | name: "function1",
1186 | details: "Change",
1187 | impactLevel: "minor",
1188 | },
1189 | ],
1190 | description: "Change",
1191 | detectedAt: "2025-01-01T00:00:00.000Z",
1192 | severity: "low",
1193 | },
1194 | ],
1195 | impactAnalysis: {
1196 | breakingChanges: 0,
1197 | majorChanges: 0,
1198 | minorChanges: 1,
1199 | affectedDocFiles: [docPath],
1200 | estimatedUpdateEffort: "low",
1201 | requiresManualReview: false,
1202 | },
1203 | suggestions: [
1204 | {
1205 | docFile: docPath,
1206 | section: "function1",
1207 | currentContent: "Old content.",
1208 | suggestedContent: `## function1
1209 |
1210 | New content from auto mode.`,
1211 | reasoning: "Auto-applied update",
1212 | confidence: 0.3, // Very low confidence
1213 | autoApplicable: false, // Not auto-applicable
1214 | },
1215 | ],
1216 | },
1217 | ]);
1218 |
1219 | const result = await handleSyncCodeToDocs({
1220 | projectPath,
1221 | docsPath,
1222 | mode: "auto", // Auto mode applies everything
1223 | });
1224 |
1225 | const data = JSON.parse(result.content[0].text);
1226 | expect(data.success).toBe(true);
1227 | expect(data.data.appliedChanges.length).toBeGreaterThan(0);
1228 |
1229 | // Verify file was modified
1230 | const updatedDoc = await fs.readFile(docPath, "utf-8");
1231 | expect(updatedDoc).toContain("New content from auto mode");
1232 | });
1233 |
1234 | test("should handle apply errors and mark as pending", async () => {
1235 | const docPath = join(docsPath, "error.md");
1236 | // Don't create the file - this will cause an error
1237 |
1238 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
1239 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
1240 | timestamp: "2025-01-01T00:00:00.000Z",
1241 | projectPath,
1242 | files: new Map(),
1243 | documentation: new Map(),
1244 | });
1245 |
1246 | jest
1247 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
1248 | .mockResolvedValue({
1249 | timestamp: "2025-01-01T00:00:00.000Z",
1250 | projectPath,
1251 | files: new Map(),
1252 | documentation: new Map(),
1253 | });
1254 |
1255 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
1256 | {
1257 | filePath: "src/error.ts",
1258 | hasDrift: true,
1259 | severity: "medium",
1260 | drifts: [
1261 | {
1262 | type: "outdated",
1263 | affectedDocs: [docPath],
1264 | codeChanges: [
1265 | {
1266 | type: "modified",
1267 | category: "function",
1268 | name: "errorFunction",
1269 | details: "Change",
1270 | impactLevel: "minor",
1271 | },
1272 | ],
1273 | description: "Change",
1274 | detectedAt: "2025-01-01T00:00:00.000Z",
1275 | severity: "medium",
1276 | },
1277 | ],
1278 | impactAnalysis: {
1279 | breakingChanges: 0,
1280 | majorChanges: 1,
1281 | minorChanges: 0,
1282 | affectedDocFiles: [docPath],
1283 | estimatedUpdateEffort: "medium",
1284 | requiresManualReview: false,
1285 | },
1286 | suggestions: [
1287 | {
1288 | docFile: docPath, // File doesn't exist
1289 | section: "errorSection",
1290 | currentContent: "N/A",
1291 | suggestedContent: "New content",
1292 | reasoning: "Should fail",
1293 | confidence: 0.95,
1294 | autoApplicable: true,
1295 | },
1296 | ],
1297 | },
1298 | ]);
1299 |
1300 | const mockContext = {
1301 | info: jest.fn(),
1302 | warn: jest.fn(),
1303 | };
1304 |
1305 | const result = await handleSyncCodeToDocs(
1306 | {
1307 | projectPath,
1308 | docsPath,
1309 | mode: "apply",
1310 | autoApplyThreshold: 0.8,
1311 | },
1312 | mockContext,
1313 | );
1314 |
1315 | const data = JSON.parse(result.content[0].text);
1316 | expect(data.success).toBe(true);
1317 | // Failed applies should be added to pending changes
1318 | expect(data.data.pendingChanges.length).toBeGreaterThan(0);
1319 | expect(mockContext.warn).toHaveBeenCalled();
1320 | });
1321 |
1322 | test("should add new section when section doesn't exist", async () => {
1323 | const docPath = join(docsPath, "newsection.md");
1324 | const originalDoc = `# New Section Test
1325 |
1326 | ## existingSection
1327 |
1328 | Existing content.`;
1329 | await fs.writeFile(docPath, originalDoc);
1330 |
1331 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
1332 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
1333 | timestamp: "2025-01-01T00:00:00.000Z",
1334 | projectPath,
1335 | files: new Map(),
1336 | documentation: new Map(),
1337 | });
1338 |
1339 | jest
1340 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
1341 | .mockResolvedValue({
1342 | timestamp: "2025-01-01T00:00:00.000Z",
1343 | projectPath,
1344 | files: new Map(),
1345 | documentation: new Map(),
1346 | });
1347 |
1348 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
1349 | {
1350 | filePath: "src/new.ts",
1351 | hasDrift: true,
1352 | severity: "low",
1353 | drifts: [
1354 | {
1355 | type: "missing",
1356 | affectedDocs: [docPath],
1357 | codeChanges: [
1358 | {
1359 | type: "added",
1360 | category: "function",
1361 | name: "newFunction",
1362 | details: "New function added",
1363 | impactLevel: "minor",
1364 | },
1365 | ],
1366 | description: "New function",
1367 | detectedAt: "2025-01-01T00:00:00.000Z",
1368 | severity: "low",
1369 | },
1370 | ],
1371 | impactAnalysis: {
1372 | breakingChanges: 0,
1373 | majorChanges: 0,
1374 | minorChanges: 1,
1375 | affectedDocFiles: [docPath],
1376 | estimatedUpdateEffort: "low",
1377 | requiresManualReview: false,
1378 | },
1379 | suggestions: [
1380 | {
1381 | docFile: docPath,
1382 | section: "newSection", // This section doesn't exist
1383 | currentContent: "",
1384 | suggestedContent: `## newSection
1385 |
1386 | This is a brand new section.`,
1387 | reasoning: "New function added",
1388 | confidence: 0.9,
1389 | autoApplicable: true,
1390 | },
1391 | ],
1392 | },
1393 | ]);
1394 |
1395 | const result = await handleSyncCodeToDocs({
1396 | projectPath,
1397 | docsPath,
1398 | mode: "apply",
1399 | autoApplyThreshold: 0.8,
1400 | });
1401 |
1402 | const data = JSON.parse(result.content[0].text);
1403 | expect(data.success).toBe(true);
1404 | expect(data.data.appliedChanges.length).toBeGreaterThan(0);
1405 |
1406 | // Verify new section was appended
1407 | const updatedDoc = await fs.readFile(docPath, "utf-8");
1408 | expect(updatedDoc).toContain("newSection");
1409 | expect(updatedDoc).toContain("brand new section");
1410 | });
1411 |
1412 | test("should handle breaking changes recommendation", async () => {
1413 | const docPath = join(docsPath, "breaking.md");
1414 | await fs.writeFile(docPath, "# Breaking");
1415 |
1416 | jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue();
1417 | jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({
1418 | timestamp: "2025-01-01T00:00:00.000Z",
1419 | projectPath,
1420 | files: new Map(),
1421 | documentation: new Map(),
1422 | });
1423 |
1424 | jest
1425 | .spyOn(DriftDetector.prototype, "loadLatestSnapshot")
1426 | .mockResolvedValue({
1427 | timestamp: "2025-01-01T00:00:00.000Z",
1428 | projectPath,
1429 | files: new Map(),
1430 | documentation: new Map(),
1431 | });
1432 |
1433 | jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([
1434 | {
1435 | filePath: "src/breaking.ts",
1436 | hasDrift: true,
1437 | severity: "critical",
1438 | drifts: [
1439 | {
1440 | type: "breaking",
1441 | affectedDocs: [docPath],
1442 | codeChanges: [
1443 | {
1444 | type: "removed",
1445 | category: "function",
1446 | name: "oldAPI",
1447 | details: "Breaking change",
1448 | impactLevel: "breaking",
1449 | },
1450 | ],
1451 | description: "Breaking change",
1452 | detectedAt: "2025-01-01T00:00:00.000Z",
1453 | severity: "critical",
1454 | },
1455 | ],
1456 | impactAnalysis: {
1457 | breakingChanges: 2, // Multiple breaking changes
1458 | majorChanges: 0,
1459 | minorChanges: 0,
1460 | affectedDocFiles: [docPath],
1461 | estimatedUpdateEffort: "high",
1462 | requiresManualReview: true,
1463 | },
1464 | suggestions: [
1465 | {
1466 | docFile: docPath,
1467 | section: "API",
1468 | currentContent: "Old API",
1469 | suggestedContent: "New API",
1470 | reasoning: "Breaking change",
1471 | confidence: 0.9,
1472 | autoApplicable: true,
1473 | },
1474 | ],
1475 | },
1476 | ]);
1477 |
1478 | const result = await handleSyncCodeToDocs({
1479 | projectPath,
1480 | docsPath,
1481 | mode: "detect",
1482 | });
1483 |
1484 | const data = JSON.parse(result.content[0].text);
1485 | expect(data.success).toBe(true);
1486 | expect(data.data.stats.breakingChanges).toBe(2);
1487 |
1488 | // Should have critical recommendation
1489 | const criticalRec = data.recommendations.find(
1490 | (r: any) => r.type === "critical",
1491 | );
1492 | expect(criticalRec).toBeDefined();
1493 | expect(criticalRec.title).toContain("Breaking");
1494 | });
1495 |
1496 | afterEach(() => {
1497 | jest.restoreAllMocks();
1498 | });
1499 | });
1500 | });
1501 |
```