import { describe, it, expect, vi, beforeEach } from "vitest"; import { get } from "svelte/store"; import { invoke } from "@tauri-apps/api/core"; import { setMockInvokeResult } from "../../../vitest.setup"; import { projectContextStore, PROJECT_FILE_NAMES, PROJECT_TEMPLATES, PROJECT_CONTEXT_SYSTEM_ADDENDUM, injectTextStore, type ProjectFile, type ProjectScan, } from "./projectContext"; describe("PROJECT_FILE_NAMES", () => { it("maps all five project file types", () => { expect(PROJECT_FILE_NAMES.PROJECT).toBe("PROJECT.md"); expect(PROJECT_FILE_NAMES.REQUIREMENTS).toBe("REQUIREMENTS.md"); expect(PROJECT_FILE_NAMES.ROADMAP).toBe("ROADMAP.md"); expect(PROJECT_FILE_NAMES.STATE).toBe("STATE.md"); expect(PROJECT_FILE_NAMES.CODEBASE).toBe("CODEBASE.md"); }); }); describe("PROJECT_TEMPLATES", () => { const editableFiles: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"]; it.each(editableFiles)("returns a non-empty template for %s", (file) => { expect(PROJECT_TEMPLATES[file]).toBeTruthy(); expect(PROJECT_TEMPLATES[file].length).toBeGreaterThan(0); }); it("has an empty string template for CODEBASE (auto-generated)", () => { expect(PROJECT_TEMPLATES.CODEBASE).toBe(""); }); }); describe("projectContextStore", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("initial state", () => { it("has null contents for all files", () => { const state = get(projectContextStore.contents); expect(state.PROJECT).toBeNull(); expect(state.REQUIREMENTS).toBeNull(); expect(state.ROADMAP).toBeNull(); expect(state.STATE).toBeNull(); expect(state.CODEBASE).toBeNull(); }); it("has false isLoading for all files", () => { const state = get(projectContextStore.isLoading); expect(state.PROJECT).toBe(false); expect(state.REQUIREMENTS).toBe(false); expect(state.ROADMAP).toBe(false); expect(state.STATE).toBe(false); expect(state.CODEBASE).toBe(false); }); it("has false isSaving for all files", () => { const state = get(projectContextStore.isSaving); expect(state.PROJECT).toBe(false); expect(state.REQUIREMENTS).toBe(false); expect(state.ROADMAP).toBe(false); expect(state.STATE).toBe(false); expect(state.CODEBASE).toBe(false); }); it("has PROJECT as the default activeFile", () => { expect(get(projectContextStore.activeFile)).toBe("PROJECT"); }); it("has false isMappingCodebase initially", () => { expect(get(projectContextStore.isMappingCodebase)).toBe(false); }); it("exposes all expected methods", () => { expect(typeof projectContextStore.loadFile).toBe("function"); expect(typeof projectContextStore.saveFile).toBe("function"); expect(typeof projectContextStore.loadAll).toBe("function"); expect(typeof projectContextStore.setActiveFile).toBe("function"); expect(typeof projectContextStore.getTemplate).toBe("function"); expect(typeof projectContextStore.mapCodebase).toBe("function"); expect(typeof projectContextStore.finishMapping).toBe("function"); }); }); describe("loadFile", () => { it("calls read_file_content with the correct path", async () => { setMockInvokeResult("read_file_content", "# Project\n\nContent here"); await projectContextStore.loadFile("PROJECT", "/home/naomi/myproject"); expect(invoke).toHaveBeenCalledWith("read_file_content", { path: "/home/naomi/myproject/PROJECT.md", }); }); it("updates contents store with file content on success", async () => { const content = "# My Project\n\nDescription here"; setMockInvokeResult("read_file_content", content); await projectContextStore.loadFile("PROJECT", "/home/naomi/myproject"); expect(get(projectContextStore.contents).PROJECT).toBe(content); }); it("sets content to null when file does not exist", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("read_file_content", new Error("File not found")); await projectContextStore.loadFile("REQUIREMENTS", "/home/naomi/myproject"); expect(get(projectContextStore.contents).REQUIREMENTS).toBeNull(); consoleSpy.mockRestore(); }); it("sets isLoading to false after completion", async () => { setMockInvokeResult("read_file_content", "content"); await projectContextStore.loadFile("ROADMAP", "/home/naomi/myproject"); expect(get(projectContextStore.isLoading).ROADMAP).toBe(false); }); it("sets isLoading to false even on error", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("read_file_content", new Error("Read error")); await projectContextStore.loadFile("STATE", "/home/naomi/myproject"); expect(get(projectContextStore.isLoading).STATE).toBe(false); consoleSpy.mockRestore(); }); it("uses correct filename for each file type", async () => { const files: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"]; for (const file of files) { setMockInvokeResult("read_file_content", `content for ${file}`); await projectContextStore.loadFile(file, "/wd"); expect(invoke).toHaveBeenCalledWith("read_file_content", { path: `/wd/${PROJECT_FILE_NAMES[file]}`, }); } }); }); describe("saveFile", () => { it("calls write_file_content with the correct path and content", async () => { setMockInvokeResult("write_file_content", undefined); await projectContextStore.saveFile("PROJECT", "# New content", "/home/naomi/myproject"); expect(invoke).toHaveBeenCalledWith("write_file_content", { path: "/home/naomi/myproject/PROJECT.md", content: "# New content", }); }); it("returns true on success", async () => { setMockInvokeResult("write_file_content", undefined); const result = await projectContextStore.saveFile( "PROJECT", "# Content", "/home/naomi/myproject" ); expect(result).toBe(true); }); it("updates contents store with saved content on success", async () => { setMockInvokeResult("write_file_content", undefined); const newContent = "# Updated Project\n\nNew content"; await projectContextStore.saveFile("REQUIREMENTS", newContent, "/home/naomi/myproject"); expect(get(projectContextStore.contents).REQUIREMENTS).toBe(newContent); }); it("returns false and logs error on failure", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("write_file_content", new Error("Write failed")); const result = await projectContextStore.saveFile( "ROADMAP", "content", "/home/naomi/myproject" ); expect(result).toBe(false); expect(consoleSpy).toHaveBeenCalledWith( "Failed to save project context file:", expect.any(Error) ); consoleSpy.mockRestore(); }); it("sets isSaving to false after completion", async () => { setMockInvokeResult("write_file_content", undefined); await projectContextStore.saveFile("STATE", "content", "/home/naomi/myproject"); expect(get(projectContextStore.isSaving).STATE).toBe(false); }); it("sets isSaving to false even on error", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("write_file_content", new Error("Error")); await projectContextStore.saveFile("PROJECT", "content", "/home/naomi/myproject"); expect(get(projectContextStore.isSaving).PROJECT).toBe(false); consoleSpy.mockRestore(); }); }); describe("loadAll", () => { it("loads all five files in parallel", async () => { setMockInvokeResult("read_file_content", "file content"); await projectContextStore.loadAll("/home/naomi/myproject"); const calls = vi.mocked(invoke).mock.calls.filter(([cmd]) => cmd === "read_file_content"); const paths = calls.map(([, args]) => (args as { path: string }).path); expect(paths).toContain("/home/naomi/myproject/PROJECT.md"); expect(paths).toContain("/home/naomi/myproject/REQUIREMENTS.md"); expect(paths).toContain("/home/naomi/myproject/ROADMAP.md"); expect(paths).toContain("/home/naomi/myproject/STATE.md"); expect(paths).toContain("/home/naomi/myproject/CODEBASE.md"); }); it("sets all files isLoading to false after completion", async () => { setMockInvokeResult("read_file_content", "content"); await projectContextStore.loadAll("/home/naomi/myproject"); const loadingState = get(projectContextStore.isLoading); expect(loadingState.PROJECT).toBe(false); expect(loadingState.REQUIREMENTS).toBe(false); expect(loadingState.ROADMAP).toBe(false); expect(loadingState.STATE).toBe(false); expect(loadingState.CODEBASE).toBe(false); }); }); describe("setActiveFile", () => { it("updates the activeFile store", () => { projectContextStore.setActiveFile("REQUIREMENTS"); expect(get(projectContextStore.activeFile)).toBe("REQUIREMENTS"); projectContextStore.setActiveFile("STATE"); expect(get(projectContextStore.activeFile)).toBe("STATE"); projectContextStore.setActiveFile("PROJECT"); expect(get(projectContextStore.activeFile)).toBe("PROJECT"); }); }); describe("getTemplate", () => { const files: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"]; it.each(files)("returns a non-empty string for %s", (file) => { const template = projectContextStore.getTemplate(file); expect(typeof template).toBe("string"); expect(template.length).toBeGreaterThan(0); }); it("returns distinct templates for each file type", () => { const templates = files.map((f) => projectContextStore.getTemplate(f)); const uniqueTemplates = new Set(templates); expect(uniqueTemplates.size).toBe(files.length); }); it("returns empty string for CODEBASE", () => { expect(projectContextStore.getTemplate("CODEBASE")).toBe(""); }); }); describe("mapCodebase", () => { const mockScan: ProjectScan = { working_dir: "/home/naomi/myproject", file_tree: "/home/naomi/myproject/\n├── src/\n└── package.json", detected_type: "Node.js", key_files: ["package.json"], }; it("calls scan_project with the working directory", async () => { setMockInvokeResult("scan_project", mockScan); setMockInvokeResult("send_prompt", undefined); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); expect(invoke).toHaveBeenCalledWith("scan_project", { workingDir: "/home/naomi/myproject", }); }); it("calls send_prompt with the conversation id and a non-empty prompt", async () => { setMockInvokeResult("scan_project", mockScan); setMockInvokeResult("send_prompt", undefined); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); expect(invoke).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: expect.stringContaining("CODEBASE.md"), }); }); it("prompt includes detected project type", async () => { setMockInvokeResult("scan_project", mockScan); setMockInvokeResult("send_prompt", undefined); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt"); const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? ""; expect(message).toContain("Node.js"); }); it("prompt includes file tree", async () => { setMockInvokeResult("scan_project", mockScan); setMockInvokeResult("send_prompt", undefined); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt"); const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? ""; expect(message).toContain("package.json"); }); it("resets isMappingCodebase to false on error", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("scan_project", new Error("Scan failed")); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); expect(get(projectContextStore.isMappingCodebase)).toBe(false); consoleSpy.mockRestore(); }); it("logs error when scan_project fails", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("scan_project", new Error("Scan failed")); await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123"); expect(consoleSpy).toHaveBeenCalledWith("Failed to map codebase:", expect.any(Error)); consoleSpy.mockRestore(); }); }); describe("finishMapping", () => { it("sets isMappingCodebase to false", () => { projectContextStore.finishMapping(); expect(get(projectContextStore.isMappingCodebase)).toBe(false); }); }); }); describe("PROJECT_CONTEXT_SYSTEM_ADDENDUM", () => { it("is a non-empty string", () => { expect(typeof PROJECT_CONTEXT_SYSTEM_ADDENDUM).toBe("string"); expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM.length).toBeGreaterThan(0); }); it("mentions all five context file names", () => { expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("PROJECT.md"); expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("REQUIREMENTS.md"); expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("ROADMAP.md"); expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("STATE.md"); expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("CODEBASE.md"); }); }); describe("injectTextStore", () => { it("initialises to null", () => { expect(get(injectTextStore)).toBeNull(); }); it("can be set and read", () => { injectTextStore.set("hello world"); expect(get(injectTextStore)).toBe("hello world"); injectTextStore.set(null); }); });