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,526 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parseDecisionLog, parseDocument, parseMarkdown, parseTask } from "../markdown/parser.ts";
3
+ import {
4
+ serializeDecisionLog,
5
+ serializeDocument,
6
+ serializeTask,
7
+ updateTaskAcceptanceCriteria,
8
+ } from "../markdown/serializer.ts";
9
+ import type { DecisionLog, Document, Task } from "../types/index.ts";
10
+
11
+ describe("Markdown Parser", () => {
12
+ describe("parseMarkdown", () => {
13
+ it("should parse frontmatter and content", () => {
14
+ const content = `---
15
+ title: "Test Task"
16
+ status: "To Do"
17
+ labels: ["bug", "urgent"]
18
+ ---
19
+
20
+ This is the task description.
21
+
22
+ ## Acceptance Criteria
23
+
24
+ - [ ] First criterion
25
+ - [ ] Second criterion`;
26
+
27
+ const result = parseMarkdown(content);
28
+
29
+ expect(result.frontmatter.title).toBe("Test Task");
30
+ expect(result.frontmatter.status).toBe("To Do");
31
+ expect(result.frontmatter.labels).toEqual(["bug", "urgent"]);
32
+ expect(result.content).toContain("This is the task description");
33
+ });
34
+
35
+ it("should handle content without frontmatter", () => {
36
+ const content = "Just some markdown content";
37
+ const result = parseMarkdown(content);
38
+
39
+ expect(result.frontmatter).toEqual({});
40
+ expect(result.content).toBe("Just some markdown content");
41
+ });
42
+
43
+ it("should handle empty content", () => {
44
+ const content = "";
45
+ const result = parseMarkdown(content);
46
+
47
+ expect(result.frontmatter).toEqual({});
48
+ expect(result.content).toBe("");
49
+ });
50
+ });
51
+
52
+ describe("parseTask", () => {
53
+ it("should parse a complete task", () => {
54
+ const content = `---
55
+ id: task-1
56
+ title: "Fix login bug"
57
+ status: "In Progress"
58
+ assignee: "@developer"
59
+ reporter: "@manager"
60
+ created_date: "2025-06-03"
61
+ labels: ["bug", "frontend"]
62
+ milestone: "v1.0"
63
+ dependencies: ["task-0"]
64
+ parent_task_id: "task-parent"
65
+ subtasks: ["task-1.1", "task-1.2"]
66
+ ---
67
+
68
+ ## Description
69
+
70
+ Fix the login bug that prevents users from signing in.
71
+
72
+ ## Acceptance Criteria
73
+
74
+ - [ ] Login form validates correctly
75
+ - [ ] Error messages are displayed properly`;
76
+
77
+ const task = parseTask(content);
78
+
79
+ expect(task.id).toBe("task-1");
80
+ expect(task.title).toBe("Fix login bug");
81
+ expect(task.status).toBe("In Progress");
82
+ expect(task.assignee).toEqual(["@developer"]);
83
+ expect(task.reporter).toBe("@manager");
84
+ expect(task.createdDate).toBe("2025-06-03");
85
+ expect(task.labels).toEqual(["bug", "frontend"]);
86
+ expect(task.milestone).toBe("v1.0");
87
+ expect(task.dependencies).toEqual(["task-0"]);
88
+ expect(task.parentTaskId).toBe("task-parent");
89
+ expect(task.subtasks).toEqual(["task-1.1", "task-1.2"]);
90
+ expect(task.acceptanceCriteria).toEqual([
91
+ "Login form validates correctly",
92
+ "Error messages are displayed properly",
93
+ ]);
94
+ });
95
+
96
+ it("should parse a task with minimal fields", () => {
97
+ const content = `---
98
+ id: task-2
99
+ title: "Simple task"
100
+ ---
101
+
102
+ Just a basic task.`;
103
+
104
+ const task = parseTask(content);
105
+
106
+ expect(task.id).toBe("task-2");
107
+ expect(task.title).toBe("Simple task");
108
+ expect(task.status).toBe("");
109
+ expect(task.assignee).toEqual([]);
110
+ expect(task.reporter).toBeUndefined();
111
+ expect(task.labels).toEqual([]);
112
+ expect(task.dependencies).toEqual([]);
113
+ expect(task.acceptanceCriteria).toEqual([]);
114
+ expect(task.parentTaskId).toBeUndefined();
115
+ expect(task.subtasks).toBeUndefined();
116
+ });
117
+
118
+ it("should handle task with empty status", () => {
119
+ const content = `---
120
+ id: task-3
121
+ title: "No status task"
122
+ created_date: "2025-06-07"
123
+ ---
124
+
125
+ Task without status.`;
126
+
127
+ const task = parseTask(content);
128
+
129
+ expect(task.status).toBe("");
130
+ expect(task.createdDate).toBe("2025-06-07");
131
+ });
132
+
133
+ it("should parse unquoted created_date", () => {
134
+ const content = `---
135
+ id: task-5
136
+ title: "Unquoted"
137
+ created_date: 2025-06-08
138
+ ---`;
139
+
140
+ const task = parseTask(content);
141
+
142
+ expect(task.createdDate).toBe("2025-06-08");
143
+ });
144
+
145
+ it("should parse created_date in short format", () => {
146
+ const content = `---
147
+ id: task-6
148
+ title: "Short"
149
+ created_date: 08-06-25
150
+ ---`;
151
+
152
+ const task = parseTask(content);
153
+
154
+ expect(task.createdDate).toBe("2025-06-08");
155
+ });
156
+
157
+ it("should extract acceptance criteria with checked items", () => {
158
+ const content = `---
159
+ id: task-4
160
+ title: "Test with mixed criteria"
161
+ ---
162
+
163
+ ## Acceptance Criteria
164
+
165
+ - [ ] Todo item
166
+ - [x] Done item
167
+ - [ ] Another todo`;
168
+
169
+ const task = parseTask(content);
170
+
171
+ expect(task.acceptanceCriteria).toEqual(["Todo item", "Done item", "Another todo"]);
172
+ });
173
+
174
+ it("should parse unquoted assignee names starting with @", () => {
175
+ const content = `---
176
+ id: task-5
177
+ title: "Assignee Test"
178
+ assignee: @MrLesk
179
+ ---
180
+
181
+ Test task.`;
182
+
183
+ const task = parseTask(content);
184
+
185
+ expect(task.assignee).toEqual(["@MrLesk"]);
186
+ });
187
+
188
+ it("should parse unquoted reporter names starting with @", () => {
189
+ const content = `---
190
+ id: task-6
191
+ title: "Reporter Test"
192
+ assignee: []
193
+ reporter: @MrLesk
194
+ created_date: 2025-06-08
195
+ ---
196
+
197
+ Test task with reporter.`;
198
+
199
+ const task = parseTask(content);
200
+
201
+ expect(task.reporter).toBe("@MrLesk");
202
+ });
203
+ });
204
+
205
+ describe("parseDecisionLog", () => {
206
+ it("should parse a decision log", () => {
207
+ const content = `---
208
+ id: decision-1
209
+ title: "Use TypeScript for backend"
210
+ date: "2025-06-03"
211
+ status: "accepted"
212
+ ---
213
+
214
+ ## Context
215
+
216
+ We need to choose a language for the backend.
217
+
218
+ ## Decision
219
+
220
+ We will use TypeScript for better type safety.
221
+
222
+ ## Consequences
223
+
224
+ Better development experience but steeper learning curve.`;
225
+
226
+ const decision = parseDecisionLog(content);
227
+
228
+ expect(decision.id).toBe("decision-1");
229
+ expect(decision.title).toBe("Use TypeScript for backend");
230
+ expect(decision.status).toBe("accepted");
231
+ expect(decision.context).toBe("We need to choose a language for the backend.");
232
+ expect(decision.decision).toBe("We will use TypeScript for better type safety.");
233
+ expect(decision.consequences).toBe("Better development experience but steeper learning curve.");
234
+ });
235
+
236
+ it("should parse decision log with alternatives", () => {
237
+ const content = `---
238
+ id: decision-2
239
+ title: "Choose database"
240
+ date: "2025-06-03"
241
+ status: "proposed"
242
+ ---
243
+
244
+ ## Context
245
+
246
+ Need a database solution.
247
+
248
+ ## Decision
249
+
250
+ Use PostgreSQL.
251
+
252
+ ## Consequences
253
+
254
+ Good performance and reliability.
255
+
256
+ ## Alternatives
257
+
258
+ Considered MongoDB and MySQL.`;
259
+
260
+ const decision = parseDecisionLog(content);
261
+
262
+ expect(decision.alternatives).toBe("Considered MongoDB and MySQL.");
263
+ });
264
+
265
+ it("should handle missing sections", () => {
266
+ const content = `---
267
+ id: decision-3
268
+ title: "Minimal decision"
269
+ date: "2025-06-03"
270
+ status: "proposed"
271
+ ---
272
+
273
+ ## Context
274
+
275
+ Some context.`;
276
+
277
+ const decision = parseDecisionLog(content);
278
+
279
+ expect(decision.context).toBe("Some context.");
280
+ expect(decision.decision).toBe("");
281
+ expect(decision.consequences).toBe("");
282
+ expect(decision.alternatives).toBeUndefined();
283
+ });
284
+ });
285
+
286
+ describe("parseDocument", () => {
287
+ it("should parse a document", () => {
288
+ const content = `---
289
+ id: doc-1
290
+ title: "API Guide"
291
+ type: "guide"
292
+ created_date: 2025-06-07
293
+ tags: [api]
294
+ ---
295
+
296
+ Document body.`;
297
+
298
+ const doc = parseDocument(content);
299
+
300
+ expect(doc.id).toBe("doc-1");
301
+ expect(doc.title).toBe("API Guide");
302
+ expect(doc.type).toBe("guide");
303
+ expect(doc.createdDate).toBe("2025-06-07");
304
+ expect(doc.tags).toEqual(["api"]);
305
+ expect(doc.content).toBe("Document body.");
306
+ });
307
+ });
308
+ });
309
+
310
+ describe("Markdown Serializer", () => {
311
+ describe("serializeTask", () => {
312
+ it("should serialize a task correctly", () => {
313
+ const task: Task = {
314
+ id: "task-1",
315
+ title: "Test Task",
316
+ status: "To Do",
317
+ assignee: ["@developer"],
318
+ reporter: "@manager",
319
+ createdDate: "2025-06-03",
320
+ labels: ["bug", "frontend"],
321
+ milestone: "v1.0",
322
+ dependencies: ["task-0"],
323
+ description: "This is a test task description.",
324
+ };
325
+
326
+ const result = serializeTask(task);
327
+
328
+ expect(result).toContain("id: task-1");
329
+ expect(result).toContain("title: Test Task");
330
+ expect(result).toContain("status: To Do");
331
+ expect(result).toContain("created_date: '2025-06-03'");
332
+ expect(result).toContain("labels:");
333
+ expect(result).toContain("- bug");
334
+ expect(result).toContain("- frontend");
335
+ expect(result).toContain("This is a test task description.");
336
+ });
337
+
338
+ it("should serialize task with subtasks", () => {
339
+ const task: Task = {
340
+ id: "task-parent",
341
+ title: "Parent Task",
342
+ status: "In Progress",
343
+ assignee: [],
344
+ createdDate: "2025-06-03",
345
+ labels: [],
346
+ dependencies: [],
347
+ description: "A parent task with subtasks.",
348
+ subtasks: ["task-parent.1", "task-parent.2"],
349
+ };
350
+
351
+ const result = serializeTask(task);
352
+
353
+ expect(result).toContain("subtasks:");
354
+ expect(result).toContain("- task-parent.1");
355
+ expect(result).toContain("- task-parent.2");
356
+ });
357
+
358
+ it("should serialize task with parent", () => {
359
+ const task: Task = {
360
+ id: "task-1.1",
361
+ title: "Subtask",
362
+ status: "To Do",
363
+ assignee: [],
364
+ createdDate: "2025-06-03",
365
+ labels: [],
366
+ dependencies: [],
367
+ description: "A subtask.",
368
+ parentTaskId: "task-1",
369
+ };
370
+
371
+ const result = serializeTask(task);
372
+
373
+ expect(result).toContain("parent_task_id: task-1");
374
+ });
375
+
376
+ it("should serialize minimal task", () => {
377
+ const task: Task = {
378
+ id: "task-minimal",
379
+ title: "Minimal Task",
380
+ status: "Draft",
381
+ assignee: [],
382
+ createdDate: "2025-06-03",
383
+ labels: [],
384
+ dependencies: [],
385
+ description: "Minimal task.",
386
+ };
387
+
388
+ const result = serializeTask(task);
389
+
390
+ expect(result).toContain("id: task-minimal");
391
+ expect(result).toContain("title: Minimal Task");
392
+ expect(result).toContain("assignee: []");
393
+ expect(result).not.toContain("reporter:");
394
+ expect(result).not.toContain("updated_date:");
395
+ });
396
+ });
397
+
398
+ describe("serializeDecisionLog", () => {
399
+ it("should serialize a decision log correctly", () => {
400
+ const decision: DecisionLog = {
401
+ id: "decision-1",
402
+ title: "Use TypeScript",
403
+ date: "2025-06-03",
404
+ status: "accepted",
405
+ context: "We need type safety",
406
+ decision: "Use TypeScript",
407
+ consequences: "Better DX",
408
+ };
409
+
410
+ const result = serializeDecisionLog(decision);
411
+
412
+ expect(result).toContain("id: decision-1");
413
+ expect(result).toContain("## Context");
414
+ expect(result).toContain("We need type safety");
415
+ expect(result).toContain("## Decision");
416
+ expect(result).toContain("Use TypeScript");
417
+ });
418
+
419
+ it("should serialize decision log with alternatives", () => {
420
+ const decision: DecisionLog = {
421
+ id: "decision-2",
422
+ title: "Database Choice",
423
+ date: "2025-06-03",
424
+ status: "accepted",
425
+ context: "Need database",
426
+ decision: "PostgreSQL",
427
+ consequences: "Good performance",
428
+ alternatives: "Considered MongoDB",
429
+ };
430
+
431
+ const result = serializeDecisionLog(decision);
432
+
433
+ expect(result).toContain("## Alternatives");
434
+ expect(result).toContain("Considered MongoDB");
435
+ });
436
+ });
437
+
438
+ describe("serializeDocument", () => {
439
+ it("should serialize a document correctly", () => {
440
+ const document: Document = {
441
+ id: "doc-1",
442
+ title: "API Documentation",
443
+ type: "specification",
444
+ createdDate: "2025-06-07",
445
+ updatedDate: "2025-06-08",
446
+ content: "This document describes the API endpoints.",
447
+ tags: ["api", "docs"],
448
+ };
449
+
450
+ const result = serializeDocument(document);
451
+
452
+ expect(result).toContain("id: doc-1");
453
+ expect(result).toContain("title: API Documentation");
454
+ expect(result).toContain("type: specification");
455
+ expect(result).toContain("created_date: '2025-06-07'");
456
+ expect(result).toContain("updated_date: '2025-06-08'");
457
+ expect(result).toContain("tags:");
458
+ expect(result).toContain("- api");
459
+ expect(result).toContain("- docs");
460
+ expect(result).toContain("This document describes the API endpoints.");
461
+ });
462
+
463
+ it("should serialize document without optional fields", () => {
464
+ const document: Document = {
465
+ id: "doc-2",
466
+ title: "Simple Doc",
467
+ type: "guide",
468
+ createdDate: "2025-06-07",
469
+ content: "Simple content.",
470
+ };
471
+
472
+ const result = serializeDocument(document);
473
+
474
+ expect(result).toContain("id: doc-2");
475
+ expect(result).not.toContain("updated_date:");
476
+ expect(result).not.toContain("tags:");
477
+ });
478
+ });
479
+
480
+ describe("updateTaskAcceptanceCriteria", () => {
481
+ it("should add acceptance criteria to content without existing section", () => {
482
+ const content = "# Task Description\n\nThis is a simple task.";
483
+ const criteria = ["Login works correctly", "Error handling is proper"];
484
+
485
+ const result = updateTaskAcceptanceCriteria(content, criteria);
486
+
487
+ expect(result).toContain("## Acceptance Criteria");
488
+ expect(result).toContain("- [ ] Login works correctly");
489
+ expect(result).toContain("- [ ] Error handling is proper");
490
+ });
491
+
492
+ it("should replace existing acceptance criteria section", () => {
493
+ const content = `# Task Description
494
+
495
+ This is a task with existing criteria.
496
+
497
+ ## Acceptance Criteria
498
+
499
+ - [ ] Old criterion 1
500
+ - [ ] Old criterion 2
501
+
502
+ ## Notes
503
+
504
+ Some additional notes.`;
505
+
506
+ const criteria = ["New criterion 1", "New criterion 2"];
507
+
508
+ const result = updateTaskAcceptanceCriteria(content, criteria);
509
+
510
+ expect(result).toContain("- [ ] New criterion 1");
511
+ expect(result).toContain("- [ ] New criterion 2");
512
+ expect(result).not.toContain("Old criterion 1");
513
+ expect(result).toContain("## Notes");
514
+ });
515
+
516
+ it("should handle empty criteria array", () => {
517
+ const content = "# Task Description\n\nSimple task.";
518
+ const criteria: string[] = [];
519
+
520
+ const result = updateTaskAcceptanceCriteria(content, criteria);
521
+
522
+ expect(result).toContain("## Acceptance Criteria");
523
+ expect(result).not.toContain("- [ ]");
524
+ });
525
+ });
526
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { join } from "node:path";
3
+ import type { GitOps } from "../core/git-ops.ts";
4
+ import { loadRemoteTasks, resolveTaskConflict } from "../core/remote-tasks.ts";
5
+ import type { TaskWithMetadata } from "../core/remote-tasks.ts";
6
+
7
+ // Mock GitOps for testing
8
+ class MockGitOps implements Partial<GitOps> {
9
+ private tasks: Record<string, { content: string; timestamp: Date }>[] = [];
10
+
11
+ constructor(tasks: Record<string, { content: string; timestamp: Date }>[]) {
12
+ this.tasks = tasks;
13
+ }
14
+
15
+ async fetch(): Promise<void> {
16
+ // Mock fetch
17
+ }
18
+
19
+ async listRemoteBranches(): Promise<string[]> {
20
+ return ["main", "feature", "feature2"];
21
+ }
22
+
23
+ async listFilesInTree(ref: string, path: string): Promise<string[]> {
24
+ if (ref === "origin/main") {
25
+ return [".backlog/tasks/task-1 - Main Task.md"];
26
+ }
27
+ if (ref === "origin/feature") {
28
+ return [".backlog/tasks/task-2 - Feature Task.md"];
29
+ }
30
+ if (ref === "origin/feature2") {
31
+ return [".backlog/tasks/task-3 - Feature2 Task.md"];
32
+ }
33
+ return [];
34
+ }
35
+
36
+ async showFile(ref: string, file: string): Promise<string> {
37
+ if (file.includes("task-1")) {
38
+ return `---
39
+ id: task-1
40
+ title: Main Task
41
+ status: To Do
42
+ assignee: []
43
+ created_date: 2025-06-13
44
+ labels: []
45
+ dependencies: []
46
+ ---\n\n## Description\n\nMain task`;
47
+ }
48
+ if (file.includes("task-2")) {
49
+ return `---
50
+ id: task-2
51
+ title: Feature Task
52
+ status: In Progress
53
+ assignee: []
54
+ created_date: 2025-06-13
55
+ labels: []
56
+ dependencies: []
57
+ ---\n\n## Description\n\nFeature task`;
58
+ }
59
+ if (file.includes("task-3")) {
60
+ return `---
61
+ id: task-3
62
+ title: Feature2 Task
63
+ status: Done
64
+ assignee: []
65
+ created_date: 2025-06-13
66
+ labels: []
67
+ dependencies: []
68
+ ---\n\n## Description\n\nFeature2 task`;
69
+ }
70
+ return "";
71
+ }
72
+
73
+ async getFileLastModifiedTime(ref: string, file: string): Promise<Date | null> {
74
+ return new Date("2025-06-13");
75
+ }
76
+ }
77
+
78
+ describe("Parallel remote task loading", () => {
79
+ it("should load tasks from multiple branches in parallel", async () => {
80
+ const mockGitOps = new MockGitOps([]) as unknown as GitOps;
81
+
82
+ // Track progress messages
83
+ const progressMessages: string[] = [];
84
+ const remoteTasks = await loadRemoteTasks(mockGitOps, (msg) => {
85
+ progressMessages.push(msg);
86
+ });
87
+
88
+ // Verify results - we should have tasks from all remote branches
89
+ expect(remoteTasks.length).toBe(3);
90
+ const taskIds = remoteTasks.map((t) => t.id);
91
+ expect(taskIds).toContain("task-1");
92
+ expect(taskIds).toContain("task-2");
93
+ expect(taskIds).toContain("task-3");
94
+
95
+ // Verify each task has correct metadata
96
+ const task1 = remoteTasks.find((t) => t.id === "task-1");
97
+ expect(task1?.source).toBe("remote");
98
+ expect(task1?.branch).toBe("main");
99
+ expect(task1?.status).toBe("To Do");
100
+
101
+ // Verify progress reporting
102
+ expect(progressMessages.some((msg) => msg.includes("Fetching remote branches"))).toBe(true);
103
+ expect(progressMessages.some((msg) => msg.includes("Found 3 remote branches"))).toBe(true);
104
+ expect(progressMessages.some((msg) => msg.includes("Loaded 3 total remote tasks"))).toBe(true);
105
+ });
106
+
107
+ it("should handle errors gracefully", async () => {
108
+ // Create a mock that throws an error
109
+ const errorGitOps = {
110
+ fetch: async () => {
111
+ throw new Error("Network error");
112
+ },
113
+ } as unknown as GitOps;
114
+
115
+ // Should return empty array on error
116
+ const remoteTasks = await loadRemoteTasks(errorGitOps);
117
+ expect(remoteTasks).toEqual([]);
118
+ });
119
+
120
+ it("should resolve task conflicts correctly", async () => {
121
+ const statuses = ["To Do", "In Progress", "Done"];
122
+
123
+ const localTask: TaskWithMetadata = {
124
+ id: "task-1",
125
+ title: "Local Task",
126
+ status: "To Do",
127
+ assignee: [],
128
+ createdDate: "2025-06-13",
129
+ labels: [],
130
+ dependencies: [],
131
+ description: "Local version",
132
+ source: "local",
133
+ lastModified: new Date("2025-06-13T10:00:00Z"),
134
+ };
135
+
136
+ const remoteTask: TaskWithMetadata = {
137
+ id: "task-1",
138
+ title: "Remote Task",
139
+ status: "Done",
140
+ assignee: [],
141
+ createdDate: "2025-06-13",
142
+ labels: [],
143
+ dependencies: [],
144
+ description: "Remote version",
145
+ source: "remote",
146
+ branch: "feature",
147
+ lastModified: new Date("2025-06-13T12:00:00Z"),
148
+ };
149
+
150
+ // Test most_progressed strategy - should pick Done over To Do
151
+ const resolved1 = resolveTaskConflict(localTask, remoteTask, statuses, "most_progressed");
152
+ expect(resolved1.status).toBe("Done");
153
+ expect(resolved1.title).toBe("Remote Task");
154
+
155
+ // Test most_recent strategy - should pick the more recent one
156
+ const resolved2 = resolveTaskConflict(localTask, remoteTask, statuses, "most_recent");
157
+ expect(resolved2.lastModified).toEqual(new Date("2025-06-13T12:00:00Z"));
158
+ expect(resolved2.title).toBe("Remote Task");
159
+ });
160
+ });