// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited /** * End-to-end workflow test * * This test exercises the COMPLETE lifecycle of SocratiCode through the * tool handler API — the same interface the MCP server exposes to clients. * * Workflow: * 1. Health check (codebase_health) * 2. About (codebase_about) * 3. Index a fixture project (codebase_index + poll codebase_status) * 4. Semantic search (codebase_search) * 5. Build code graph (codebase_graph_build) * 6. Query graph (codebase_graph_query, codebase_graph_stats, codebase_graph_circular, codebase_graph_visualize) * 7. Incremental update — add file (codebase_update) * 8. Search for the new file's content (codebase_search) * 9. Watch lifecycle (codebase_watch start → status → stop) * 10. List projects (codebase_list_projects) * 11. Remove index (codebase_remove) * 12. Confirm removal (codebase_status, codebase_list_projects) */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { invalidateGraphCache } from "../../src/services/code-graph.js"; import { stopAllWatchers } from "../../src/services/watcher.js"; import { handleGraphTool } from "../../src/tools/graph-tools.js"; import { handleIndexTool } from "../../src/tools/index-tools.js"; import { handleManageTool } from "../../src/tools/manage-tools.js"; import { handleQueryTool } from "../../src/tools/query-tools.js"; import { addFileToFixture, createFixtureProject, type FixtureProject, isDockerAvailable, } from "../helpers/fixtures.js"; import { cleanupTestCollections, waitForOllama, waitForQdrant, } from "../helpers/setup.js"; const dockerAvailable = isDockerAvailable(); async function waitForIndexingComplete( projectPath: string, timeoutMs = 180_000, ): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { const status = await handleQueryTool("codebase_status", { projectPath, }); if ( status.includes("Last completed") || status.includes("chunks") || (status.includes("files") && status.includes("indexed")) ) { return; } await new Promise((resolve) => setTimeout(resolve, 3_000)); } throw new Error(`Indexing did not complete within ${timeoutMs}ms`); } describe.skipIf(!dockerAvailable)( "e2e: full SocratiCode workflow", { timeout: 600_000 }, () => { let fixture: FixtureProject; beforeAll(async () => { await waitForQdrant(); await waitForOllama(); fixture = createFixtureProject("e2e-workflow"); await cleanupTestCollections(fixture.root); }, 120_000); afterAll(async () => { await stopAllWatchers(); invalidateGraphCache(fixture.root); fixture.cleanup(); await cleanupTestCollections(fixture.root); }); // ─── Step 1: Health check ──────────────────────────────── it("step 1 — health check passes", async () => { const result = await handleManageTool("codebase_health", {}); expect(result).toContain("[OK]"); expect(result).toContain("Docker"); expect(result).toContain("Qdrant"); expect(result).toContain("Ollama"); }); // ─── Step 2: About ─────────────────────────────────────── it("step 2 — about returns feature description", async () => { const result = await handleManageTool("codebase_about", {}); expect(result).toContain("SocratiCode"); }); // ─── Step 3: Index the fixture project ─────────────────── it("step 3 — index project (fire-and-forget)", async () => { const result = await handleIndexTool("codebase_index", { projectPath: fixture.root, }); expect(result).toContain("Indexing started"); }, 60_000); it("step 3b — poll until indexing completes", async () => { await waitForIndexingComplete(fixture.root); const status = await handleQueryTool("codebase_status", { projectPath: fixture.root, }); expect(status.length).toBeGreaterThan(0); }, 180_000); // ─── Step 4: Semantic search ───────────────────────────── it("step 4 — search finds authentication code", async () => { const result = await handleQueryTool("codebase_search", { query: "user authentication login", projectPath: fixture.root, }); expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); }, 30_000); it("step 4b — search with limit", async () => { const result = await handleQueryTool("codebase_search", { query: "function", projectPath: fixture.root, limit: 3, }); expect(result).toBeDefined(); }, 30_000); // ─── Step 5: Build code graph ──────────────────────────── it("step 5 — build code graph (fire-and-forget)", async () => { const result = await handleGraphTool("codebase_graph_build", { projectPath: fixture.root, }); expect(result).toContain("Graph build started in the background"); expect(result).toContain("codebase_graph_status"); }); it("step 5b — poll until graph build completes", async () => { const start = Date.now(); const timeout = 60_000; while (Date.now() - start < timeout) { const status = await handleGraphTool("codebase_graph_status", { projectPath: fixture.root, }); if (status.includes("READY")) { return; } await new Promise((resolve) => setTimeout(resolve, 1_000)); } throw new Error("Graph build did not complete within 60s"); }, 90_000); // ─── Step 6: Graph queries ─────────────────────────────── it("step 6a — graph query for index.ts dependencies", async () => { const result = await handleGraphTool("codebase_graph_query", { projectPath: fixture.root, filePath: "src/index.ts", }); expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); }); it("step 6b — graph stats", async () => { const result = await handleGraphTool("codebase_graph_stats", { projectPath: fixture.root, }); expect(result).toContain("Code Graph Statistics"); expect(result).toContain("Total files:"); }); it("step 6c — circular dependency check", async () => { const result = await handleGraphTool("codebase_graph_circular", { projectPath: fixture.root, }); // Reports circular dependency findings (fixture has a self-cycle) expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); }); it("step 6d — Mermaid visualization", async () => { const result = await handleGraphTool("codebase_graph_visualize", { projectPath: fixture.root, }); expect(result).toContain("```mermaid"); expect(result).toContain("graph LR"); }); // ─── Step 7: Incremental update ────────────────────────── it("step 7 — add a file and run incremental update", async () => { addFileToFixture( fixture.root, "src/services/cache.ts", ` import { User } from "../types"; interface CacheEntry { value: T; expiresAt: number; } export class InMemoryCache { private store = new Map>(); set(key: string, value: T, ttlMs: number): void { this.store.set(key, { value, expiresAt: Date.now() + ttlMs }); } get(key: string): T | null { const entry = this.store.get(key); if (!entry || entry.expiresAt < Date.now()) { this.store.delete(key); return null; } return entry.value as T; } invalidate(key: string): void { this.store.delete(key); } clear(): void { this.store.clear(); } cacheUser(user: User): void { this.set(user.id, user, 60_000); } } `.trim(), ); const result = await handleIndexTool("codebase_update", { projectPath: fixture.root, }); expect(result).toBeDefined(); expect(result).toContain("added"); }, 60_000); // ─── Step 8: Search for newly added content ────────────── it("step 8 — search finds the Cache class", async () => { const result = await handleQueryTool("codebase_search", { query: "in-memory cache TTL expiration", projectPath: fixture.root, }); expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); // The cache.ts content should appear expect( result.toLowerCase().includes("cache") || result.toLowerCase().includes("ttl") || result.toLowerCase().includes("expir"), ).toBe(true); }, 30_000); // ─── Step 9: Watch lifecycle ───────────────────────────── it("step 9a — start watching", async () => { const result = await handleIndexTool("codebase_watch", { projectPath: fixture.root, action: "start", }); expect(result.toLowerCase()).toContain("watch"); }, 30_000); it("step 9b — watch status shows active", async () => { const result = await handleIndexTool("codebase_watch", { projectPath: fixture.root, action: "status", }); expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); }); it("step 9c — stop watching", async () => { const result = await handleIndexTool("codebase_watch", { projectPath: fixture.root, action: "stop", }); expect(result.toLowerCase()).toContain("stop"); }); // ─── Step 10: List projects ────────────────────────────── it("step 10 — project appears in list", async () => { const result = await handleManageTool("codebase_list_projects", {}); expect(result).toBeDefined(); // Should contain some reference to our fixture expect(result.length).toBeGreaterThan(10); }); // ─── Step 11: Remove index ─────────────────────────────── it("step 11 — remove project index", async () => { const result = await handleIndexTool("codebase_remove", { projectPath: fixture.root, }); expect(result).toContain("Removed"); }, 30_000); // ─── Step 12: Confirm removal ──────────────────────────── it("step 12a — status confirms not indexed", async () => { const result = await handleQueryTool("codebase_status", { projectPath: fixture.root, }); expect( result.toLowerCase().includes("not") || result.toLowerCase().includes("no ") || result.toLowerCase().includes("does not"), ).toBe(true); }, 30_000); }, );