diff --git a/src/dashboard/applyRawTaskContent.test.ts b/src/dashboard/applyRawTaskContent.test.ts index bad3b74..4169798 100644 --- a/src/dashboard/applyRawTaskContent.test.ts +++ b/src/dashboard/applyRawTaskContent.test.ts @@ -68,4 +68,44 @@ body if (result.isOk()) return; expect(result.error.variant).toBe(AppErrorVariant.INVALID_TASK_FILE); }); + + it("handles raw content with fields in different order", () => { + const raw = `--- +status: done +id: "should-be-ignored" +title: Reordered +priority: low +tags: [x, y] +assignee: "" +created: 2099-01-01T00:00:00.000Z +updated: 2026-01-02T00:00:00.000Z +--- +body text +`; + const result = applyRawTaskContent(raw, previous); + expect(result.isOk()).toBe(true); + if (result.isErr()) return; + expect(result.value.title).toBe("Reordered"); + expect(result.value.id).toBe(previous.id); + expect(result.value.created).toBe(previous.created); + expect(result.value.status).toBe(TaskStatus.DONE); + }); + + it("handles raw content with empty description body", () => { + const raw = `--- +id: "x" +title: No body +status: todo +priority: medium +tags: [] +assignee: "" +created: 2026-01-01T00:00:00.000Z +updated: 2026-01-01T00:00:00.000Z +--- +`; + const result = applyRawTaskContent(raw, previous); + expect(result.isOk()).toBe(true); + if (result.isErr()) return; + expect(result.value.description).toBe(""); + }); }); diff --git a/src/dashboard/buildDashboardView.test.ts b/src/dashboard/buildDashboardView.test.ts index 7ae93af..5a55808 100644 --- a/src/dashboard/buildDashboardView.test.ts +++ b/src/dashboard/buildDashboardView.test.ts @@ -80,4 +80,11 @@ describe("buildDashboardView", () => { const view = buildDashboardView(tasks, { selectedId: "missing" }); expect(view.selected).toBeUndefined(); }); + + it("handles empty tasks array", () => { + const view = buildDashboardView([]); + expect(view.visibleTasks).toEqual([]); + expect(view.groups).toEqual([]); + expect(view.selected).toBeUndefined(); + }); }); diff --git a/src/dashboard/filterTasks.test.ts b/src/dashboard/filterTasks.test.ts index 33fb1f7..62280c8 100644 --- a/src/dashboard/filterTasks.test.ts +++ b/src/dashboard/filterTasks.test.ts @@ -98,4 +98,9 @@ describe("filterTasks", () => { }); expect(result.map((t) => t.id)).toEqual(["4"]); }); + + it("returns empty array for empty input", () => { + expect(filterTasks([], {})).toEqual([]); + expect(filterTasks([], { statuses: ["todo"] })).toEqual([]); + }); }); diff --git a/src/dashboard/groupTasks.test.ts b/src/dashboard/groupTasks.test.ts index f8f811c..0abf42d 100644 --- a/src/dashboard/groupTasks.test.ts +++ b/src/dashboard/groupTasks.test.ts @@ -93,4 +93,30 @@ describe("groupTasks", () => { expect(groupTasks([], GroupBy.STATUS)).toEqual([]); expect(groupTasks([], GroupBy.NONE)).toEqual([]); }); + + it("silently drops tasks with unknown status from groups", () => { + const tasks: Task[] = [ + task({ id: "a", title: "A", status: "__garbage__" as TaskStatus }), + task({ id: "b", title: "B", status: TaskStatus.TODO }), + ]; + const groups = groupTasks(tasks, GroupBy.STATUS); + const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id)); + expect(allGrouped).toEqual(["b"]); + expect(allGrouped).not.toContain("a"); + }); + + it("silently drops tasks with unknown priority from groups", () => { + const tasks: Task[] = [ + task({ + id: "a", + title: "A", + priority: "__garbage__" as TaskPriority, + }), + task({ id: "b", title: "B", priority: TaskPriority.LOW }), + ]; + const groups = groupTasks(tasks, GroupBy.PRIORITY); + const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id)); + expect(allGrouped).toEqual(["b"]); + expect(allGrouped).not.toContain("a"); + }); }); diff --git a/src/dashboard/sortTasks.test.ts b/src/dashboard/sortTasks.test.ts index ea4d30a..3fc5ff5 100644 --- a/src/dashboard/sortTasks.test.ts +++ b/src/dashboard/sortTasks.test.ts @@ -61,4 +61,28 @@ describe("sortTasks", () => { sortTasks(tasks, TaskSortDirection.DESC); expect(tasks.map((t) => t.id)).toEqual(copy.map((t) => t.id)); }); + + it("returns empty array for empty input", () => { + expect(sortTasks([])).toEqual([]); + }); + + it("preserves input order when created dates are identical (stable sort)", () => { + const sameDate = "2026-01-01T00:00:00.000Z"; + const tasks: Task[] = [ + task({ id: "a", title: "A", created: sameDate }), + task({ id: "b", title: "B", created: sameDate }), + task({ id: "c", title: "C", created: sameDate }), + ]; + const result = sortTasks(tasks, TaskSortDirection.ASC); + expect(result.map((t) => t.id)).toEqual(["a", "b", "c"]); + }); + + it("handles invalid date strings without crashing", () => { + const tasks: Task[] = [ + task({ id: "a", title: "A", created: "invalid-date" }), + task({ id: "b", title: "B", created: "2026-01-01T00:00:00.000Z" }), + ]; + const result = sortTasks(tasks, TaskSortDirection.ASC); + expect(result).toHaveLength(2); + }); }); diff --git a/src/kanban/buildKanbanBoard.test.ts b/src/kanban/buildKanbanBoard.test.ts index b802d6e..5480428 100644 --- a/src/kanban/buildKanbanBoard.test.ts +++ b/src/kanban/buildKanbanBoard.test.ts @@ -117,4 +117,31 @@ describe("buildKanbanBoard", () => { expect(col.tasks).toEqual([]); } }); + + it("preserves input order for tasks with same updated timestamp (stable sort)", () => { + const sameUpdated = "2026-01-01T00:00:00.000Z"; + const tasks: Task[] = [ + task({ + id: "a", + title: "A", + status: TaskStatus.TODO, + updated: sameUpdated, + }), + task({ + id: "b", + title: "B", + status: TaskStatus.TODO, + updated: sameUpdated, + }), + task({ + id: "c", + title: "C", + status: TaskStatus.TODO, + updated: sameUpdated, + }), + ]; + const board = buildKanbanBoard(tasks); + const todo = board.columns.find((col) => col.status === TaskStatus.TODO); + expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]); + }); }); diff --git a/src/storage/FsTaskRepository.test.ts b/src/storage/FsTaskRepository.test.ts index 0858c5d..bc8eb66 100644 --- a/src/storage/FsTaskRepository.test.ts +++ b/src/storage/FsTaskRepository.test.ts @@ -195,4 +195,52 @@ describe("FsTaskRepository", () => { 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}`]); + }); });