kg-documentation-workflow.test.ts•11.9 kB
/**
* Integration Tests for Knowledge Graph Documentation Workflow
* Tests end-to-end workflow from repository analysis to documentation tracking
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import path from "path";
import { tmpdir } from "os";
import { analyzeRepository } from "../../src/tools/analyze-repository.js";
import {
initializeKnowledgeGraph,
getKnowledgeGraph,
saveKnowledgeGraph,
} from "../../src/memory/kg-integration.js";
describe("KG Documentation Workflow Integration", () => {
let testDir: string;
beforeEach(async () => {
testDir = path.join(tmpdir(), `documcp-integration-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
// Initialize KG with test storage
const storageDir = path.join(testDir, ".documcp/memory");
await initializeKnowledgeGraph(storageDir);
});
afterEach(async () => {
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
it("should complete full workflow: analyze → create entities → link relationships", async () => {
// Setup: Create a test repository structure
const srcDir = path.join(testDir, "src");
const docsDir = path.join(testDir, "docs");
await fs.mkdir(srcDir, { recursive: true });
await fs.mkdir(docsDir, { recursive: true });
// Create source code
await fs.writeFile(
path.join(srcDir, "auth.ts"),
`
export class AuthService {
async login(username: string, password: string) {
return { token: "abc123" };
}
async logout(token: string) {
return true;
}
}
export function validateToken(token: string) {
return token.length > 0;
}
`,
"utf-8",
);
// Create documentation
await fs.writeFile(
path.join(docsDir, "api.md"),
`
# Authentication API
## Login
Use the \`login()\` method from \`AuthService\` class in \`src/auth.ts\`:
\`\`\`typescript
const auth = new AuthService();
const result = await auth.login(username, password);
\`\`\`
## Logout
Call \`logout()\` with the authentication token:
\`\`\`typescript
await auth.logout(token);
\`\`\`
## Token Validation
Use \`validateToken()\` function to validate tokens.
`,
"utf-8",
);
await fs.writeFile(
path.join(testDir, "README.md"),
"# Test Project",
"utf-8",
);
await fs.writeFile(
path.join(testDir, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
"utf-8",
);
// Act: Run repository analysis
const analysisResult = await analyzeRepository({
path: testDir,
depth: "standard",
});
// Assert: Analysis completed (may have errors due to test environment)
expect(analysisResult.content).toBeDefined();
expect(analysisResult.content.length).toBeGreaterThan(0);
// If analysis succeeded, verify structure
if (!analysisResult.isError) {
const analysis = JSON.parse(analysisResult.content[0].text);
if (analysis.success) {
expect(analysis.data.structure.hasDocs).toBe(true);
}
}
// Wait for KG operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify: Check knowledge graph entities
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
const allEdges = await kg.getAllEdges();
// Should have project, code files, and documentation sections
const projectNodes = allNodes.filter((n) => n.type === "project");
const codeFileNodes = allNodes.filter((n) => n.type === "code_file");
const docSectionNodes = allNodes.filter(
(n) => n.type === "documentation_section",
);
expect(projectNodes.length).toBeGreaterThan(0);
expect(codeFileNodes.length).toBeGreaterThan(0);
expect(docSectionNodes.length).toBeGreaterThan(0);
// Verify code file details
const authFile = codeFileNodes.find((n) =>
n.properties.path.includes("auth.ts"),
);
expect(authFile).toBeDefined();
expect(authFile?.properties.language).toBe("typescript");
expect(authFile?.properties.classes).toContain("AuthService");
expect(authFile?.properties.functions).toContain("validateToken");
// Verify documentation sections
const apiDoc = docSectionNodes.find((n) =>
n.properties.filePath.includes("api.md"),
);
expect(apiDoc).toBeDefined();
expect(apiDoc?.properties.hasCodeExamples).toBe(true);
expect(apiDoc?.properties.referencedFunctions.length).toBeGreaterThan(0);
// Verify relationships
const referencesEdges = allEdges.filter((e) => e.type === "references");
const documentsEdges = allEdges.filter((e) => e.type === "documents");
expect(referencesEdges.length).toBeGreaterThan(0);
expect(documentsEdges.length).toBeGreaterThan(0);
// Verify specific relationship: api.md references auth.ts
const apiToAuthEdge = referencesEdges.find(
(e) => e.source === apiDoc?.id && e.target === authFile?.id,
);
expect(apiToAuthEdge).toBeDefined();
expect(apiToAuthEdge?.properties.referenceType).toBe("api-reference");
});
it("should detect outdated documentation when code changes", async () => {
// Setup: Create initial code and docs
const srcDir = path.join(testDir, "src");
const docsDir = path.join(testDir, "docs");
await fs.mkdir(srcDir, { recursive: true });
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "user.ts"),
"export function getUser() {}",
"utf-8",
);
await fs.writeFile(
path.join(docsDir, "guide.md"),
"Call `getUser()` from `src/user.ts`",
"utf-8",
);
await fs.writeFile(path.join(testDir, "README.md"), "# Test", "utf-8");
await fs.writeFile(path.join(testDir, "package.json"), "{}", "utf-8");
// First analysis
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Simulate code change
await new Promise((resolve) => setTimeout(resolve, 100)); // Ensure different timestamp
await fs.writeFile(
path.join(srcDir, "user.ts"),
"export function getUser(id: string) {} // CHANGED",
"utf-8",
);
// Second analysis
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify: Check that system handled multiple analyses
// In a real scenario, outdated_for edges would be created
// For this test, just verify no crashes occurred
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
// Should have created some nodes from both analyses
expect(allNodes.length).toBeGreaterThan(0);
});
it("should handle projects with no documentation gracefully", async () => {
// Setup: Code-only project
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "index.ts"),
"export function main() {}",
"utf-8",
);
await fs.writeFile(path.join(testDir, "package.json"), "{}", "utf-8");
// Act
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify: Should still create code entities, just no doc entities
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
const codeFileNodes = allNodes.filter((n) => n.type === "code_file");
const docSectionNodes = allNodes.filter(
(n) => n.type === "documentation_section",
);
expect(codeFileNodes.length).toBeGreaterThan(0);
expect(docSectionNodes.length).toBe(0);
});
it("should handle multi-file projects correctly", async () => {
// Setup: Multiple source files
const srcDir = path.join(testDir, "src");
await fs.mkdir(path.join(srcDir, "auth"), { recursive: true });
await fs.mkdir(path.join(srcDir, "db"), { recursive: true });
await fs.writeFile(
path.join(srcDir, "auth", "login.ts"),
"export function login() {}",
"utf-8",
);
await fs.writeFile(
path.join(srcDir, "auth", "logout.ts"),
"export function logout() {}",
"utf-8",
);
await fs.writeFile(
path.join(srcDir, "db", "query.ts"),
"export function query() {}",
"utf-8",
);
await fs.writeFile(path.join(testDir, "package.json"), "{}", "utf-8");
// Act
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify
const kg = await getKnowledgeGraph();
const codeFileNodes = (await kg.getAllNodes()).filter(
(n) => n.type === "code_file",
);
expect(codeFileNodes.length).toBe(3);
const paths = codeFileNodes.map((n) => n.properties.path);
expect(paths).toContain("src/auth/login.ts");
expect(paths).toContain("src/auth/logout.ts");
expect(paths).toContain("src/db/query.ts");
});
it("should persist knowledge graph to storage", async () => {
// Setup
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "test.ts"),
"export function test() {}",
"utf-8",
);
await fs.writeFile(path.join(testDir, "package.json"), "{}", "utf-8");
// Act
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Save KG
await saveKnowledgeGraph();
// Verify storage files exist
const storageDir = path.join(testDir, ".documcp/memory");
const entitiesFile = path.join(
storageDir,
"knowledge-graph-entities.jsonl",
);
const relationshipsFile = path.join(
storageDir,
"knowledge-graph-relationships.jsonl",
);
const entitiesExist = await fs
.access(entitiesFile)
.then(() => true)
.catch(() => false);
const relationshipsExist = await fs
.access(relationshipsFile)
.then(() => true)
.catch(() => false);
expect(entitiesExist).toBe(true);
expect(relationshipsExist).toBe(true);
// Verify content
const entitiesContent = await fs.readFile(entitiesFile, "utf-8");
expect(entitiesContent).toContain("code_file");
});
it("should calculate coverage metrics for documentation", async () => {
// Setup: 3 functions, docs covering 2 of them
const srcDir = path.join(testDir, "src");
const docsDir = path.join(testDir, "docs");
await fs.mkdir(srcDir, { recursive: true });
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "api.ts"),
`
export function create() {}
export function read() {}
export function update() {} // Not documented
`,
"utf-8",
);
await fs.writeFile(
path.join(docsDir, "api.md"),
`
# API Reference
- \`create()\`: Creates a resource
- \`read()\`: Reads a resource
`,
"utf-8",
);
await fs.writeFile(path.join(testDir, "README.md"), "# Test", "utf-8");
await fs.writeFile(path.join(testDir, "package.json"), "{}", "utf-8");
// Act
await analyzeRepository({ path: testDir, depth: "standard" });
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify coverage
const kg = await getKnowledgeGraph();
const documentsEdges = (await kg.getAllEdges()).filter(
(e) => e.type === "documents",
);
expect(documentsEdges.length).toBeGreaterThan(0);
const coverage = documentsEdges[0].properties.coverage;
expect(["partial", "complete", "comprehensive"]).toContain(coverage);
// 2/3 = 66% should be "complete"
expect(coverage).toBe("complete");
});
});