import { describe, it, expect, vi, beforeEach } from "vitest"; import { getDyadWriteTags, getDyadRenameTags, getDyadDeleteTags, processFullResponseActions, getDyadAddDependencyTags, } from "../ipc/processors/response_processor"; import fs from "node:fs"; import git from "isomorphic-git"; import { db } from "../db"; // Mock fs with default export vi.mock("node:fs", async () => { return { default: { mkdirSync: vi.fn(), writeFileSync: vi.fn(), existsSync: vi.fn(), renameSync: vi.fn(), unlinkSync: vi.fn(), lstatSync: vi.fn().mockReturnValue({ isDirectory: () => false }), promises: { readFile: vi.fn().mockResolvedValue(""), }, }, }; }); // Mock isomorphic-git vi.mock("isomorphic-git", () => ({ default: { add: vi.fn().mockResolvedValue(undefined), remove: vi.fn().mockResolvedValue(undefined), commit: vi.fn().mockResolvedValue(undefined), statusMatrix: vi.fn().mockResolvedValue([]), }, })); // Mock paths module to control getDyadAppPath vi.mock("../paths/paths", () => ({ getDyadAppPath: vi.fn().mockImplementation((appPath) => { return `/mock/user/data/path/${appPath}`; }), getUserDataPath: vi.fn().mockReturnValue("/mock/user/data/path"), })); // Mock db vi.mock("../db", () => ({ db: { query: { chats: { findFirst: vi.fn(), }, messages: { findFirst: vi.fn(), }, }, update: vi.fn(() => ({ set: vi.fn(() => ({ where: vi.fn().mockResolvedValue(undefined), })), })), }, })); describe("getDyadAddDependencyTags", () => { it("should return an empty array when no dyad-add-dependency tags are found", () => { const result = getDyadAddDependencyTags("No dyad-add-dependency tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-add-dependency tags", () => { const result = getDyadAddDependencyTags( ``, ); expect(result).toEqual(["uuid"]); }); it("should return all the packages in the dyad-add-dependency tags", () => { const result = getDyadAddDependencyTags( ``, ); expect(result).toEqual(["pkg1", "pkg2"]); }); it("should return all the packages in the dyad-add-dependency tags", () => { const result = getDyadAddDependencyTags( `txt beforetext after`, ); expect(result).toEqual(["pkg1", "pkg2"]); }); it("should return all the packages in multiple dyad-add-dependency tags", () => { const result = getDyadAddDependencyTags( `txt beforetxt betweentext after`, ); expect(result).toEqual(["pkg1", "pkg2", "pkg3"]); }); }); describe("getDyadWriteTags", () => { it("should return an empty array when no dyad-write tags are found", () => { const result = getDyadWriteTags("No dyad-write tags here"); expect(result).toEqual([]); }); it("should return a dyad-write tag", () => { const result = getDyadWriteTags(` import React from "react"; console.log("TodoItem"); `); expect(result).toEqual([ { path: "src/components/TodoItem.tsx", description: "Creating a component for individual todo items", content: `import React from "react"; console.log("TodoItem");`, }, ]); }); it("should strip out code fence (if needed) from a dyad-write tag", () => { const result = getDyadWriteTags(` \`\`\`tsx import React from "react"; console.log("TodoItem"); \`\`\` `); expect(result).toEqual([ { path: "src/components/TodoItem.tsx", description: "Creating a component for individual todo items", content: `import React from "react"; console.log("TodoItem");`, }, ]); }); it("should return an array of dyad-write tags", () => { const result = getDyadWriteTags( `I'll create a simple todo list app using React, TypeScript, and shadcn/ui components. Let's get started! First, I'll create the necessary files for our todo list application: export interface Todo { id: string; text: string; completed: boolean; createdAt: Date; } import React from "react"; import { Trash2, Check, X } from "lucide-react"; import { Todo } from "../types/todo"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; interface TodoItemProps { todo: Todo; onToggle: (id: string) => void; onDelete: (id: string) => void; } const TodoItem: React.FC = ({ todo, onToggle, onDelete }) => { return (
{todo.text}
); }; export default TodoItem;
import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { PlusCircle } from "lucide-react"; interface TodoFormProps { onAddTodo: (text: string) => void; } const TodoForm: React.FC = ({ onAddTodo }) => { const [text, setText] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (text.trim()) { onAddTodo(text.trim()); setText(""); } }; return (
setText(e.target.value)} placeholder="Add a new task..." className="flex-1" />
); }; export default TodoForm;
import React from "react"; import { Todo } from "../types/todo"; import TodoItem from "./TodoItem"; import { ScrollArea } from "@/components/ui/scroll-area"; interface TodoListProps { todos: Todo[]; onToggle: (id: string) => void; onDelete: (id: string) => void; } const TodoList: React.FC = ({ todos, onToggle, onDelete }) => { if (todos.length === 0) { return (
No tasks yet. Add one above!
); } return (
{todos.map((todo) => ( ))}
); }; export default TodoList;
import React from "react"; import { Todo } from "../types/todo"; import { Card, CardContent } from "@/components/ui/card"; interface TodoStatsProps { todos: Todo[]; } const TodoStats: React.FC = ({ todos }) => { const total = todos.length; const completed = todos.filter((todo) => todo.completed).length; const pending = total - completed; const percentComplete = total > 0 ? Math.round((completed / total) * 100) : 0; return (

Total Tasks

{total}

Completed

{completed}

Pending

{pending}

Progress {percentComplete}%
); }; export default TodoStats; import React, { useState, useEffect } from "react"; import { v4 as uuidv4 } from "uuid"; import { Todo } from "../types/todo"; import TodoForm from "../components/TodoForm"; import TodoList from "../components/TodoList"; import TodoStats from "../components/TodoStats"; import { Toaster } from "@/components/ui/toaster"; import { useToast } from "@/components/ui/use-toast"; const Index: React.FC = () => { const [todos, setTodos] = useState(() => { const savedTodos = localStorage.getItem("todos"); if (savedTodos) { try { // Parse the saved todos and convert string dates back to Date objects return JSON.parse(savedTodos).map((todo: any) => ({ ...todo, createdAt: new Date(todo.createdAt) })); } catch (e) { console.error("Failed to parse saved todos", e); return []; } } return []; }); const { toast } = useToast(); // Save todos to localStorage whenever they change useEffect(() => { localStorage.setItem("todos", JSON.stringify(todos)); }, [todos]); const handleAddTodo = (text: string) => { const newTodo: Todo = { id: uuidv4(), text, completed: false, createdAt: new Date() }; setTodos([...todos, newTodo]); toast({ title: "Task added", description: "Your new task has been added successfully.", }); }; const handleToggleTodo = (id: string) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); const todo = todos.find(t => t.id === id); if (todo) { toast({ title: todo.completed ? "Task marked as incomplete" : "Task completed", description: todo.text, }); } }; const handleDeleteTodo = (id: string) => { const todoToDelete = todos.find(todo => todo.id === id); setTodos(todos.filter((todo) => todo.id !== id)); if (todoToDelete) { toast({ title: "Task deleted", description: todoToDelete.text, variant: "destructive", }); } }; return (

Todo List

Keep track of your tasks and stay organized

); }; export default Index;
declare module 'uuid' { export function v4(): string; } I've created a complete todo list application with the ability to add, complete, and delete tasks. The app includes statistics and uses local storage to persist data.`, ); expect(result.length).toEqual(7); }); }); describe("getDyadRenameTags", () => { it("should return an empty array when no dyad-rename tags are found", () => { const result = getDyadRenameTags("No dyad-rename tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-rename tags", () => { const result = getDyadRenameTags( ` `, ); expect(result).toEqual([ { from: "src/components/UserProfile.jsx", to: "src/components/ProfileCard.jsx", }, { from: "src/utils/helpers.js", to: "src/utils/utils.js" }, ]); }); }); describe("getDyadDeleteTags", () => { it("should return an empty array when no dyad-delete tags are found", () => { const result = getDyadDeleteTags("No dyad-delete tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-delete paths", () => { const result = getDyadDeleteTags( ` `, ); expect(result).toEqual([ "src/components/Analytics.jsx", "src/utils/unused.js", ]); }); }); describe("processFullResponse", () => { beforeEach(() => { vi.clearAllMocks(); // Mock db query response vi.mocked(db.query.chats.findFirst).mockResolvedValue({ id: 1, appId: 1, title: "Test Chat", createdAt: new Date(), app: { id: 1, name: "Mock App", path: "mock-app-path", createdAt: new Date(), updatedAt: new Date(), }, messages: [], } as any); vi.mocked(db.query.messages.findFirst).mockResolvedValue({ id: 1, chatId: 1, role: "assistant", content: "some content", createdAt: new Date(), approvalState: null, commitHash: null, } as any); // Default mock for existsSync to return true vi.mocked(fs.existsSync).mockReturnValue(true); }); it("should return empty object when no dyad-write tags are found", async () => { const result = await processFullResponseActions( "No dyad-write tags here", 1, { chatSummary: undefined, messageId: 1, }, ); expect(result).toEqual({ updatedFiles: false, extraFiles: undefined, extraFilesError: undefined, }); expect(fs.mkdirSync).not.toHaveBeenCalled(); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); it("should process dyad-write tags and create files", async () => { // Set up fs mocks to succeed vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); const response = `console.log('Hello');`; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(fs.mkdirSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src", { recursive: true }, ); expect(fs.writeFileSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/file1.js", "console.log('Hello');", ); expect(git.add).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/file1.js", }), ); expect(git.commit).toHaveBeenCalled(); expect(result).toEqual({ updatedFiles: true }); }); it("should handle file system errors gracefully", async () => { // Set up the mock to throw an error on mkdirSync vi.mocked(fs.mkdirSync).mockImplementationOnce(() => { throw new Error("Mock filesystem error"); }); const response = `This will fail`; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(result).toHaveProperty("error"); expect(result.error).toContain("Mock filesystem error"); }); it("should process multiple dyad-write tags and commit all files", async () => { // Clear previous mock calls vi.clearAllMocks(); // Set up fs mocks to succeed vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); const response = ` console.log('First file'); export const add = (a, b) => a + b; import React from 'react'; export const Button = ({ children }) => ; `; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); // Check that directories were created for each file path expect(fs.mkdirSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src", { recursive: true }, ); expect(fs.mkdirSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/utils", { recursive: true }, ); expect(fs.mkdirSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components", { recursive: true }, ); // Using toHaveBeenNthCalledWith to check each specific call expect(fs.writeFileSync).toHaveBeenNthCalledWith( 1, "/mock/user/data/path/mock-app-path/src/file1.js", "console.log('First file');", ); expect(fs.writeFileSync).toHaveBeenNthCalledWith( 2, "/mock/user/data/path/mock-app-path/src/utils/file2.js", "export const add = (a, b) => a + b;", ); expect(fs.writeFileSync).toHaveBeenNthCalledWith( 3, "/mock/user/data/path/mock-app-path/src/components/Button.tsx", "import React from 'react';\n export const Button = ({ children }) => ;", ); // Verify git operations were called for each file expect(git.add).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/file1.js", }), ); expect(git.add).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/utils/file2.js", }), ); expect(git.add).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/components/Button.tsx", }), ); // Verify commit was called once after all files were added expect(git.commit).toHaveBeenCalledTimes(1); expect(result).toEqual({ updatedFiles: true }); }); it("should process dyad-rename tags and rename files", async () => { // Set up fs mocks to succeed vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.renameSync).mockImplementation(() => undefined); const response = ``; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(fs.mkdirSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components", { recursive: true }, ); expect(fs.renameSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components/OldComponent.jsx", "/mock/user/data/path/mock-app-path/src/components/NewComponent.jsx", ); expect(git.add).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/components/NewComponent.jsx", }), ); expect(git.remove).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/components/OldComponent.jsx", }), ); expect(git.commit).toHaveBeenCalled(); expect(result).toEqual({ updatedFiles: true }); }); it("should handle non-existent files during rename gracefully", async () => { // Set up the mock to return false for existsSync vi.mocked(fs.existsSync).mockReturnValue(false); const response = ``; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(fs.mkdirSync).toHaveBeenCalled(); expect(fs.renameSync).not.toHaveBeenCalled(); expect(git.commit).not.toHaveBeenCalled(); expect(result).toEqual({ updatedFiles: false, extraFiles: undefined, extraFilesError: undefined, }); }); it("should process dyad-delete tags and delete files", async () => { // Set up fs mocks to succeed vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.unlinkSync).mockImplementation(() => undefined); const response = ``; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(fs.unlinkSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components/Unused.jsx", ); expect(git.remove).toHaveBeenCalledWith( expect.objectContaining({ filepath: "src/components/Unused.jsx", }), ); expect(git.commit).toHaveBeenCalled(); expect(result).toEqual({ updatedFiles: true }); }); it("should handle non-existent files during delete gracefully", async () => { // Set up the mock to return false for existsSync vi.mocked(fs.existsSync).mockReturnValue(false); const response = ``; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); expect(fs.unlinkSync).not.toHaveBeenCalled(); expect(git.remove).not.toHaveBeenCalled(); expect(git.commit).not.toHaveBeenCalled(); expect(result).toEqual({ updatedFiles: false, extraFiles: undefined, extraFilesError: undefined, }); }); it("should process mixed operations (write, rename, delete) in one response", async () => { // Set up fs mocks to succeed vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); vi.mocked(fs.renameSync).mockImplementation(() => undefined); vi.mocked(fs.unlinkSync).mockImplementation(() => undefined); const response = ` import React from 'react'; export default () =>
New
;
`; const result = await processFullResponseActions(response, 1, { chatSummary: undefined, messageId: 1, }); // Check write operation happened expect(fs.writeFileSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components/NewComponent.jsx", "import React from 'react'; export default () =>
New
;", ); // Check rename operation happened expect(fs.renameSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components/OldComponent.jsx", "/mock/user/data/path/mock-app-path/src/components/RenamedComponent.jsx", ); // Check delete operation happened expect(fs.unlinkSync).toHaveBeenCalledWith( "/mock/user/data/path/mock-app-path/src/components/Unused.jsx", ); // Check git operations expect(git.add).toHaveBeenCalledTimes(2); // For the write and rename expect(git.remove).toHaveBeenCalledTimes(2); // For the rename and delete // Check the commit message includes all operations expect(git.commit).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining( "wrote 1 file(s), renamed 1 file(s), deleted 1 file(s)", ), }), ); expect(result).toEqual({ updatedFiles: true }); }); });