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 (
);
};
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 (
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 });
});
});