backlog.md 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.backlog/archive/drafts/readme.md +3 -0
  2. package/.backlog/archive/drafts/task-41 - temporary-test-task.md +13 -0
  3. package/.backlog/archive/readme.md +6 -0
  4. package/.backlog/archive/tasks/readme.md +3 -0
  5. package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +14 -0
  6. package/.backlog/config.yml +7 -0
  7. package/.backlog/decisions/readme.md +7 -0
  8. package/.backlog/docs/readme.md +20 -0
  9. package/.backlog/drafts/readme.md +3 -0
  10. package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +21 -0
  11. package/.backlog/drafts/task-28 - add-code-of-conduct.md +20 -0
  12. package/.backlog/drafts/task-30 - create-changelog.md +19 -0
  13. package/.backlog/milestones/m-0 - project-setup.md +8 -0
  14. package/.backlog/milestones/m-1 - cli.md +8 -0
  15. package/.backlog/milestones/m-2 - cli-kanban.md +8 -0
  16. package/.backlog/milestones/m-3 - gui.md +8 -0
  17. package/.backlog/milestones/m-4 - gui-kanban.md +8 -0
  18. package/.backlog/milestones/m-5 - gui-advanced.md +12 -0
  19. package/.backlog/milestones/readme.md +3 -0
  20. package/.backlog/readme.md +5 -0
  21. package/.backlog/tasks/readme.md +37 -0
  22. package/.backlog/tasks/task-1 - cli-setup-core-project.md +23 -0
  23. package/.backlog/tasks/task-10 - gui-init-packaging.md +23 -0
  24. package/.backlog/tasks/task-11 - gui-kanban-board.md +26 -0
  25. package/.backlog/tasks/task-12 - gui-advanced.md +25 -0
  26. package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +53 -0
  27. package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +40 -0
  28. package/.backlog/tasks/task-14 - gui-introduction-screens.md +21 -0
  29. package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +20 -0
  30. package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +20 -0
  31. package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +20 -0
  32. package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +20 -0
  33. package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +55 -0
  34. package/.backlog/tasks/task-2 - cli-core-logic-library.md +28 -0
  35. package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +32 -0
  36. package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +31 -0
  37. package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +24 -0
  38. package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +30 -0
  39. package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +38 -0
  40. package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +19 -0
  41. package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +28 -0
  42. package/.backlog/tasks/task-27 - add-contributing-guidelines.md +27 -0
  43. package/.backlog/tasks/task-29 - add-github-templates.md +28 -0
  44. package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +63 -0
  45. package/.backlog/tasks/task-31 - update-readme-for-open-source.md +26 -0
  46. package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +31 -0
  47. package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +20 -0
  48. package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +26 -0
  49. package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +24 -0
  50. package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +24 -0
  51. package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +19 -0
  52. package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +25 -0
  53. package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +31 -0
  54. package/.backlog/tasks/task-4 - cli-task-management-commands.md +28 -0
  55. package/.backlog/tasks/task-4.1 - cli-task-create.md +27 -0
  56. package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +51 -0
  57. package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +23 -0
  58. package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +53 -0
  59. package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +58 -0
  60. package/.backlog/tasks/task-4.2 - cli-task-list-view.md +25 -0
  61. package/.backlog/tasks/task-4.3 - cli-task-edit.md +24 -0
  62. package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +27 -0
  63. package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +28 -0
  64. package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +35 -0
  65. package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +40 -0
  66. package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +48 -0
  67. package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +66 -0
  68. package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +38 -0
  69. package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +93 -0
  70. package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +42 -0
  71. package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +44 -0
  72. package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +45 -0
  73. package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +49 -0
  74. package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +55 -0
  75. package/.backlog/tasks/task-42 - visual-hierarchy.md +54 -0
  76. package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +56 -0
  77. package/.backlog/tasks/task-44 - checklist-alignment.md +24 -0
  78. package/.backlog/tasks/task-45 - safe-line-wrapping.md +23 -0
  79. package/.backlog/tasks/task-46 - split-pane-layout.md +24 -0
  80. package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +43 -0
  81. package/.backlog/tasks/task-48 - footer-hint-line.md +21 -0
  82. package/.backlog/tasks/task-49 - status-styling.md +53 -0
  83. package/.backlog/tasks/task-5 - cli-docs-decisions.md +57 -0
  84. package/.backlog/tasks/task-50 - borders-&-padding.md +22 -0
  85. package/.backlog/tasks/task-51 - code-path-styling.md +23 -0
  86. package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +29 -0
  87. package/.backlog/tasks/task-6 - cli-packaging.md +65 -0
  88. package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +49 -0
  89. package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +64 -0
  90. package/.backlog/tasks/task-7 - cli-kanban-view.md +60 -0
  91. package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +62 -0
  92. package/.backlog/tasks/task-8 - gui-project-setup.md +21 -0
  93. package/.backlog/tasks/task-9 - gui-task-crud.md +24 -0
  94. package/.cursorrules +223 -0
  95. package/.gitattributes +2 -0
  96. package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
  97. package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  98. package/.github/PULL_REQUEST_TEMPLATE.md +8 -0
  99. package/.github/workflows/ci.yml +36 -0
  100. package/.husky/pre-commit +1 -0
  101. package/AGENTS.md +65 -0
  102. package/CLAUDE.md +87 -0
  103. package/CONTRIBUTING.md +19 -0
  104. package/DEVELOPMENT.md +37 -0
  105. package/LICENSE +21 -0
  106. package/biome.json +31 -0
  107. package/bun.lock +152 -0
  108. package/cli/.cursorrules-xh86jabm.md +82 -0
  109. package/cli/AGENTS-xh86jabm.md +82 -0
  110. package/cli/CLAUDE-xh86jabm.md +82 -0
  111. package/cli/backlog +0 -0
  112. package/cli/cli.js +19622 -0
  113. package/cli/index.js +2 -0
  114. package/docs/npm-publishing.md +69 -0
  115. package/package.json +47 -0
  116. package/readme.md +97 -0
  117. package/scripts/build.js +73 -0
  118. package/src/agent-instructions.ts +54 -0
  119. package/src/board.ts +263 -0
  120. package/src/cli.ts +806 -0
  121. package/src/constants/index.ts +48 -0
  122. package/src/core/backlog.ts +183 -0
  123. package/src/core/remote-tasks.ts +168 -0
  124. package/src/file-system/operations.ts +515 -0
  125. package/src/git/operations.ts +189 -0
  126. package/src/guidelines/.cursorrules.md +82 -0
  127. package/src/guidelines/AGENTS.md +82 -0
  128. package/src/guidelines/CLAUDE.md +82 -0
  129. package/src/guidelines/index.ts +7 -0
  130. package/src/index.ts +30 -0
  131. package/src/markdown/parser.ts +145 -0
  132. package/src/markdown/serializer.ts +71 -0
  133. package/src/test/agent-instructions.test.ts +62 -0
  134. package/src/test/board.test.ts +291 -0
  135. package/src/test/build.test.ts +28 -0
  136. package/src/test/checklist.test.ts +273 -0
  137. package/src/test/cli.test.ts +1300 -0
  138. package/src/test/code-path.test.ts +204 -0
  139. package/src/test/core.test.ts +330 -0
  140. package/src/test/filesystem.test.ts +435 -0
  141. package/src/test/git.test.ts +26 -0
  142. package/src/test/heading.test.ts +102 -0
  143. package/src/test/line-wrapping.test.ts +252 -0
  144. package/src/test/local-install.test.ts +34 -0
  145. package/src/test/markdown.test.ts +526 -0
  146. package/src/test/parallel-loading.test.ts +160 -0
  147. package/src/test/parent-id-normalization.test.ts +48 -0
  148. package/src/test/remote-id-conflict.test.ts +60 -0
  149. package/src/test/status-icon.test.ts +93 -0
  150. package/src/types/blessed.d.ts +14 -0
  151. package/src/types/index.ts +55 -0
  152. package/src/types/raw.d.ts +4 -0
  153. package/src/ui/board.ts +322 -0
  154. package/src/ui/checklist.ts +103 -0
  155. package/src/ui/code-path.ts +113 -0
  156. package/src/ui/heading.ts +121 -0
  157. package/src/ui/loading.ts +216 -0
  158. package/src/ui/status-icon.ts +53 -0
  159. package/src/ui/task-list.ts +168 -0
  160. package/src/ui/task-viewer.ts +640 -0
  161. package/src/ui/tui.ts +301 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,1300 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { mkdir, rm, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { Core, isGitRepository } from "../index.ts";
5
+
6
+ const TEST_DIR = join(process.cwd(), "test-cli");
7
+ const CLI_PATH = join(process.cwd(), "src", "cli.ts");
8
+
9
+ describe("CLI Integration", () => {
10
+ beforeEach(async () => {
11
+ try {
12
+ await rm(TEST_DIR, { recursive: true, force: true });
13
+ } catch {
14
+ // Ignore cleanup errors
15
+ }
16
+ await mkdir(TEST_DIR, { recursive: true });
17
+ });
18
+
19
+ afterEach(async () => {
20
+ try {
21
+ await rm(TEST_DIR, { recursive: true, force: true });
22
+ } catch {
23
+ // Ignore cleanup errors
24
+ }
25
+ });
26
+
27
+ describe("backlog init command", () => {
28
+ it("should initialize backlog project in existing git repo", async () => {
29
+ // Set up a git repository
30
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
31
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
32
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
33
+
34
+ // Initialize backlog project using Core (simulating CLI)
35
+ const core = new Core(TEST_DIR);
36
+ await core.initializeProject("CLI Test Project");
37
+
38
+ // Verify directory structure was created
39
+ const configExists = await Bun.file(join(TEST_DIR, ".backlog", "config.yml")).exists();
40
+ expect(configExists).toBe(true);
41
+
42
+ // Verify config content
43
+ const config = await core.filesystem.loadConfig();
44
+ expect(config?.projectName).toBe("CLI Test Project");
45
+ expect(config?.statuses).toEqual(["To Do", "In Progress", "Done"]);
46
+ expect(config?.defaultStatus).toBe("To Do");
47
+
48
+ // Verify git commit was created
49
+ const lastCommit = await core.gitOps.getLastCommitMessage();
50
+ expect(lastCommit).toContain("Initialize backlog project: CLI Test Project");
51
+ });
52
+
53
+ it("should create all required directories", async () => {
54
+ // Set up a git repository
55
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
56
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
57
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
58
+
59
+ const core = new Core(TEST_DIR);
60
+ await core.initializeProject("Directory Test");
61
+
62
+ // Check all expected directories exist
63
+ const expectedDirs = [
64
+ ".backlog",
65
+ ".backlog/tasks",
66
+ ".backlog/drafts",
67
+ ".backlog/archive",
68
+ ".backlog/archive/tasks",
69
+ ".backlog/archive/drafts",
70
+ ".backlog/docs",
71
+ ".backlog/decisions",
72
+ ];
73
+
74
+ for (const dir of expectedDirs) {
75
+ try {
76
+ const stats = await stat(join(TEST_DIR, dir));
77
+ expect(stats.isDirectory()).toBe(true);
78
+ } catch {
79
+ // If stat fails, directory doesn't exist
80
+ expect(false).toBe(true);
81
+ }
82
+ }
83
+ });
84
+
85
+ it("should handle project names with special characters", async () => {
86
+ // Set up a git repository
87
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
88
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
89
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
90
+
91
+ const core = new Core(TEST_DIR);
92
+ const specialProjectName = "My-Project_2024 (v1.0)";
93
+ await core.initializeProject(specialProjectName);
94
+
95
+ const config = await core.filesystem.loadConfig();
96
+ expect(config?.projectName).toBe(specialProjectName);
97
+ });
98
+
99
+ it("should work when git repo exists", async () => {
100
+ // Set up existing git repo
101
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
102
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
103
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
104
+
105
+ const isRepo = await isGitRepository(TEST_DIR);
106
+ expect(isRepo).toBe(true);
107
+
108
+ const core = new Core(TEST_DIR);
109
+ await core.initializeProject("Existing Repo Test");
110
+
111
+ const config = await core.filesystem.loadConfig();
112
+ expect(config?.projectName).toBe("Existing Repo Test");
113
+ });
114
+
115
+ it("should accept optional project name parameter", async () => {
116
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
117
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
118
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
119
+
120
+ // Test the CLI implementation by directly using the Core functionality
121
+ const core = new Core(TEST_DIR);
122
+ await core.initializeProject("Test Project");
123
+
124
+ const config = await core.filesystem.loadConfig();
125
+ expect(config?.projectName).toBe("Test Project");
126
+ });
127
+
128
+ it("should create agent instruction files when requested", async () => {
129
+ // Set up a git repository
130
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
131
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
132
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
133
+
134
+ // Simulate the agent instructions being added
135
+ const core = new Core(TEST_DIR);
136
+ await core.initializeProject("Agent Test Project");
137
+
138
+ // Import and call addAgentInstructions directly (simulating user saying "y")
139
+ const { addAgentInstructions } = await import("../index.ts");
140
+ await addAgentInstructions(TEST_DIR, core.gitOps);
141
+
142
+ // Verify agent files were created
143
+ const agentsFile = await Bun.file(join(TEST_DIR, "AGENTS.md")).exists();
144
+ const claudeFile = await Bun.file(join(TEST_DIR, "CLAUDE.md")).exists();
145
+ const cursorFile = await Bun.file(join(TEST_DIR, ".cursorrules")).exists();
146
+
147
+ expect(agentsFile).toBe(true);
148
+ expect(claudeFile).toBe(true);
149
+ expect(cursorFile).toBe(true);
150
+
151
+ // Verify content
152
+ const agentsContent = await Bun.file(join(TEST_DIR, "AGENTS.md")).text();
153
+ const claudeContent = await Bun.file(join(TEST_DIR, "CLAUDE.md")).text();
154
+ const cursorContent = await Bun.file(join(TEST_DIR, ".cursorrules")).text();
155
+ expect(agentsContent.length).toBeGreaterThan(0);
156
+ expect(claudeContent.length).toBeGreaterThan(0);
157
+ expect(cursorContent.length).toBeGreaterThan(0);
158
+ });
159
+ });
160
+
161
+ describe("git integration", () => {
162
+ beforeEach(async () => {
163
+ // Set up a git repository
164
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
165
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
166
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
167
+ });
168
+
169
+ it("should create initial commit with backlog structure", async () => {
170
+ const core = new Core(TEST_DIR);
171
+ await core.initializeProject("Git Integration Test");
172
+
173
+ const lastCommit = await core.gitOps.getLastCommitMessage();
174
+ expect(lastCommit).toBe("backlog: Initialize backlog project: Git Integration Test");
175
+
176
+ // Verify git status is clean after initialization
177
+ const isClean = await core.gitOps.isClean();
178
+ expect(isClean).toBe(true);
179
+ });
180
+ });
181
+
182
+ describe("task list command", () => {
183
+ beforeEach(async () => {
184
+ // Set up a git repository and initialize backlog
185
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
186
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
187
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
188
+
189
+ const core = new Core(TEST_DIR);
190
+ await core.initializeProject("List Test Project");
191
+ });
192
+
193
+ it("should show 'No tasks found' when no tasks exist", async () => {
194
+ const core = new Core(TEST_DIR);
195
+ const tasks = await core.filesystem.listTasks();
196
+ expect(tasks).toHaveLength(0);
197
+ });
198
+
199
+ it("should list tasks grouped by status", async () => {
200
+ const core = new Core(TEST_DIR);
201
+
202
+ // Create test tasks with different statuses
203
+ await core.createTask(
204
+ {
205
+ id: "task-1",
206
+ title: "First Task",
207
+ status: "To Do",
208
+ assignee: [],
209
+ createdDate: "2025-06-08",
210
+ labels: [],
211
+ dependencies: [],
212
+ description: "First test task",
213
+ },
214
+ false,
215
+ );
216
+
217
+ await core.createTask(
218
+ {
219
+ id: "task-2",
220
+ title: "Second Task",
221
+ status: "Done",
222
+ assignee: [],
223
+ createdDate: "2025-06-08",
224
+ labels: [],
225
+ dependencies: [],
226
+ description: "Second test task",
227
+ },
228
+ false,
229
+ );
230
+
231
+ await core.createTask(
232
+ {
233
+ id: "task-3",
234
+ title: "Third Task",
235
+ status: "To Do",
236
+ assignee: [],
237
+ createdDate: "2025-06-08",
238
+ labels: [],
239
+ dependencies: [],
240
+ description: "Third test task",
241
+ },
242
+ false,
243
+ );
244
+
245
+ const tasks = await core.filesystem.listTasks();
246
+ expect(tasks).toHaveLength(3);
247
+
248
+ // Verify tasks are grouped correctly by status
249
+ const todoTasks = tasks.filter((t) => t.status === "To Do");
250
+ const doneTasks = tasks.filter((t) => t.status === "Done");
251
+
252
+ expect(todoTasks).toHaveLength(2);
253
+ expect(doneTasks).toHaveLength(1);
254
+ expect(todoTasks.map((t) => t.id)).toEqual(["task-1", "task-3"]);
255
+ expect(doneTasks.map((t) => t.id)).toEqual(["task-2"]);
256
+ });
257
+
258
+ it("should respect config status order", async () => {
259
+ const core = new Core(TEST_DIR);
260
+
261
+ // Load and verify default config status order
262
+ const config = await core.filesystem.loadConfig();
263
+ expect(config?.statuses).toEqual(["To Do", "In Progress", "Done"]);
264
+ });
265
+
266
+ it("should filter tasks by status", async () => {
267
+ const core = new Core(TEST_DIR);
268
+
269
+ await core.createTask(
270
+ {
271
+ id: "task-1",
272
+ title: "First Task",
273
+ status: "To Do",
274
+ assignee: [],
275
+ createdDate: "2025-06-08",
276
+ labels: [],
277
+ dependencies: [],
278
+ description: "First test task",
279
+ },
280
+ false,
281
+ );
282
+ await core.createTask(
283
+ {
284
+ id: "task-2",
285
+ title: "Second Task",
286
+ status: "Done",
287
+ assignee: [],
288
+ createdDate: "2025-06-08",
289
+ labels: [],
290
+ dependencies: [],
291
+ description: "Second test task",
292
+ },
293
+ false,
294
+ );
295
+
296
+ const result = Bun.spawnSync(["bun", CLI_PATH, "task", "list", "--plain", "--status", "Done"], { cwd: TEST_DIR });
297
+ const out = result.stdout.toString();
298
+ expect(out).toContain("Done:");
299
+ expect(out).toContain("task-2 - Second Task");
300
+ expect(out).not.toContain("task-1");
301
+ });
302
+
303
+ it("should filter tasks by assignee", async () => {
304
+ const core = new Core(TEST_DIR);
305
+
306
+ await core.createTask(
307
+ {
308
+ id: "task-1",
309
+ title: "Assigned Task",
310
+ status: "To Do",
311
+ assignee: ["alice"],
312
+ createdDate: "2025-06-08",
313
+ labels: [],
314
+ dependencies: [],
315
+ description: "Assigned task",
316
+ },
317
+ false,
318
+ );
319
+ await core.createTask(
320
+ {
321
+ id: "task-2",
322
+ title: "Unassigned Task",
323
+ status: "To Do",
324
+ assignee: [],
325
+ createdDate: "2025-06-08",
326
+ labels: [],
327
+ dependencies: [],
328
+ description: "Other task",
329
+ },
330
+ false,
331
+ );
332
+
333
+ const result = Bun.spawnSync(["bun", CLI_PATH, "task", "list", "--plain", "--assignee", "alice"], {
334
+ cwd: TEST_DIR,
335
+ });
336
+ const out = result.stdout.toString();
337
+ expect(out).toContain("task-1 - Assigned Task");
338
+ expect(out).not.toContain("task-2 - Unassigned Task");
339
+ });
340
+ });
341
+
342
+ describe("task view command", () => {
343
+ beforeEach(async () => {
344
+ // Set up a git repository and initialize backlog
345
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
346
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
347
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
348
+
349
+ const core = new Core(TEST_DIR);
350
+ await core.initializeProject("View Test Project");
351
+ });
352
+
353
+ it("should display task details with markdown formatting", async () => {
354
+ const core = new Core(TEST_DIR);
355
+
356
+ // Create a test task
357
+ const testTask = {
358
+ id: "task-1",
359
+ title: "Test View Task",
360
+ status: "To Do",
361
+ assignee: ["testuser"],
362
+ createdDate: "2025-06-08",
363
+ labels: ["test", "cli"],
364
+ dependencies: [],
365
+ description: "This is a test task for view command",
366
+ };
367
+
368
+ await core.createTask(testTask, false);
369
+
370
+ // Load the task back
371
+ const loadedTask = await core.filesystem.loadTask("task-1");
372
+ expect(loadedTask).not.toBeNull();
373
+ expect(loadedTask?.id).toBe("task-1");
374
+ expect(loadedTask?.title).toBe("Test View Task");
375
+ expect(loadedTask?.status).toBe("To Do");
376
+ expect(loadedTask?.assignee).toEqual(["testuser"]);
377
+ expect(loadedTask?.labels).toEqual(["test", "cli"]);
378
+ expect(loadedTask?.description).toBe("## Description\n\nThis is a test task for view command");
379
+ });
380
+
381
+ it("should handle task IDs with and without 'task-' prefix", async () => {
382
+ const core = new Core(TEST_DIR);
383
+
384
+ // Create a test task
385
+ await core.createTask(
386
+ {
387
+ id: "task-5",
388
+ title: "Prefix Test Task",
389
+ status: "To Do",
390
+ assignee: [],
391
+ createdDate: "2025-06-08",
392
+ labels: [],
393
+ dependencies: [],
394
+ description: "Testing task ID normalization",
395
+ },
396
+ false,
397
+ );
398
+
399
+ // Test loading with full task-5 ID
400
+ const taskWithPrefix = await core.filesystem.loadTask("task-5");
401
+ expect(taskWithPrefix?.id).toBe("task-5");
402
+
403
+ // Test loading with just numeric ID (5)
404
+ const taskWithoutPrefix = await core.filesystem.loadTask("5");
405
+ // The filesystem loadTask should handle normalization
406
+ expect(taskWithoutPrefix?.id).toBe("task-5");
407
+ });
408
+
409
+ it("should return null for non-existent tasks", async () => {
410
+ const core = new Core(TEST_DIR);
411
+
412
+ const nonExistentTask = await core.filesystem.loadTask("task-999");
413
+ expect(nonExistentTask).toBeNull();
414
+ });
415
+
416
+ it("should not modify task files (read-only operation)", async () => {
417
+ const core = new Core(TEST_DIR);
418
+
419
+ // Create a test task
420
+ const originalTask = {
421
+ id: "task-1",
422
+ title: "Read Only Test",
423
+ status: "To Do",
424
+ assignee: [],
425
+ createdDate: "2025-06-08",
426
+ labels: ["readonly"],
427
+ dependencies: [],
428
+ description: "Original description",
429
+ };
430
+
431
+ await core.createTask(originalTask, false);
432
+
433
+ // Load the task (simulating view operation)
434
+ const viewedTask = await core.filesystem.loadTask("task-1");
435
+
436
+ // Load again to verify nothing changed
437
+ const secondView = await core.filesystem.loadTask("task-1");
438
+
439
+ expect(viewedTask).toEqual(secondView);
440
+ expect(viewedTask?.title).toBe("Read Only Test");
441
+ expect(viewedTask?.description).toBe("## Description\n\nOriginal description");
442
+ });
443
+ });
444
+
445
+ describe("task edit command", () => {
446
+ beforeEach(async () => {
447
+ // Set up a git repository and initialize backlog
448
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
449
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
450
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
451
+
452
+ const core = new Core(TEST_DIR);
453
+ await core.initializeProject("Edit Test Project");
454
+ });
455
+
456
+ it("should update task title, description, and status", async () => {
457
+ const core = new Core(TEST_DIR);
458
+
459
+ // Create a test task
460
+ await core.createTask(
461
+ {
462
+ id: "task-1",
463
+ title: "Original Title",
464
+ status: "To Do",
465
+ assignee: [],
466
+ createdDate: "2025-06-08",
467
+ labels: [],
468
+ dependencies: [],
469
+ description: "Original description",
470
+ },
471
+ false,
472
+ );
473
+
474
+ // Load and edit the task
475
+ const task = await core.filesystem.loadTask("task-1");
476
+ expect(task).not.toBeNull();
477
+
478
+ if (task) {
479
+ task.title = "Updated Title";
480
+ task.description = "Updated description";
481
+ task.status = "In Progress";
482
+ task.updatedDate = "2025-06-08";
483
+
484
+ await core.updateTask(task, false);
485
+ }
486
+
487
+ // Verify changes were persisted
488
+ const updatedTask = await core.filesystem.loadTask("task-1");
489
+ expect(updatedTask?.title).toBe("Updated Title");
490
+ expect(updatedTask?.description).toBe("## Description\n\nUpdated description");
491
+ expect(updatedTask?.status).toBe("In Progress");
492
+ expect(updatedTask?.updatedDate).toBe("2025-06-08");
493
+ });
494
+
495
+ it("should update assignee", async () => {
496
+ const core = new Core(TEST_DIR);
497
+
498
+ // Create a test task
499
+ await core.createTask(
500
+ {
501
+ id: "task-2",
502
+ title: "Assignee Test",
503
+ status: "To Do",
504
+ assignee: [],
505
+ createdDate: "2025-06-08",
506
+ labels: [],
507
+ dependencies: [],
508
+ description: "Testing assignee updates",
509
+ },
510
+ false,
511
+ );
512
+
513
+ // Update assignee
514
+ const task = await core.filesystem.loadTask("task-2");
515
+ if (task) {
516
+ task.assignee = ["newuser@example.com"];
517
+ task.updatedDate = "2025-06-08";
518
+ await core.updateTask(task, false);
519
+ }
520
+
521
+ // Verify assignee was updated
522
+ const updatedTask = await core.filesystem.loadTask("task-2");
523
+ expect(updatedTask?.assignee).toEqual(["newuser@example.com"]);
524
+ });
525
+
526
+ it("should replace all labels with new labels", async () => {
527
+ const core = new Core(TEST_DIR);
528
+
529
+ // Create a test task with existing labels
530
+ await core.createTask(
531
+ {
532
+ id: "task-3",
533
+ title: "Label Replace Test",
534
+ status: "To Do",
535
+ assignee: [],
536
+ createdDate: "2025-06-08",
537
+ labels: ["old1", "old2"],
538
+ dependencies: [],
539
+ description: "Testing label replacement",
540
+ },
541
+ false,
542
+ );
543
+
544
+ // Replace all labels
545
+ const task = await core.filesystem.loadTask("task-3");
546
+ if (task) {
547
+ task.labels = ["new1", "new2", "new3"];
548
+ task.updatedDate = "2025-06-08";
549
+ await core.updateTask(task, false);
550
+ }
551
+
552
+ // Verify labels were replaced
553
+ const updatedTask = await core.filesystem.loadTask("task-3");
554
+ expect(updatedTask?.labels).toEqual(["new1", "new2", "new3"]);
555
+ });
556
+
557
+ it("should add labels without replacing existing ones", async () => {
558
+ const core = new Core(TEST_DIR);
559
+
560
+ // Create a test task with existing labels
561
+ await core.createTask(
562
+ {
563
+ id: "task-4",
564
+ title: "Label Add Test",
565
+ status: "To Do",
566
+ assignee: [],
567
+ createdDate: "2025-06-08",
568
+ labels: ["existing"],
569
+ dependencies: [],
570
+ description: "Testing label addition",
571
+ },
572
+ false,
573
+ );
574
+
575
+ // Add new labels
576
+ const task = await core.filesystem.loadTask("task-4");
577
+ if (task) {
578
+ const newLabels = [...task.labels];
579
+ const labelsToAdd = ["added1", "added2"];
580
+ for (const label of labelsToAdd) {
581
+ if (!newLabels.includes(label)) {
582
+ newLabels.push(label);
583
+ }
584
+ }
585
+ task.labels = newLabels;
586
+ task.updatedDate = "2025-06-08";
587
+ await core.updateTask(task, false);
588
+ }
589
+
590
+ // Verify labels were added
591
+ const updatedTask = await core.filesystem.loadTask("task-4");
592
+ expect(updatedTask?.labels).toEqual(["existing", "added1", "added2"]);
593
+ });
594
+
595
+ it("should remove specific labels", async () => {
596
+ const core = new Core(TEST_DIR);
597
+
598
+ // Create a test task with multiple labels
599
+ await core.createTask(
600
+ {
601
+ id: "task-5",
602
+ title: "Label Remove Test",
603
+ status: "To Do",
604
+ assignee: [],
605
+ createdDate: "2025-06-08",
606
+ labels: ["keep1", "remove", "keep2"],
607
+ dependencies: [],
608
+ description: "Testing label removal",
609
+ },
610
+ false,
611
+ );
612
+
613
+ // Remove specific label
614
+ const task = await core.filesystem.loadTask("task-5");
615
+ if (task) {
616
+ const newLabels = task.labels.filter((label) => label !== "remove");
617
+ task.labels = newLabels;
618
+ task.updatedDate = "2025-06-08";
619
+ await core.updateTask(task, false);
620
+ }
621
+
622
+ // Verify label was removed
623
+ const updatedTask = await core.filesystem.loadTask("task-5");
624
+ expect(updatedTask?.labels).toEqual(["keep1", "keep2"]);
625
+ });
626
+
627
+ it("should handle non-existent task gracefully", async () => {
628
+ const core = new Core(TEST_DIR);
629
+
630
+ const nonExistentTask = await core.filesystem.loadTask("task-999");
631
+ expect(nonExistentTask).toBeNull();
632
+ });
633
+
634
+ it("should set updated_date field when editing", async () => {
635
+ const core = new Core(TEST_DIR);
636
+
637
+ // Create a test task
638
+ await core.createTask(
639
+ {
640
+ id: "task-6",
641
+ title: "Updated Date Test",
642
+ status: "To Do",
643
+ assignee: [],
644
+ createdDate: "2025-06-07",
645
+ labels: [],
646
+ dependencies: [],
647
+ description: "Testing updated date",
648
+ },
649
+ false,
650
+ );
651
+
652
+ // Edit the task
653
+ const task = await core.filesystem.loadTask("task-6");
654
+ if (task) {
655
+ task.title = "Updated Title";
656
+ task.updatedDate = "2025-06-08";
657
+ await core.updateTask(task, false);
658
+ }
659
+
660
+ // Verify updated_date was set
661
+ const updatedTask = await core.filesystem.loadTask("task-6");
662
+ expect(updatedTask?.updatedDate).toBe("2025-06-08");
663
+ expect(updatedTask?.createdDate).toBe("2025-06-07"); // Should remain unchanged
664
+ });
665
+
666
+ it("should commit changes automatically", async () => {
667
+ const core = new Core(TEST_DIR);
668
+
669
+ // Create a test task
670
+ await core.createTask(
671
+ {
672
+ id: "task-7",
673
+ title: "Commit Test",
674
+ status: "To Do",
675
+ assignee: [],
676
+ createdDate: "2025-06-08",
677
+ labels: [],
678
+ dependencies: [],
679
+ description: "Testing auto-commit",
680
+ },
681
+ false,
682
+ );
683
+
684
+ // Edit the task with auto-commit enabled
685
+ const task = await core.filesystem.loadTask("task-7");
686
+ if (task) {
687
+ task.title = "Updated for Commit";
688
+ task.updatedDate = "2025-06-08";
689
+ await core.updateTask(task, true); // autoCommit = true
690
+ }
691
+
692
+ // Verify the task was updated (this confirms the update functionality works)
693
+ const updatedTask = await core.filesystem.loadTask("task-7");
694
+ expect(updatedTask?.title).toBe("Updated for Commit");
695
+
696
+ // For now, just verify that updateTask with autoCommit=true doesn't throw
697
+ // The actual git commit functionality is tested at the Core level
698
+ });
699
+
700
+ it("should preserve YAML frontmatter formatting", async () => {
701
+ const core = new Core(TEST_DIR);
702
+
703
+ // Create a test task
704
+ await core.createTask(
705
+ {
706
+ id: "task-8",
707
+ title: "YAML Test",
708
+ status: "To Do",
709
+ assignee: ["testuser"],
710
+ createdDate: "2025-06-08",
711
+ labels: ["yaml", "test"],
712
+ dependencies: ["task-1"],
713
+ description: "Testing YAML preservation",
714
+ },
715
+ false,
716
+ );
717
+
718
+ // Edit the task
719
+ const task = await core.filesystem.loadTask("task-8");
720
+ if (task) {
721
+ task.title = "Updated YAML Test";
722
+ task.status = "In Progress";
723
+ task.updatedDate = "2025-06-08";
724
+ await core.updateTask(task, false);
725
+ }
726
+
727
+ // Verify all frontmatter fields are preserved
728
+ const updatedTask = await core.filesystem.loadTask("task-8");
729
+ expect(updatedTask?.id).toBe("task-8");
730
+ expect(updatedTask?.title).toBe("Updated YAML Test");
731
+ expect(updatedTask?.status).toBe("In Progress");
732
+ expect(updatedTask?.assignee).toEqual(["testuser"]);
733
+ expect(updatedTask?.createdDate).toBe("2025-06-08");
734
+ expect(updatedTask?.updatedDate).toBe("2025-06-08");
735
+ expect(updatedTask?.labels).toEqual(["yaml", "test"]);
736
+ expect(updatedTask?.dependencies).toEqual(["task-1"]);
737
+ expect(updatedTask?.description).toBe("## Description\n\nTesting YAML preservation");
738
+ });
739
+ });
740
+
741
+ describe("task archive and state transition commands", () => {
742
+ beforeEach(async () => {
743
+ // Set up a git repository and initialize backlog
744
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
745
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
746
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
747
+
748
+ const core = new Core(TEST_DIR);
749
+ await core.initializeProject("Archive Test Project");
750
+ });
751
+
752
+ it("should archive a task", async () => {
753
+ const core = new Core(TEST_DIR);
754
+
755
+ // Create a test task
756
+ await core.createTask(
757
+ {
758
+ id: "task-1",
759
+ title: "Archive Test Task",
760
+ status: "Done",
761
+ assignee: [],
762
+ createdDate: "2025-06-08",
763
+ labels: ["completed"],
764
+ dependencies: [],
765
+ description: "Task ready for archiving",
766
+ },
767
+ false,
768
+ );
769
+
770
+ // Archive the task
771
+ const success = await core.archiveTask("task-1", false);
772
+ expect(success).toBe(true);
773
+
774
+ // Verify task is no longer in tasks directory
775
+ const task = await core.filesystem.loadTask("task-1");
776
+ expect(task).toBeNull();
777
+
778
+ // Verify task exists in archive
779
+ const { readdir } = await import("node:fs/promises");
780
+ const archiveFiles = await readdir(join(TEST_DIR, ".backlog", "archive", "tasks"));
781
+ expect(archiveFiles.some((f) => f.startsWith("task-1"))).toBe(true);
782
+ });
783
+
784
+ it("should handle archiving non-existent task", async () => {
785
+ const core = new Core(TEST_DIR);
786
+
787
+ const success = await core.archiveTask("task-999", false);
788
+ expect(success).toBe(false);
789
+ });
790
+
791
+ it("should demote task to drafts", async () => {
792
+ const core = new Core(TEST_DIR);
793
+
794
+ // Create a test task
795
+ await core.createTask(
796
+ {
797
+ id: "task-2",
798
+ title: "Demote Test Task",
799
+ status: "To Do",
800
+ assignee: [],
801
+ createdDate: "2025-06-08",
802
+ labels: ["needs-revision"],
803
+ dependencies: [],
804
+ description: "Task that needs to go back to drafts",
805
+ },
806
+ false,
807
+ );
808
+
809
+ // Demote the task
810
+ const success = await core.demoteTask("task-2", false);
811
+ expect(success).toBe(true);
812
+
813
+ // Verify task is no longer in tasks directory
814
+ const task = await core.filesystem.loadTask("task-2");
815
+ expect(task).toBeNull();
816
+
817
+ // Verify task now exists as a draft
818
+ const draft = await core.filesystem.loadDraft("task-2");
819
+ expect(draft?.id).toBe("task-2");
820
+ expect(draft?.title).toBe("Demote Test Task");
821
+ });
822
+
823
+ it("should promote draft to tasks", async () => {
824
+ const core = new Core(TEST_DIR);
825
+
826
+ // Create a test draft
827
+ await core.createDraft(
828
+ {
829
+ id: "task-3",
830
+ title: "Promote Test Draft",
831
+ status: "Draft",
832
+ assignee: [],
833
+ createdDate: "2025-06-08",
834
+ labels: ["ready"],
835
+ dependencies: [],
836
+ description: "Draft ready for promotion",
837
+ },
838
+ false,
839
+ );
840
+
841
+ // Promote the draft
842
+ const success = await core.promoteDraft("task-3", false);
843
+ expect(success).toBe(true);
844
+
845
+ // Verify draft is no longer in drafts directory
846
+ const draft = await core.filesystem.loadDraft("task-3");
847
+ expect(draft).toBeNull();
848
+
849
+ // Verify draft now exists as a task
850
+ const task = await core.filesystem.loadTask("task-3");
851
+ expect(task?.id).toBe("task-3");
852
+ expect(task?.title).toBe("Promote Test Draft");
853
+ });
854
+
855
+ it("should archive a draft", async () => {
856
+ const core = new Core(TEST_DIR);
857
+
858
+ // Create a test draft
859
+ await core.createDraft(
860
+ {
861
+ id: "task-4",
862
+ title: "Archive Test Draft",
863
+ status: "Draft",
864
+ assignee: [],
865
+ createdDate: "2025-06-08",
866
+ labels: ["cancelled"],
867
+ dependencies: [],
868
+ description: "Draft that should be archived",
869
+ },
870
+ false,
871
+ );
872
+
873
+ // Archive the draft
874
+ const success = await core.archiveDraft("task-4", false);
875
+ expect(success).toBe(true);
876
+
877
+ // Verify draft is no longer in drafts directory
878
+ const draft = await core.filesystem.loadDraft("task-4");
879
+ expect(draft).toBeNull();
880
+
881
+ // Verify draft exists in archive
882
+ const { readdir } = await import("node:fs/promises");
883
+ const archiveFiles = await readdir(join(TEST_DIR, ".backlog", "archive", "drafts"));
884
+ expect(archiveFiles.some((f) => f.startsWith("task-4"))).toBe(true);
885
+ });
886
+
887
+ it("should handle promoting non-existent draft", async () => {
888
+ const core = new Core(TEST_DIR);
889
+
890
+ const success = await core.promoteDraft("task-999", false);
891
+ expect(success).toBe(false);
892
+ });
893
+
894
+ it("should handle demoting non-existent task", async () => {
895
+ const core = new Core(TEST_DIR);
896
+
897
+ const success = await core.demoteTask("task-999", false);
898
+ expect(success).toBe(false);
899
+ });
900
+
901
+ it("should handle archiving non-existent draft", async () => {
902
+ const core = new Core(TEST_DIR);
903
+
904
+ const success = await core.archiveDraft("task-999", false);
905
+ expect(success).toBe(false);
906
+ });
907
+
908
+ it("should commit archive operations automatically", async () => {
909
+ const core = new Core(TEST_DIR);
910
+
911
+ // Create and archive a task with auto-commit
912
+ await core.createTask(
913
+ {
914
+ id: "task-5",
915
+ title: "Commit Archive Test",
916
+ status: "Done",
917
+ assignee: [],
918
+ createdDate: "2025-06-08",
919
+ labels: [],
920
+ dependencies: [],
921
+ description: "Testing auto-commit on archive",
922
+ },
923
+ false,
924
+ );
925
+
926
+ const success = await core.archiveTask("task-5", true); // autoCommit = true
927
+ expect(success).toBe(true);
928
+
929
+ // Verify operation completed successfully
930
+ const task = await core.filesystem.loadTask("task-5");
931
+ expect(task).toBeNull();
932
+ });
933
+
934
+ it("should preserve task content through state transitions", async () => {
935
+ const core = new Core(TEST_DIR);
936
+
937
+ // Create a task with rich content
938
+ const originalTask = {
939
+ id: "task-6",
940
+ title: "Content Preservation Test",
941
+ status: "In Progress",
942
+ assignee: ["testuser"],
943
+ createdDate: "2025-06-08",
944
+ labels: ["important", "preservation-test"],
945
+ dependencies: ["task-1", "task-2"],
946
+ description: "This task has rich metadata that should be preserved through transitions",
947
+ };
948
+
949
+ await core.createTask(originalTask, false);
950
+
951
+ // Demote to draft
952
+ await core.demoteTask("task-6", false);
953
+ const asDraft = await core.filesystem.loadDraft("task-6");
954
+
955
+ expect(asDraft?.title).toBe(originalTask.title);
956
+ expect(asDraft?.assignee).toEqual(originalTask.assignee);
957
+ expect(asDraft?.labels).toEqual(originalTask.labels);
958
+ expect(asDraft?.dependencies).toEqual(originalTask.dependencies);
959
+ expect(asDraft?.description).toBe(originalTask.description);
960
+
961
+ // Promote back to task
962
+ await core.promoteDraft("task-6", false);
963
+ const backToTask = await core.filesystem.loadTask("task-6");
964
+
965
+ expect(backToTask?.title).toBe(originalTask.title);
966
+ expect(backToTask?.assignee).toEqual(originalTask.assignee);
967
+ expect(backToTask?.labels).toEqual(originalTask.labels);
968
+ expect(backToTask?.dependencies).toEqual(originalTask.dependencies);
969
+ expect(backToTask?.description).toBe(originalTask.description);
970
+ });
971
+ });
972
+
973
+ describe("doc and decision commands", () => {
974
+ beforeEach(async () => {
975
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
976
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
977
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
978
+
979
+ const core = new Core(TEST_DIR);
980
+ await core.initializeProject("Doc Test Project");
981
+ });
982
+
983
+ it("should create and list documents", async () => {
984
+ const core = new Core(TEST_DIR);
985
+ const doc: DocType = {
986
+ id: "doc-1",
987
+ title: "Guide",
988
+ type: "guide",
989
+ createdDate: "2025-06-08",
990
+ content: "Content",
991
+ };
992
+ await core.createDocument(doc, false);
993
+
994
+ const docs = await core.filesystem.listDocuments();
995
+ expect(docs).toHaveLength(1);
996
+ expect(docs[0].title).toBe("Guide");
997
+ });
998
+
999
+ it("should create and list decisions", async () => {
1000
+ const core = new Core(TEST_DIR);
1001
+ const decision: DecisionLog = {
1002
+ id: "decision-1",
1003
+ title: "Choose Stack",
1004
+ date: "2025-06-08",
1005
+ status: "accepted",
1006
+ context: "context",
1007
+ decision: "decide",
1008
+ consequences: "conseq",
1009
+ };
1010
+ await core.createDecisionLog(decision, false);
1011
+ const decisions = await core.filesystem.listDecisionLogs();
1012
+ expect(decisions).toHaveLength(1);
1013
+ expect(decisions[0].title).toBe("Choose Stack");
1014
+ });
1015
+ });
1016
+
1017
+ describe("board view command", () => {
1018
+ beforeEach(async () => {
1019
+ await Bun.spawn(["git", "init"], { cwd: TEST_DIR }).exited;
1020
+ await Bun.spawn(["git", "config", "user.name", "Test User"], { cwd: TEST_DIR }).exited;
1021
+ await Bun.spawn(["git", "config", "user.email", "test@example.com"], { cwd: TEST_DIR }).exited;
1022
+
1023
+ const core = new Core(TEST_DIR);
1024
+ await core.initializeProject("Board Test Project");
1025
+ });
1026
+
1027
+ it("should display kanban board with tasks grouped by status", async () => {
1028
+ const core = new Core(TEST_DIR);
1029
+
1030
+ // Create test tasks with different statuses
1031
+ await core.createTask(
1032
+ {
1033
+ id: "task-1",
1034
+ title: "Todo Task",
1035
+ status: "To Do",
1036
+ assignee: [],
1037
+ createdDate: "2025-06-08",
1038
+ labels: [],
1039
+ dependencies: [],
1040
+ description: "A task in todo",
1041
+ },
1042
+ false,
1043
+ );
1044
+
1045
+ await core.createTask(
1046
+ {
1047
+ id: "task-2",
1048
+ title: "Progress Task",
1049
+ status: "In Progress",
1050
+ assignee: [],
1051
+ createdDate: "2025-06-08",
1052
+ labels: [],
1053
+ dependencies: [],
1054
+ description: "A task in progress",
1055
+ },
1056
+ false,
1057
+ );
1058
+
1059
+ await core.createTask(
1060
+ {
1061
+ id: "task-3",
1062
+ title: "Done Task",
1063
+ status: "Done",
1064
+ assignee: [],
1065
+ createdDate: "2025-06-08",
1066
+ labels: [],
1067
+ dependencies: [],
1068
+ description: "A completed task",
1069
+ },
1070
+ false,
1071
+ );
1072
+
1073
+ const tasks = await core.filesystem.listTasks();
1074
+ expect(tasks).toHaveLength(3);
1075
+
1076
+ const config = await core.filesystem.loadConfig();
1077
+ const statuses = config?.statuses || [];
1078
+ expect(statuses).toEqual(["To Do", "In Progress", "Done"]);
1079
+
1080
+ // Test the kanban board generation
1081
+ const { generateKanbanBoard } = await import("../board.ts");
1082
+ const board = generateKanbanBoard(tasks, statuses);
1083
+
1084
+ // Verify board contains all statuses and tasks (now on separate lines)
1085
+ expect(board).toContain("To Do");
1086
+ expect(board).toContain("In Progress");
1087
+ expect(board).toContain("Done");
1088
+ expect(board).toContain("task-1");
1089
+ expect(board).toContain("Todo Task");
1090
+ expect(board).toContain("task-2");
1091
+ expect(board).toContain("Progress Task");
1092
+ expect(board).toContain("task-3");
1093
+ expect(board).toContain("Done Task");
1094
+
1095
+ // Verify board structure
1096
+ const lines = board.split("\n");
1097
+ expect(lines[0]).toContain("To Do"); // Header should contain statuses with tasks
1098
+ expect(lines[0]).toContain("In Progress");
1099
+ expect(lines[0]).toContain("Done");
1100
+ expect(lines[1]).toContain("-"); // Separator line
1101
+ expect(lines.length).toBeGreaterThan(2); // Should have content rows
1102
+ });
1103
+
1104
+ it("should handle empty project with default statuses", async () => {
1105
+ const core = new Core(TEST_DIR);
1106
+
1107
+ const tasks = await core.filesystem.listTasks();
1108
+ expect(tasks).toHaveLength(0);
1109
+
1110
+ const config = await core.filesystem.loadConfig();
1111
+ const statuses = config?.statuses || [];
1112
+
1113
+ const { generateKanbanBoard } = await import("../board.ts");
1114
+ const board = generateKanbanBoard(tasks, statuses);
1115
+
1116
+ // Should return empty board when no tasks exist
1117
+ expect(board).toBe("");
1118
+ });
1119
+
1120
+ it("should support vertical layout option", async () => {
1121
+ const core = new Core(TEST_DIR);
1122
+
1123
+ await core.createTask(
1124
+ {
1125
+ id: "task-1",
1126
+ title: "Todo Task",
1127
+ status: "To Do",
1128
+ assignee: [],
1129
+ createdDate: "2025-06-08",
1130
+ labels: [],
1131
+ dependencies: [],
1132
+ description: "A task in todo",
1133
+ },
1134
+ false,
1135
+ );
1136
+
1137
+ const tasks = await core.filesystem.listTasks();
1138
+ const config = await core.filesystem.loadConfig();
1139
+ const statuses = config?.statuses || [];
1140
+
1141
+ const { generateKanbanBoard } = await import("../board.ts");
1142
+ const board = generateKanbanBoard(tasks, statuses, "vertical");
1143
+
1144
+ const lines = board.split("\n");
1145
+ expect(lines[0]).toBe("To Do");
1146
+ expect(board).toContain("task-1");
1147
+ expect(board).toContain("Todo Task");
1148
+ });
1149
+
1150
+ it("should support --vertical shortcut flag", async () => {
1151
+ const core = new Core(TEST_DIR);
1152
+
1153
+ await core.createTask(
1154
+ {
1155
+ id: "task-1",
1156
+ title: "Shortcut Task",
1157
+ status: "To Do",
1158
+ assignee: [],
1159
+ createdDate: "2025-06-09",
1160
+ labels: [],
1161
+ dependencies: [],
1162
+ description: "Testing vertical shortcut",
1163
+ },
1164
+ false,
1165
+ );
1166
+
1167
+ const tasks = await core.filesystem.listTasks();
1168
+ const config = await core.filesystem.loadConfig();
1169
+ const statuses = config?.statuses || [];
1170
+
1171
+ // Test that --vertical flag produces vertical layout
1172
+ const { generateKanbanBoard } = await import("../board.ts");
1173
+ const board = generateKanbanBoard(tasks, statuses, "vertical");
1174
+
1175
+ const lines = board.split("\n");
1176
+ expect(lines[0]).toBe("To Do");
1177
+ expect(board).toContain("task-1");
1178
+ expect(board).toContain("Shortcut Task");
1179
+ });
1180
+
1181
+ it("should merge task status from remote branches", async () => {
1182
+ const core = new Core(TEST_DIR);
1183
+
1184
+ const task = {
1185
+ id: "task-1",
1186
+ title: "Remote Task",
1187
+ status: "To Do",
1188
+ assignee: [],
1189
+ createdDate: "2025-06-09",
1190
+ labels: [],
1191
+ dependencies: [],
1192
+ description: "from remote",
1193
+ } as Task;
1194
+
1195
+ await core.createTask(task, true);
1196
+
1197
+ // set up remote repository
1198
+ const remoteDir = join(TEST_DIR, "remote.git");
1199
+ await Bun.spawn(["git", "init", "--bare", remoteDir]).exited;
1200
+ await Bun.spawn(["git", "remote", "add", "origin", remoteDir], { cwd: TEST_DIR }).exited;
1201
+ await Bun.spawn(["git", "push", "-u", "origin", "master"], { cwd: TEST_DIR }).exited;
1202
+
1203
+ // create branch with updated status
1204
+ await Bun.spawn(["git", "checkout", "-b", "feature"], { cwd: TEST_DIR }).exited;
1205
+ await core.updateTask({ ...task, status: "Done" }, true);
1206
+ await Bun.spawn(["git", "push", "-u", "origin", "feature"], { cwd: TEST_DIR }).exited;
1207
+
1208
+ // switch back to master where status is still To Do
1209
+ await Bun.spawn(["git", "checkout", "master"], { cwd: TEST_DIR }).exited;
1210
+
1211
+ await core.gitOps.fetch();
1212
+ const branches = await core.gitOps.listRemoteBranches();
1213
+ const config = await core.filesystem.loadConfig();
1214
+ const statuses = config?.statuses || [];
1215
+
1216
+ const localTasks = await core.filesystem.listTasks();
1217
+ const tasksById = new Map(localTasks.map((t) => [t.id, t]));
1218
+
1219
+ for (const branch of branches) {
1220
+ const ref = `origin/${branch}`;
1221
+ const files = await core.gitOps.listFilesInTree(ref, ".backlog/tasks");
1222
+ for (const file of files) {
1223
+ const content = await core.gitOps.showFile(ref, file);
1224
+ const remoteTask = parseTask(content);
1225
+ const existing = tasksById.get(remoteTask.id);
1226
+ const currentIdx = existing ? statuses.indexOf(existing.status) : -1;
1227
+ const newIdx = statuses.indexOf(remoteTask.status);
1228
+ if (!existing || newIdx > currentIdx || currentIdx === -1 || newIdx === currentIdx) {
1229
+ tasksById.set(remoteTask.id, remoteTask);
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ const final = tasksById.get("task-1");
1235
+ expect(final?.status).toBe("Done");
1236
+ });
1237
+
1238
+ it("should default to view when no subcommand is provided", async () => {
1239
+ const core = new Core(TEST_DIR);
1240
+
1241
+ await core.createTask(
1242
+ {
1243
+ id: "task-99",
1244
+ title: "Default Cmd Task",
1245
+ status: "To Do",
1246
+ assignee: [],
1247
+ createdDate: "2025-06-10",
1248
+ labels: [],
1249
+ dependencies: [],
1250
+ description: "test",
1251
+ },
1252
+ false,
1253
+ );
1254
+
1255
+ const resultDefault = Bun.spawnSync(["bun", "src/cli.ts", "board"], { cwd: TEST_DIR });
1256
+ const resultView = Bun.spawnSync(["bun", "src/cli.ts", "board", "view"], { cwd: TEST_DIR });
1257
+
1258
+ expect(resultDefault.stdout.toString()).toBe(resultView.stdout.toString());
1259
+ });
1260
+
1261
+ it("should export kanban board to file", async () => {
1262
+ const core = new Core(TEST_DIR);
1263
+
1264
+ // Create test tasks
1265
+ await core.createTask(
1266
+ {
1267
+ id: "task-1",
1268
+ title: "Export Test Task",
1269
+ status: "To Do",
1270
+ assignee: [],
1271
+ createdDate: "2025-06-09",
1272
+ labels: [],
1273
+ dependencies: [],
1274
+ description: "Testing board export",
1275
+ },
1276
+ false,
1277
+ );
1278
+
1279
+ const { exportKanbanBoardToFile } = await import("../index.ts");
1280
+ const outputPath = join(TEST_DIR, "test-export.md");
1281
+ const tasks = await core.filesystem.listTasks();
1282
+ const config = await core.filesystem.loadConfig();
1283
+ const statuses = config?.statuses || [];
1284
+
1285
+ await exportKanbanBoardToFile(tasks, statuses, outputPath);
1286
+
1287
+ // Verify file was created and contains expected content
1288
+ const content = await Bun.file(outputPath).text();
1289
+ expect(content).toContain("To Do");
1290
+ expect(content).toContain("task-1");
1291
+ expect(content).toContain("Export Test Task");
1292
+
1293
+ // Test appending behavior
1294
+ await exportKanbanBoardToFile(tasks, statuses, outputPath);
1295
+ const appendedContent = await Bun.file(outputPath).text();
1296
+ const occurrences = appendedContent.split("task-1").length - 1;
1297
+ expect(occurrences).toBe(2); // Should appear twice after appending
1298
+ });
1299
+ });
1300
+ });