import { Task } from "@/model/task"; import { ITaskRepository } from "@/ports"; import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { beforeEach, describe, expect, it } from "vitest"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { FsTaskRepository } from "./FsTaskRepository"; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; describe("FsTaskRepository", () => { let fs: InMemoryFileSystem; let repo: ITaskRepository; let clockStep: number; const folder = "/tasks"; const t0 = "2026-01-01T00:00:00.000Z"; const t1 = "2026-01-01T00:00:01.000Z"; const t2 = "2026-01-01T00:00:02.000Z"; const sampleTask = { title: "Fix login bug", description: "Token validation error", status: "todo" as const, priority: "high" as const, tags: ["bug", "frontend"], assignee: "ivanov", }; beforeEach(() => { fs = new InMemoryFileSystem(); clockStep = 0; const timestamps = [t0, t1, t2]; repo = new FsTaskRepository(fs, TASK_FILE_EXTENSION, () => { const value = timestamps[clockStep] ?? t2; clockStep += 1; return value; }); }); it("creates a task and writes it as a task file", async () => { const task = await repo.create(folder, sampleTask); expect(task.id).toMatch(UUID_RE); expect(task.title).toBe("Fix login bug"); expect(task.created).toBe(t0); expect(task.updated).toBe(t0); const files = await fs.readdir(folder); expect(files).toEqual([`fix-login-bug${TASK_FILE_EXTENSION}`]); const content = await fs.readFile( `${folder}/fix-login-bug${TASK_FILE_EXTENSION}`, ); expect(content).toContain("Fix login bug"); expect(content).toContain("status: todo"); }); it("list returns created tasks sorted by updated desc", async () => { await repo.create(folder, { ...sampleTask, title: "A" }); await repo.create(folder, { ...sampleTask, title: "B" }); const tasks = await repo.list(folder); expect(tasks).toHaveLength(2); expect(tasks[0].title).toBe("B"); expect(tasks[0].updated).toBe(t1); expect(tasks[1].title).toBe("A"); expect(tasks[1].updated).toBe(t0); }); it("getById returns the correct task", async () => { const created = await repo.create(folder, sampleTask); const found = await repo.getById(folder, created.id); expect(found).toEqual(created); }); it("getById returns undefined for unknown id", async () => { const found = await repo.getById(folder, "nonexistent"); expect(found).toBeUndefined(); }); it("update modifies fields and bumps updated timestamp", async () => { const task = await repo.create(folder, sampleTask); const updated: Task = { ...task, status: "done" }; await repo.update(folder, updated); const found = await repo.getById(folder, task.id); expect(found!.status).toBe("done"); expect(found!.title).toBe(task.title); expect(found!.updated).toBe(t1); expect(found!.created).toBe(t0); }); it("update renames file when title changes", async () => { const task = await repo.create(folder, sampleTask); expect(await fs.readdir(folder)).toEqual([ `fix-login-bug${TASK_FILE_EXTENSION}`, ]); await repo.update(folder, { ...task, title: "Fixed!" }); const files = await fs.readdir(folder); expect(files).toEqual([`fixed${TASK_FILE_EXTENSION}`]); const found = await repo.getById(folder, task.id); expect(found!.title).toBe("Fixed!"); expect(found!.updated).toBe(t1); }); it("update of unknown id writes a new file", async () => { const orphan: Task = { ...sampleTask, id: "orphan-id", title: "Orphan", created: t0, updated: t0, }; await repo.update(folder, orphan); const found = await repo.getById(folder, "orphan-id"); expect(found).toMatchObject({ id: "orphan-id", title: "Orphan", updated: t0, }); expect(await fs.readdir(folder)).toEqual([`orphan${TASK_FILE_EXTENSION}`]); }); it("delete removes the task file", async () => { const task = await repo.create(folder, sampleTask); await repo.delete(folder, task.id); const tasks = await repo.list(folder); expect(tasks).toHaveLength(0); expect(await fs.readdir(folder)).toEqual([]); }); it("delete is a no-op for unknown id", async () => { await expect(repo.delete(folder, "nope")).resolves.toBeUndefined(); }); it("list returns empty array for non-existent folder", async () => { const tasks = await repo.list("/nonexistent"); expect(tasks).toEqual([]); }); it("filters files by extension", async () => { await fs.writeFile(`${folder}/notes.txt`, "hello"); await fs.writeFile(`${folder}/chore.md`, "---\nid: x\n---\n"); await repo.create(folder, sampleTask); const tasks = await repo.list(folder); expect(tasks).toHaveLength(1); }); it("list returns empty when only non-matching extensions exist", async () => { await fs.writeFile(`${folder}/notes.txt`, "hello"); await fs.writeFile(`${folder}/chore.md`, "---\nid: x\n---\n"); const tasks = await repo.list(folder); expect(tasks).toEqual([]); }); it("skips files without id", async () => { await fs.writeFile( `${folder}/no-id${TASK_FILE_EXTENSION}`, "not valid frontmatter", ); await fs.writeFile( `${folder}/empty-id${TASK_FILE_EXTENSION}`, "---\ntitle: No Id\n---\n", ); await repo.create(folder, sampleTask); const tasks = await repo.list(folder); expect(tasks).toHaveLength(1); expect(tasks[0].title).toBe("Fix login bug"); }); it("create with same title overwrites the previous file", async () => { const first = await repo.create(folder, sampleTask); const second = await repo.create(folder, { ...sampleTask, description: "second", }); const files = await fs.readdir(folder); expect(files).toEqual([`fix-login-bug${TASK_FILE_EXTENSION}`]); const tasks = await repo.list(folder); expect(tasks).toHaveLength(1); expect(tasks[0].id).toBe(second.id); expect(tasks[0].id).not.toBe(first.id); expect(tasks[0].description).toBe("second"); }); it("creates task with Cyrillic title and slug", async () => { const task = await repo.create(folder, { ...sampleTask, title: "Тестовая задача", }); expect(task.title).toBe("Тестовая задача"); const files = await fs.readdir(folder); expect(files).toEqual([`тестовая-задача${TASK_FILE_EXTENSION}`]); }); it("creates task with title exactly at MAX_TASK_TITLE_LENGTH", async () => { const title = "a".repeat(80); const task = await repo.create(folder, { ...sampleTask, title }); expect(task.title).toBe(title); const files = await fs.readdir(folder); expect(files).toEqual([`${title}${TASK_FILE_EXTENSION}`]); const found = await repo.getById(folder, task.id); expect(found?.title).toBe(title); }); it("creates task with tags containing special characters", async () => { const task = await repo.create(folder, { ...sampleTask, tags: ["c#", "c++", "tag name", "tag:value"], }); const files = await fs.readdir(folder); expect(files).toHaveLength(1); const content = await fs.readFile( `${folder}/fix-login-bug${TASK_FILE_EXTENSION}`, ); expect(content).toContain("c#"); expect(content).toContain("tag:value"); }); it("getFilePath produces consistent slug regardless of special characters", async () => { const task = await repo.create(folder, { ...sampleTask, title: "Fix___bug...", }); const files = await fs.readdir(folder); expect(files).toEqual([`fix-bug${TASK_FILE_EXTENSION}`]); }); });