feat: add unknown status and priority, change todo status to open

This commit is contained in:
2026-07-18 07:48:11 +05:00
parent 526a001d06
commit 148ed75841
28 changed files with 162 additions and 111 deletions
+10 -8
View File
@@ -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)));
});
+4 -3
View File
@@ -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("");
+3 -3
View File
@@ -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, {
+4 -2
View File
@@ -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: "",
});
+6 -4
View File
@@ -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: "",
});
+6 -4
View File
@@ -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: "",
});
+4 -2
View File
@@ -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: "",
});
+8 -6
View File
@@ -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: "",
});
+3 -2
View File
@@ -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: "",
});
+5 -4
View File
@@ -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");
});
+13 -11
View File
@@ -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<Task> & Pick<Task, "id" | "title">): 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",
});
+3 -2
View File
@@ -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",
+18 -16
View File
@@ -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<Task> & Pick<Task, "id" | "title">): 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([]);
});
});
+4 -4
View File
@@ -16,7 +16,7 @@ import { groupTasks } from "./groupTasks";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): 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));
+3 -2
View File
@@ -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",
+4 -2
View File
@@ -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");
+2 -2
View File
@@ -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();
+11 -10
View File
@@ -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<Task> & Pick<Task, "id" | "title">): 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"]);
});
});
@@ -6,5 +6,6 @@ exports[`TaskPriority > exposes wire values on const keys 1`] = `
"HIGH": "high",
"LOW": "low",
"MEDIUM": "medium",
"UNKNOWN": "unknown",
}
`;
@@ -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",
}
`;
+1
View File
@@ -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);
});
});
+4 -1
View File
@@ -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<TaskPriority, TaskPriorityMeta> = {
@@ -24,6 +26,7 @@ export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = {
[TaskPriority.HIGH]: { label: "High" },
[TaskPriority.MEDIUM]: { label: "Medium" },
[TaskPriority.LOW]: { label: "Low" },
[TaskPriority.UNKNOWN]: { label: "Unknown" },
};
export function isTaskPriority(value: unknown): value is TaskPriority {
+3 -2
View File
@@ -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);
});
+11 -4
View File
@@ -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<TaskStatus, TaskStatusMeta> = {
@@ -31,8 +33,8 @@ export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
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<TaskStatus, TaskStatusMeta> = {
defaultHidden: true,
icon: "close",
},
[TaskStatus.UNKNOWN]: {
label: "Unknown",
defaultHidden: false,
icon: "question",
},
};
export function isTaskStatus(value: unknown): value is TaskStatus {
+9 -7
View File
@@ -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...",
});
+2 -1
View File
@@ -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, "/");
+6 -4
View File
@@ -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);
+12 -4
View File
@@ -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<Record<string, unknown>>(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),