From 148ed75841773b081891ceac7766934472e0993b Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 18 Jul 2026 07:48:11 +0500 Subject: [PATCH] feat: add unknown status and priority, change todo status to open --- src/commands/changeStatus.test.ts | 18 +++++----- src/commands/createTask.test.ts | 7 ++-- src/commands/createTask.ts | 6 ++-- src/commands/deleteTask.test.ts | 6 ++-- src/commands/listLocatedTasks.test.ts | 10 +++--- src/commands/loadDashboard.test.ts | 10 +++--- src/commands/openTask.test.ts | 6 ++-- src/commands/saveTaskDescription.test.ts | 14 ++++---- src/commands/saveTaskRaw.test.ts | 5 +-- src/dashboard/applyRawTaskContent.test.ts | 9 ++--- src/dashboard/buildDashboardView.test.ts | 24 +++++++------ src/dashboard/filterLocatedByScope.test.ts | 5 +-- src/dashboard/filterTasks.test.ts | 34 ++++++++++--------- src/dashboard/groupTasks.test.ts | 8 ++--- src/dashboard/sortTasks.test.ts | 5 +-- src/frontmatter/frontmatterFields.test.ts | 6 ++-- src/frontmatter/validateFrontmatter.test.ts | 4 +-- src/kanban/buildKanbanBoard.test.ts | 21 ++++++------ .../__snapshots__/taskPriority.test.ts.snap | 1 + .../__snapshots__/taskStatus.test.ts.snap | 3 +- src/model/taskPriority.test.ts | 1 + src/model/taskPriority.ts | 5 ++- src/model/taskStatus.test.ts | 5 +-- src/model/taskStatus.ts | 15 +++++--- src/storage/FsTaskRepository.test.ts | 16 +++++---- src/ui/resolveTaskFolder.ts | 3 +- src/utils/markdown.test.ts | 10 +++--- src/utils/markdown.ts | 16 ++++++--- 28 files changed, 162 insertions(+), 111 deletions(-) diff --git a/src/commands/changeStatus.test.ts b/src/commands/changeStatus.test.ts index 1b078bd..82901e1 100644 --- a/src/commands/changeStatus.test.ts +++ b/src/commands/changeStatus.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { changeStatus } from "./changeStatus"; @@ -18,8 +20,8 @@ describe("changeStatus", () => { const created = await repo.create(folder, { title: "Work item", description: "", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); @@ -27,23 +29,23 @@ describe("changeStatus", () => { const result = await changeStatus(repo, { folderPath: folder, id: created.id, - status: "in-progress", + status: TaskStatus.IN_PROGRESS, }); expect(result.isOk()).toBe(true); if (result.isErr()) return; - expect(result.value.status).toBe("in-progress"); + expect(result.value.status).toBe(TaskStatus.IN_PROGRESS); expect(result.value.id).toBe(created.id); const stored = await repo.getById(folder, created.id); - expect(stored?.status).toBe("in-progress"); + expect(stored?.status).toBe(TaskStatus.IN_PROGRESS); }); it("returns not-found for unknown id", async () => { const result = await changeStatus(repo, { folderPath: folder, id: "missing", - status: "done", + status: TaskStatus.DONE, }); expect(result).toEqual( err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })), @@ -54,7 +56,7 @@ describe("changeStatus", () => { const result = await changeStatus(repo, { folderPath: undefined, id: "x", - status: "done", + status: TaskStatus.DONE, }); expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER))); }); @@ -63,7 +65,7 @@ describe("changeStatus", () => { const result = await changeStatus(repo, { folderPath: folder, id: undefined, - status: "done", + status: TaskStatus.DONE, }); expect(result).toEqual(err(appError(AppErrorVariant.NO_ID))); }); diff --git a/src/commands/createTask.test.ts b/src/commands/createTask.test.ts index 76c7d42..1b3b028 100644 --- a/src/commands/createTask.test.ts +++ b/src/commands/createTask.test.ts @@ -3,6 +3,7 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { createTask } from "./createTask"; @@ -15,7 +16,7 @@ describe("createTask", () => { repo = new FsTaskRepository(new InMemoryFileSystem(), TASK_FILE_EXTENSION); }); - it("creates a todo/medium task from title", async () => { + it("creates an open/medium task from title", async () => { const result = await createTask(repo, { folderPath: folder, title: " New feature ", @@ -25,8 +26,8 @@ describe("createTask", () => { if (result.isErr()) return; expect(result.value.title).toBe("New feature"); - expect(result.value.status).toBe("todo"); - expect(result.value.priority).toBe("medium"); + expect(result.value.status).toBe(TaskStatus.OPEN); + expect(result.value.priority).toBe(TaskPriority.MEDIUM); expect(result.value.description).toBe(""); expect(result.value.tags).toEqual([]); expect(result.value.assignee).toBe(""); diff --git a/src/commands/createTask.ts b/src/commands/createTask.ts index b251dad..bbc23a5 100644 --- a/src/commands/createTask.ts +++ b/src/commands/createTask.ts @@ -8,7 +8,7 @@ import { ITaskRepository } from "@/ports"; export type CreateTaskInput = { folderPath?: string; title?: string; - /** Defaults to TODO when omitted. */ + /** Defaults to OPEN when omitted. */ status?: TaskStatus; }; @@ -27,10 +27,10 @@ export function createTask( const status = input.status === undefined - ? TaskStatus.TODO + ? TaskStatus.OPEN : isTaskStatus(input.status) ? input.status - : TaskStatus.TODO; + : TaskStatus.OPEN; return ResultAsync.fromSafePromise( repo.create(input.folderPath, { diff --git a/src/commands/deleteTask.test.ts b/src/commands/deleteTask.test.ts index bf8052a..4e0f712 100644 --- a/src/commands/deleteTask.test.ts +++ b/src/commands/deleteTask.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err, ok } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { deleteTask } from "./deleteTask"; @@ -18,8 +20,8 @@ describe("deleteTask", () => { const created = await repo.create(folder, { title: "To delete", description: "", - status: "todo", - priority: "medium", +status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); diff --git a/src/commands/listLocatedTasks.test.ts b/src/commands/listLocatedTasks.test.ts index f8757e1..43932b4 100644 --- a/src/commands/listLocatedTasks.test.ts +++ b/src/commands/listLocatedTasks.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { listLocatedTasks } from "./listLocatedTasks"; @@ -19,16 +21,16 @@ describe("listLocatedTasks", () => { const p = await repo.create(project, { title: "Project task", description: "", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); const g = await repo.create(global, { title: "Global task", description: "", - status: "todo", - priority: "high", + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, tags: [], assignee: "", }); diff --git a/src/commands/loadDashboard.test.ts b/src/commands/loadDashboard.test.ts index 4d7c3cc..92f6696 100644 --- a/src/commands/loadDashboard.test.ts +++ b/src/commands/loadDashboard.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { loadDashboard } from "./loadDashboard"; @@ -18,16 +20,16 @@ describe("loadDashboard", () => { const a = await repo.create(folder, { title: "First", description: "", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); const b = await repo.create(folder, { title: "Second", description: "body", - status: "done", - priority: "high", + status: TaskStatus.DONE, + priority: TaskPriority.HIGH, tags: ["x"], assignee: "", }); diff --git a/src/commands/openTask.test.ts b/src/commands/openTask.test.ts index 7411eaf..2c9baf3 100644 --- a/src/commands/openTask.test.ts +++ b/src/commands/openTask.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { openTask } from "./openTask"; @@ -18,8 +20,8 @@ describe("openTask", () => { const created = await repo.create(folder, { title: "Open me", description: "body", - status: "todo", - priority: "high", + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, tags: [], assignee: "", }); diff --git a/src/commands/saveTaskDescription.test.ts b/src/commands/saveTaskDescription.test.ts index e061089..58c0a3a 100644 --- a/src/commands/saveTaskDescription.test.ts +++ b/src/commands/saveTaskDescription.test.ts @@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { saveTaskDescription } from "./saveTaskDescription"; @@ -18,8 +20,8 @@ describe("saveTaskDescription", () => { const created = await repo.create(folder, { title: "Editable", description: "old body", - status: "todo", - priority: "high", + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, tags: ["keep"], assignee: "alice", }); @@ -36,8 +38,8 @@ describe("saveTaskDescription", () => { expect(result.value.id).toBe(created.id); expect(result.value.description).toBe("new markdown body"); expect(result.value.title).toBe("Editable"); - expect(result.value.status).toBe("todo"); - expect(result.value.priority).toBe("high"); + expect(result.value.status).toBe(TaskStatus.OPEN); + expect(result.value.priority).toBe(TaskPriority.HIGH); expect(result.value.tags).toEqual(["keep"]); expect(result.value.assignee).toBe("alice"); @@ -49,8 +51,8 @@ describe("saveTaskDescription", () => { const created = await repo.create(folder, { title: "Clear me", description: "something", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); diff --git a/src/commands/saveTaskRaw.test.ts b/src/commands/saveTaskRaw.test.ts index 473a70c..29f02ce 100644 --- a/src/commands/saveTaskRaw.test.ts +++ b/src/commands/saveTaskRaw.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { err } from "neverthrow"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { appError, AppErrorVariant } from "@/error"; +import { TaskPriority } from "@/model/taskPriority"; import { TaskStatus } from "@/model/taskStatus"; import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { serializeTask } from "@/utils/markdown"; @@ -20,8 +21,8 @@ describe("saveTaskRaw", () => { const created = await repo.create(folder, { title: "Raw me", description: "old", - status: TaskStatus.TODO, - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", }); diff --git a/src/dashboard/applyRawTaskContent.test.ts b/src/dashboard/applyRawTaskContent.test.ts index 4169798..d37bbbf 100644 --- a/src/dashboard/applyRawTaskContent.test.ts +++ b/src/dashboard/applyRawTaskContent.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { err } from "neverthrow"; import { appError, AppErrorVariant } from "@/error"; import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; import { TaskStatus } from "@/model/taskStatus"; import { serializeTask } from "@/utils/markdown"; import { applyRawTaskContent } from "./applyRawTaskContent"; @@ -10,8 +11,8 @@ const previous: Task = { id: "keep-me", title: "Old title", description: "old body", - status: TaskStatus.TODO, - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", created: "2026-01-01T00:00:00.000Z", @@ -26,7 +27,7 @@ describe("applyRawTaskContent", () => { title: "New title", description: "new body", status: TaskStatus.DONE, - priority: "high", + priority: TaskPriority.HIGH, tags: ["a"], assignee: "bob", created: "2099-01-01T00:00:00.000Z", @@ -41,7 +42,7 @@ describe("applyRawTaskContent", () => { expect(result.value.title).toBe("New title"); expect(result.value.description).toBe("new body"); expect(result.value.status).toBe(TaskStatus.DONE); - expect(result.value.priority).toBe("high"); + expect(result.value.priority).toBe(TaskPriority.HIGH); expect(result.value.tags).toEqual(["a"]); expect(result.value.assignee).toBe("bob"); }); diff --git a/src/dashboard/buildDashboardView.test.ts b/src/dashboard/buildDashboardView.test.ts index 5a55808..3f56c33 100644 --- a/src/dashboard/buildDashboardView.test.ts +++ b/src/dashboard/buildDashboardView.test.ts @@ -1,13 +1,15 @@ import { describe, expect, it } from "vitest"; import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { GroupBy } from "./groupBy"; import { buildDashboardView } from "./buildDashboardView"; function task(partial: Partial & Pick): Task { return { description: "", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", created: "2026-01-01T00:00:00.000Z", @@ -21,22 +23,22 @@ describe("buildDashboardView", () => { task({ id: "1", title: "Alpha", - status: "todo", - priority: "high", + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, description: "first", }), task({ id: "2", title: "Beta", - status: "in-progress", - priority: "low", + status: TaskStatus.IN_PROGRESS, + priority: TaskPriority.LOW, description: "second", }), task({ id: "3", title: "Gamma", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, description: "third", }), ]; @@ -52,12 +54,12 @@ describe("buildDashboardView", () => { it("applies filter then groups", () => { const view = buildDashboardView(tasks, { - filter: { statuses: ["todo"] }, + filter: { statuses: [TaskStatus.OPEN] }, groupBy: GroupBy.PRIORITY, }); expect(view.visibleTasks.map((t) => t.id)).toEqual(["1", "3"]); - expect(view.groups.map((g) => g.key)).toEqual(["high", "medium"]); + expect(view.groups.map((g) => g.key)).toEqual([TaskPriority.HIGH, TaskPriority.MEDIUM]); }); it("resolves selected task by id from the full task list", () => { @@ -68,7 +70,7 @@ describe("buildDashboardView", () => { it("keeps selection even when the task is filtered out of the list", () => { const view = buildDashboardView(tasks, { - filter: { statuses: ["todo"] }, + filter: { statuses: [TaskStatus.OPEN] }, selectedId: "2", }); diff --git a/src/dashboard/filterLocatedByScope.test.ts b/src/dashboard/filterLocatedByScope.test.ts index 55864ba..107ac1f 100644 --- a/src/dashboard/filterLocatedByScope.test.ts +++ b/src/dashboard/filterLocatedByScope.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { Task } from "@/model/task"; import { LocatedTask, TaskScope } from "@/model/taskLocation"; +import { TaskPriority } from "@/model/taskPriority"; import { TaskStatus } from "@/model/taskStatus"; import { filterLocatedByScope } from "./filterLocatedByScope"; @@ -13,8 +14,8 @@ function located( id, title: id, description: "", - status: TaskStatus.TODO, - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", created: "2026-01-01T00:00:00.000Z", diff --git a/src/dashboard/filterTasks.test.ts b/src/dashboard/filterTasks.test.ts index 62280c8..1ecf2f1 100644 --- a/src/dashboard/filterTasks.test.ts +++ b/src/dashboard/filterTasks.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "vitest"; import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { filterTasks } from "./filterTasks"; function task(partial: Partial & Pick): Task { return { description: "", - status: "todo", - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", created: "2026-01-01T00:00:00.000Z", @@ -21,32 +23,32 @@ describe("filterTasks", () => { id: "1", title: "Fix login", description: "Auth token expired", - status: "todo", - priority: "high", + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, tags: ["bug", "frontend"], }), task({ id: "2", title: "Add dark mode", description: "Theme toggle", - status: "in-progress", - priority: "medium", + status: TaskStatus.IN_PROGRESS, + priority: TaskPriority.MEDIUM, tags: ["feature", "frontend"], }), task({ id: "3", title: "Write docs", description: "API reference", - status: "done", - priority: "low", + status: TaskStatus.DONE, + priority: TaskPriority.LOW, tags: ["docs"], }), task({ id: "4", title: "Deploy hotfix", description: "Production login outage", - status: "todo", - priority: "critical", + status: TaskStatus.OPEN, + priority: TaskPriority.CRITICAL, tags: ["bug", "ops"], }), ]; @@ -56,17 +58,17 @@ describe("filterTasks", () => { }); it("filters by a single status", () => { - const result = filterTasks(tasks, { statuses: ["todo"] }); + const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN] }); expect(result.map((t) => t.id)).toEqual(["1", "4"]); }); it("filters by multiple statuses", () => { - const result = filterTasks(tasks, { statuses: ["todo", "done"] }); + const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN, TaskStatus.DONE] }); expect(result.map((t) => t.id)).toEqual(["1", "3", "4"]); }); it("filters by priority", () => { - const result = filterTasks(tasks, { priorities: ["high", "critical"] }); + const result = filterTasks(tasks, { priorities: [TaskPriority.HIGH, TaskPriority.CRITICAL] }); expect(result.map((t) => t.id)).toEqual(["1", "4"]); }); @@ -92,8 +94,8 @@ describe("filterTasks", () => { it("combines status, priority and query with AND", () => { const result = filterTasks(tasks, { - statuses: ["todo"], - priorities: ["critical"], + statuses: [TaskStatus.OPEN], + priorities: [TaskPriority.CRITICAL], query: "login", }); expect(result.map((t) => t.id)).toEqual(["4"]); @@ -101,6 +103,6 @@ describe("filterTasks", () => { it("returns empty array for empty input", () => { expect(filterTasks([], {})).toEqual([]); - expect(filterTasks([], { statuses: ["todo"] })).toEqual([]); + expect(filterTasks([], { statuses: [TaskStatus.OPEN] })).toEqual([]); }); }); diff --git a/src/dashboard/groupTasks.test.ts b/src/dashboard/groupTasks.test.ts index 0abf42d..f948133 100644 --- a/src/dashboard/groupTasks.test.ts +++ b/src/dashboard/groupTasks.test.ts @@ -16,7 +16,7 @@ import { groupTasks } from "./groupTasks"; function task(partial: Partial & Pick): Task { return { description: "", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, priority: TaskPriority.MEDIUM, tags: [], assignee: "", @@ -37,13 +37,13 @@ describe("groupTasks", () => { task({ id: "b", title: "B", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, priority: TaskPriority.CRITICAL, }), task({ id: "c", title: "C", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, priority: TaskPriority.HIGH, }), task({ @@ -97,7 +97,7 @@ describe("groupTasks", () => { 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 }), + task({ id: "b", title: "B", status: TaskStatus.OPEN }), ]; const groups = groupTasks(tasks, GroupBy.STATUS); const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id)); diff --git a/src/dashboard/sortTasks.test.ts b/src/dashboard/sortTasks.test.ts index 3fc5ff5..57ee87a 100644 --- a/src/dashboard/sortTasks.test.ts +++ b/src/dashboard/sortTasks.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; import { TaskStatus } from "@/model/taskStatus"; import { sortTasks, TaskSortDirection } from "./sortTasks"; @@ -8,8 +9,8 @@ function task( ): Task { return { description: "", - status: TaskStatus.TODO, - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", updated: "2026-01-01T00:00:00.000Z", diff --git a/src/frontmatter/frontmatterFields.test.ts b/src/frontmatter/frontmatterFields.test.ts index 5fd838f..85eecef 100644 --- a/src/frontmatter/frontmatterFields.test.ts +++ b/src/frontmatter/frontmatterFields.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { extractFrontmatterBlock, findFieldRange, @@ -20,7 +22,7 @@ Body describe("replaceFrontmatterField", () => { it("replaces status value", () => { - const result = replaceFrontmatterField(sample, "status", "done"); + const result = replaceFrontmatterField(sample, "status", TaskStatus.DONE); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.content).toContain("status: done"); @@ -29,7 +31,7 @@ describe("replaceFrontmatterField", () => { }); it("replaces priority value", () => { - const result = replaceFrontmatterField(sample, "priority", "critical"); + const result = replaceFrontmatterField(sample, "priority", TaskPriority.CRITICAL); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.content).toContain("priority: critical"); diff --git a/src/frontmatter/validateFrontmatter.test.ts b/src/frontmatter/validateFrontmatter.test.ts index a3bd93c..ff022c4 100644 --- a/src/frontmatter/validateFrontmatter.test.ts +++ b/src/frontmatter/validateFrontmatter.test.ts @@ -5,7 +5,7 @@ import { validateFrontmatter } from "./validateFrontmatter"; const valid = `--- id: "550e8400-e29b-41d4-a716-446655440000" title: Fix login -status: todo +status: open priority: high tags: [bug] assignee: "" @@ -34,7 +34,7 @@ describe("validateFrontmatter", () => { }); it("errors on invalid status", () => { - const content = valid.replace("status: todo", "status: nope"); + const content = valid.replace("status: open", "status: nope"); const issues = validateFrontmatter(content); const statusIssue = issues.find((i) => i.field === "status"); expect(statusIssue).toBeDefined(); diff --git a/src/kanban/buildKanbanBoard.test.ts b/src/kanban/buildKanbanBoard.test.ts index 5480428..ad0e609 100644 --- a/src/kanban/buildKanbanBoard.test.ts +++ b/src/kanban/buildKanbanBoard.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; import { TASK_STATUS_META, TASK_STATUS_ORDER, @@ -10,8 +11,8 @@ import { buildKanbanBoard } from "./buildKanbanBoard"; function task(partial: Partial & Pick): Task { return { description: "", - status: TaskStatus.TODO, - priority: "medium", + status: TaskStatus.OPEN, + priority: TaskPriority.MEDIUM, tags: [], assignee: "", created: "2026-01-01T00:00:00.000Z", @@ -35,13 +36,13 @@ describe("buildKanbanBoard", () => { task({ id: "b", title: "Todo older", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, updated: "2026-01-01T00:00:01.000Z", }), task({ id: "c", title: "Todo newer", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, updated: "2026-01-01T00:00:03.000Z", }), task({ @@ -96,7 +97,7 @@ describe("buildKanbanBoard", () => { const byStatus = Object.fromEntries( board.columns.map((col) => [col.status, col.tasks.map((t) => t.id)]), ); - expect(byStatus[TaskStatus.TODO]).toEqual(["c", "b"]); + expect(byStatus[TaskStatus.OPEN]).toEqual(["c", "b"]); expect(byStatus[TaskStatus.IN_PROGRESS]).toEqual(["d"]); expect(byStatus[TaskStatus.DONE]).toEqual(["a"]); expect(byStatus[TaskStatus.BACKLOG]).toEqual(["e"]); @@ -105,7 +106,7 @@ describe("buildKanbanBoard", () => { it("sorts tasks within a column by updated desc", () => { const board = buildKanbanBoard(tasks); - const todo = board.columns.find((col) => col.status === TaskStatus.TODO); + const todo = board.columns.find((col) => col.status === TaskStatus.OPEN); expect(todo?.tasks.map((t) => t.id)).toEqual(["c", "b"]); }); @@ -124,24 +125,24 @@ describe("buildKanbanBoard", () => { task({ id: "a", title: "A", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, updated: sameUpdated, }), task({ id: "b", title: "B", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, updated: sameUpdated, }), task({ id: "c", title: "C", - status: TaskStatus.TODO, + status: TaskStatus.OPEN, updated: sameUpdated, }), ]; const board = buildKanbanBoard(tasks); - const todo = board.columns.find((col) => col.status === TaskStatus.TODO); + const todo = board.columns.find((col) => col.status === TaskStatus.OPEN); expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]); }); }); diff --git a/src/model/__snapshots__/taskPriority.test.ts.snap b/src/model/__snapshots__/taskPriority.test.ts.snap index c0f643a..3b984c5 100644 --- a/src/model/__snapshots__/taskPriority.test.ts.snap +++ b/src/model/__snapshots__/taskPriority.test.ts.snap @@ -6,5 +6,6 @@ exports[`TaskPriority > exposes wire values on const keys 1`] = ` "HIGH": "high", "LOW": "low", "MEDIUM": "medium", + "UNKNOWN": "unknown", } `; diff --git a/src/model/__snapshots__/taskStatus.test.ts.snap b/src/model/__snapshots__/taskStatus.test.ts.snap index 667b111..93d50a0 100644 --- a/src/model/__snapshots__/taskStatus.test.ts.snap +++ b/src/model/__snapshots__/taskStatus.test.ts.snap @@ -6,6 +6,7 @@ exports[`TaskStatus > exposes wire values on const keys 1`] = ` "CANCELLED": "cancelled", "DONE": "done", "IN_PROGRESS": "in-progress", - "TODO": "todo", + "OPEN": "open", + "UNKNOWN": "unknown", } `; diff --git a/src/model/taskPriority.test.ts b/src/model/taskPriority.test.ts index 7a93e0d..0d782e9 100644 --- a/src/model/taskPriority.test.ts +++ b/src/model/taskPriority.test.ts @@ -23,6 +23,7 @@ describe("TaskPriority", () => { it("isTaskPriority accepts known values only", () => { expect(isTaskPriority(TaskPriority.HIGH)).toBe(true); + expect(isTaskPriority(TaskPriority.UNKNOWN)).toBe(true); expect(isTaskPriority("__not_a_priority__")).toBe(false); }); }); diff --git a/src/model/taskPriority.ts b/src/model/taskPriority.ts index 7988d3c..be3adb5 100644 --- a/src/model/taskPriority.ts +++ b/src/model/taskPriority.ts @@ -3,6 +3,7 @@ export const TaskPriority = { HIGH: "high", MEDIUM: "medium", LOW: "low", + UNKNOWN: "unknown", } as const; export type TaskPriority = (typeof TaskPriority)[keyof typeof TaskPriority]; @@ -11,12 +12,13 @@ export type TaskPriorityMeta = { label: string; }; -/** Explicit order for filters / grouping (critical → low). */ +/** Explicit order for filters / grouping (critical → low → unknown). */ export const TASK_PRIORITY_ORDER: TaskPriority[] = [ TaskPriority.CRITICAL, TaskPriority.HIGH, TaskPriority.MEDIUM, TaskPriority.LOW, + TaskPriority.UNKNOWN, ]; export const TASK_PRIORITY_META: Record = { @@ -24,6 +26,7 @@ export const TASK_PRIORITY_META: Record = { [TaskPriority.HIGH]: { label: "High" }, [TaskPriority.MEDIUM]: { label: "Medium" }, [TaskPriority.LOW]: { label: "Low" }, + [TaskPriority.UNKNOWN]: { label: "Unknown" }, }; export function isTaskPriority(value: unknown): value is TaskPriority { diff --git a/src/model/taskStatus.test.ts b/src/model/taskStatus.test.ts index 88126a8..c71b735 100644 --- a/src/model/taskStatus.test.ts +++ b/src/model/taskStatus.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; import { isTaskStatus, - TaskStatus, TASK_STATUS_META, TASK_STATUS_ORDER, + TaskStatus, } from "./taskStatus"; describe("TaskStatus", () => { @@ -23,8 +23,9 @@ describe("TaskStatus", () => { }); it("isTaskStatus accepts known values only", () => { - expect(isTaskStatus(TaskStatus.TODO)).toBe(true); + expect(isTaskStatus(TaskStatus.OPEN)).toBe(true); expect(isTaskStatus(TaskStatus.BACKLOG)).toBe(true); + expect(isTaskStatus(TaskStatus.UNKNOWN)).toBe(true); expect(isTaskStatus("__not_a_status__")).toBe(false); expect(isTaskStatus(null)).toBe(false); }); diff --git a/src/model/taskStatus.ts b/src/model/taskStatus.ts index f3b403e..07f8a16 100644 --- a/src/model/taskStatus.ts +++ b/src/model/taskStatus.ts @@ -1,9 +1,10 @@ export const TaskStatus = { BACKLOG: "backlog", - TODO: "todo", + OPEN: "open", IN_PROGRESS: "in-progress", DONE: "done", CANCELLED: "cancelled", + UNKNOWN: "unknown", } as const; export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus]; @@ -19,10 +20,11 @@ export type TaskStatusMeta = { /** Explicit display / column order (not Object.values). */ export const TASK_STATUS_ORDER: TaskStatus[] = [ TaskStatus.BACKLOG, - TaskStatus.TODO, + TaskStatus.OPEN, TaskStatus.IN_PROGRESS, TaskStatus.DONE, TaskStatus.CANCELLED, + TaskStatus.UNKNOWN, ]; export const TASK_STATUS_META: Record = { @@ -31,8 +33,8 @@ export const TASK_STATUS_META: Record = { defaultHidden: true, icon: "inbox", }, - [TaskStatus.TODO]: { - label: "To Do", + [TaskStatus.OPEN]: { + label: "Open", defaultHidden: false, icon: "circle-outline", }, @@ -51,6 +53,11 @@ export const TASK_STATUS_META: Record = { defaultHidden: true, icon: "close", }, + [TaskStatus.UNKNOWN]: { + label: "Unknown", + defaultHidden: false, + icon: "question", + }, }; export function isTaskStatus(value: unknown): value is TaskStatus { diff --git a/src/storage/FsTaskRepository.test.ts b/src/storage/FsTaskRepository.test.ts index bc8eb66..9795c03 100644 --- a/src/storage/FsTaskRepository.test.ts +++ b/src/storage/FsTaskRepository.test.ts @@ -1,6 +1,8 @@ import { Task } from "@/model/task"; import { ITaskRepository } from "@/ports"; import { TASK_FILE_EXTENSION } from "@/model/taskFile"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { beforeEach, describe, expect, it } from "vitest"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { FsTaskRepository } from "./FsTaskRepository"; @@ -21,8 +23,8 @@ describe("FsTaskRepository", () => { const sampleTask = { title: "Fix login bug", description: "Token validation error", - status: "todo" as const, - priority: "high" as const, + status: TaskStatus.OPEN, + priority: TaskPriority.HIGH, tags: ["bug", "frontend"], assignee: "ivanov", }; @@ -53,7 +55,7 @@ describe("FsTaskRepository", () => { `${folder}/fix-login-bug${TASK_FILE_EXTENSION}`, ); expect(content).toContain("Fix login bug"); - expect(content).toContain("status: todo"); + expect(content).toContain("status: open"); }); it("list returns created tasks sorted by updated desc", async () => { @@ -81,12 +83,12 @@ describe("FsTaskRepository", () => { it("update modifies fields and bumps updated timestamp", async () => { const task = await repo.create(folder, sampleTask); - const updated: Task = { ...task, status: "done" }; + const updated: Task = { ...task, status: TaskStatus.DONE }; await repo.update(folder, updated); const found = await repo.getById(folder, task.id); - expect(found!.status).toBe("done"); + expect(found!.status).toBe(TaskStatus.DONE); expect(found!.title).toBe(task.title); expect(found!.updated).toBe(t1); expect(found!.created).toBe(t0); @@ -220,7 +222,7 @@ describe("FsTaskRepository", () => { }); it("creates task with tags containing special characters", async () => { - const task = await repo.create(folder, { + await repo.create(folder, { ...sampleTask, tags: ["c#", "c++", "tag name", "tag:value"], }); @@ -236,7 +238,7 @@ describe("FsTaskRepository", () => { }); it("getFilePath produces consistent slug regardless of special characters", async () => { - const task = await repo.create(folder, { + await repo.create(folder, { ...sampleTask, title: "Fix___bug...", }); diff --git a/src/ui/resolveTaskFolder.ts b/src/ui/resolveTaskFolder.ts index 8e74136..01fb2bd 100644 --- a/src/ui/resolveTaskFolder.ts +++ b/src/ui/resolveTaskFolder.ts @@ -11,7 +11,8 @@ export function resolveTaskFolderForPath( ): string | undefined { const normalized = path.resolve(filePath).replace(/\\/g, "/"); let best: string | undefined; - let bestLen = -1; + const NO_MATCH = -1; + let bestLen = NO_MATCH; for (const location of locations) { const folder = path.resolve(location.folderPath).replace(/\\/g, "/"); diff --git a/src/utils/markdown.test.ts b/src/utils/markdown.test.ts index f7f533b..757e95d 100644 --- a/src/utils/markdown.test.ts +++ b/src/utils/markdown.test.ts @@ -1,4 +1,6 @@ import { Task } from "@/model/task"; +import { TaskPriority } from "@/model/taskPriority"; +import { TaskStatus } from "@/model/taskStatus"; import { describe, expect, it } from "vitest"; import { parseTaskFile, serializeTask } from "./markdown"; @@ -8,8 +10,8 @@ const fullTask: Task = { id: "550e8400-e29b-41d4-a716-446655440000", title: "Пофиксить баг логина", description: "Ошибка валидации токена", - status: "in-progress", - priority: "high", + status: TaskStatus.IN_PROGRESS, + priority: TaskPriority.HIGH, tags: ["bug", "frontend"], assignee: "ivanov", created: "2026-07-12T10:00:00.000Z", @@ -45,8 +47,8 @@ describe("markdown", () => { expect(task.id).toBe("abc"); expect(task.title).toBe("Minimal"); expect(task.description).toBe(""); - expect(task.status).toBe("todo"); - expect(task.priority).toBe("medium"); + expect(task.status).toBe(TaskStatus.UNKNOWN); + expect(task.priority).toBe(TaskPriority.UNKNOWN); expect(task.tags).toEqual([]); expect(task.assignee).toBe(""); expect(task.created).toMatch(ISO_RE); diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index d290bbb..72fe955 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -10,6 +10,16 @@ function toISO(value: unknown): string { return new Date().toISOString(); } +function normalizeTaskStatus(value: unknown): TaskStatus { + if (isTaskStatus(value)) return value; + return TaskStatus.UNKNOWN; +} + +function normalizeTaskPriority(value: unknown): TaskPriority { + if (isTaskPriority(value)) return value; + return TaskPriority.UNKNOWN; +} + export function parseTaskFile(content: string): Task { const parsed = fm>(content); const data = parsed.attributes; @@ -17,10 +27,8 @@ export function parseTaskFile(content: string): Task { id: data.id as string, title: data.title as string, description: parsed.body.trim(), - status: isTaskStatus(data.status) ? data.status : TaskStatus.TODO, - priority: isTaskPriority(data.priority) - ? data.priority - : TaskPriority.MEDIUM, + status: normalizeTaskStatus(data.status), + priority: normalizeTaskPriority(data.priority), tags: (data.tags as string[]) ?? [], assignee: (data.assignee as string) ?? "", created: toISO(data.created),