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.
- package/.backlog/archive/drafts/readme.md +3 -0
- package/.backlog/archive/drafts/task-41 - temporary-test-task.md +13 -0
- package/.backlog/archive/readme.md +6 -0
- package/.backlog/archive/tasks/readme.md +3 -0
- package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +14 -0
- package/.backlog/config.yml +7 -0
- package/.backlog/decisions/readme.md +7 -0
- package/.backlog/docs/readme.md +20 -0
- package/.backlog/drafts/readme.md +3 -0
- package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +21 -0
- package/.backlog/drafts/task-28 - add-code-of-conduct.md +20 -0
- package/.backlog/drafts/task-30 - create-changelog.md +19 -0
- package/.backlog/milestones/m-0 - project-setup.md +8 -0
- package/.backlog/milestones/m-1 - cli.md +8 -0
- package/.backlog/milestones/m-2 - cli-kanban.md +8 -0
- package/.backlog/milestones/m-3 - gui.md +8 -0
- package/.backlog/milestones/m-4 - gui-kanban.md +8 -0
- package/.backlog/milestones/m-5 - gui-advanced.md +12 -0
- package/.backlog/milestones/readme.md +3 -0
- package/.backlog/readme.md +5 -0
- package/.backlog/tasks/readme.md +37 -0
- package/.backlog/tasks/task-1 - cli-setup-core-project.md +23 -0
- package/.backlog/tasks/task-10 - gui-init-packaging.md +23 -0
- package/.backlog/tasks/task-11 - gui-kanban-board.md +26 -0
- package/.backlog/tasks/task-12 - gui-advanced.md +25 -0
- package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +53 -0
- package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +40 -0
- package/.backlog/tasks/task-14 - gui-introduction-screens.md +21 -0
- package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +20 -0
- package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +55 -0
- package/.backlog/tasks/task-2 - cli-core-logic-library.md +28 -0
- package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +32 -0
- package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +31 -0
- package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +24 -0
- package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +30 -0
- package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +38 -0
- package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +19 -0
- package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +28 -0
- package/.backlog/tasks/task-27 - add-contributing-guidelines.md +27 -0
- package/.backlog/tasks/task-29 - add-github-templates.md +28 -0
- package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +63 -0
- package/.backlog/tasks/task-31 - update-readme-for-open-source.md +26 -0
- package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +31 -0
- package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +20 -0
- package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +26 -0
- package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +24 -0
- package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +24 -0
- package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +19 -0
- package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +25 -0
- package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +31 -0
- package/.backlog/tasks/task-4 - cli-task-management-commands.md +28 -0
- package/.backlog/tasks/task-4.1 - cli-task-create.md +27 -0
- package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +51 -0
- package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +23 -0
- package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +53 -0
- package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +58 -0
- package/.backlog/tasks/task-4.2 - cli-task-list-view.md +25 -0
- package/.backlog/tasks/task-4.3 - cli-task-edit.md +24 -0
- package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +27 -0
- package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +28 -0
- package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +35 -0
- package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +40 -0
- package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +48 -0
- package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +66 -0
- package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +38 -0
- package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +93 -0
- package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +42 -0
- package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +44 -0
- package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +45 -0
- package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +49 -0
- package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +55 -0
- package/.backlog/tasks/task-42 - visual-hierarchy.md +54 -0
- package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +56 -0
- package/.backlog/tasks/task-44 - checklist-alignment.md +24 -0
- package/.backlog/tasks/task-45 - safe-line-wrapping.md +23 -0
- package/.backlog/tasks/task-46 - split-pane-layout.md +24 -0
- package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +43 -0
- package/.backlog/tasks/task-48 - footer-hint-line.md +21 -0
- package/.backlog/tasks/task-49 - status-styling.md +53 -0
- package/.backlog/tasks/task-5 - cli-docs-decisions.md +57 -0
- package/.backlog/tasks/task-50 - borders-&-padding.md +22 -0
- package/.backlog/tasks/task-51 - code-path-styling.md +23 -0
- package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +29 -0
- package/.backlog/tasks/task-6 - cli-packaging.md +65 -0
- package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +49 -0
- package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +64 -0
- package/.backlog/tasks/task-7 - cli-kanban-view.md +60 -0
- package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +62 -0
- package/.backlog/tasks/task-8 - gui-project-setup.md +21 -0
- package/.backlog/tasks/task-9 - gui-task-crud.md +24 -0
- package/.cursorrules +223 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +8 -0
- package/.github/workflows/ci.yml +36 -0
- package/.husky/pre-commit +1 -0
- package/AGENTS.md +65 -0
- package/CLAUDE.md +87 -0
- package/CONTRIBUTING.md +19 -0
- package/DEVELOPMENT.md +37 -0
- package/LICENSE +21 -0
- package/biome.json +31 -0
- package/bun.lock +152 -0
- package/cli/.cursorrules-xh86jabm.md +82 -0
- package/cli/AGENTS-xh86jabm.md +82 -0
- package/cli/CLAUDE-xh86jabm.md +82 -0
- package/cli/backlog +0 -0
- package/cli/cli.js +19622 -0
- package/cli/index.js +2 -0
- package/docs/npm-publishing.md +69 -0
- package/package.json +47 -0
- package/readme.md +97 -0
- package/scripts/build.js +73 -0
- package/src/agent-instructions.ts +54 -0
- package/src/board.ts +263 -0
- package/src/cli.ts +806 -0
- package/src/constants/index.ts +48 -0
- package/src/core/backlog.ts +183 -0
- package/src/core/remote-tasks.ts +168 -0
- package/src/file-system/operations.ts +515 -0
- package/src/git/operations.ts +189 -0
- package/src/guidelines/.cursorrules.md +82 -0
- package/src/guidelines/AGENTS.md +82 -0
- package/src/guidelines/CLAUDE.md +82 -0
- package/src/guidelines/index.ts +7 -0
- package/src/index.ts +30 -0
- package/src/markdown/parser.ts +145 -0
- package/src/markdown/serializer.ts +71 -0
- package/src/test/agent-instructions.test.ts +62 -0
- package/src/test/board.test.ts +291 -0
- package/src/test/build.test.ts +28 -0
- package/src/test/checklist.test.ts +273 -0
- package/src/test/cli.test.ts +1300 -0
- package/src/test/code-path.test.ts +204 -0
- package/src/test/core.test.ts +330 -0
- package/src/test/filesystem.test.ts +435 -0
- package/src/test/git.test.ts +26 -0
- package/src/test/heading.test.ts +102 -0
- package/src/test/line-wrapping.test.ts +252 -0
- package/src/test/local-install.test.ts +34 -0
- package/src/test/markdown.test.ts +526 -0
- package/src/test/parallel-loading.test.ts +160 -0
- package/src/test/parent-id-normalization.test.ts +48 -0
- package/src/test/remote-id-conflict.test.ts +60 -0
- package/src/test/status-icon.test.ts +93 -0
- package/src/types/blessed.d.ts +14 -0
- package/src/types/index.ts +55 -0
- package/src/types/raw.d.ts +4 -0
- package/src/ui/board.ts +322 -0
- package/src/ui/checklist.ts +103 -0
- package/src/ui/code-path.ts +113 -0
- package/src/ui/heading.ts +121 -0
- package/src/ui/loading.ts +216 -0
- package/src/ui/status-icon.ts +53 -0
- package/src/ui/task-list.ts +168 -0
- package/src/ui/task-viewer.ts +640 -0
- package/src/ui/tui.ts +301 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { FileSystem } from "../file-system/operations.ts";
|
|
5
|
+
import type { BacklogConfig, DecisionLog, Document, Task } from "../types/index.ts";
|
|
6
|
+
|
|
7
|
+
const TEST_DIR = join(process.cwd(), "test-backlog");
|
|
8
|
+
|
|
9
|
+
describe("FileSystem", () => {
|
|
10
|
+
let filesystem: FileSystem;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
filesystem = new FileSystem(TEST_DIR);
|
|
14
|
+
await filesystem.ensureBacklogStructure();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
try {
|
|
19
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore cleanup errors
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("ensureBacklogStructure", () => {
|
|
26
|
+
it("should create all required directories", async () => {
|
|
27
|
+
const expectedDirs = [
|
|
28
|
+
join(TEST_DIR, ".backlog"),
|
|
29
|
+
join(TEST_DIR, ".backlog", "tasks"),
|
|
30
|
+
join(TEST_DIR, ".backlog", "drafts"),
|
|
31
|
+
join(TEST_DIR, ".backlog", "archive", "tasks"),
|
|
32
|
+
join(TEST_DIR, ".backlog", "archive", "drafts"),
|
|
33
|
+
join(TEST_DIR, ".backlog", "docs"),
|
|
34
|
+
join(TEST_DIR, ".backlog", "decisions"),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const dir of expectedDirs) {
|
|
38
|
+
const stats = await stat(dir);
|
|
39
|
+
expect(stats.isDirectory()).toBe(true);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("task operations", () => {
|
|
45
|
+
const sampleTask: Task = {
|
|
46
|
+
id: "task-1",
|
|
47
|
+
title: "Test Task",
|
|
48
|
+
status: "To Do",
|
|
49
|
+
assignee: ["@developer"],
|
|
50
|
+
reporter: "@manager",
|
|
51
|
+
createdDate: "2025-06-03",
|
|
52
|
+
labels: ["test"],
|
|
53
|
+
milestone: "v1.0",
|
|
54
|
+
dependencies: [],
|
|
55
|
+
description: "This is a test task",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it("should save and load a task", async () => {
|
|
59
|
+
await filesystem.saveTask(sampleTask);
|
|
60
|
+
|
|
61
|
+
const loadedTask = await filesystem.loadTask("task-1");
|
|
62
|
+
expect(loadedTask?.id).toBe(sampleTask.id);
|
|
63
|
+
expect(loadedTask?.title).toBe(sampleTask.title);
|
|
64
|
+
expect(loadedTask?.status).toBe(sampleTask.status);
|
|
65
|
+
expect(loadedTask?.description).toBe(sampleTask.description);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return null for non-existent task", async () => {
|
|
69
|
+
const task = await filesystem.loadTask("non-existent");
|
|
70
|
+
expect(task).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should list all tasks", async () => {
|
|
74
|
+
await filesystem.saveTask(sampleTask);
|
|
75
|
+
await filesystem.saveTask({
|
|
76
|
+
...sampleTask,
|
|
77
|
+
id: "task-2",
|
|
78
|
+
title: "Second Task",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const tasks = await filesystem.listTasks();
|
|
82
|
+
expect(tasks).toHaveLength(2);
|
|
83
|
+
expect(tasks.map((t) => t.id)).toEqual(["task-1", "task-2"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should archive a task", async () => {
|
|
87
|
+
await filesystem.saveTask(sampleTask);
|
|
88
|
+
|
|
89
|
+
const archived = await filesystem.archiveTask("task-1");
|
|
90
|
+
expect(archived).toBe(true);
|
|
91
|
+
|
|
92
|
+
const task = await filesystem.loadTask("task-1");
|
|
93
|
+
expect(task).toBeNull();
|
|
94
|
+
|
|
95
|
+
// Check that file exists in archive
|
|
96
|
+
const archiveFiles = await readdir(join(TEST_DIR, ".backlog", "archive", "tasks"));
|
|
97
|
+
expect(archiveFiles.some((f) => f.startsWith("task-1"))).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should demote a task to drafts", async () => {
|
|
101
|
+
await filesystem.saveTask(sampleTask);
|
|
102
|
+
|
|
103
|
+
const demoted = await filesystem.demoteTask("task-1");
|
|
104
|
+
expect(demoted).toBe(true);
|
|
105
|
+
|
|
106
|
+
const draft = await filesystem.loadDraft("task-1");
|
|
107
|
+
expect(draft?.id).toBe("task-1");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("draft operations", () => {
|
|
112
|
+
const sampleDraft: Task = {
|
|
113
|
+
id: "task-draft",
|
|
114
|
+
title: "Draft Task",
|
|
115
|
+
status: "Draft",
|
|
116
|
+
assignee: [],
|
|
117
|
+
createdDate: "2025-06-07",
|
|
118
|
+
labels: [],
|
|
119
|
+
dependencies: [],
|
|
120
|
+
description: "Draft description",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
it("should save and load a draft", async () => {
|
|
124
|
+
await filesystem.saveDraft(sampleDraft);
|
|
125
|
+
|
|
126
|
+
const loaded = await filesystem.loadDraft("task-draft");
|
|
127
|
+
expect(loaded?.id).toBe(sampleDraft.id);
|
|
128
|
+
expect(loaded?.title).toBe(sampleDraft.title);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should list all drafts", async () => {
|
|
132
|
+
await filesystem.saveDraft(sampleDraft);
|
|
133
|
+
await filesystem.saveDraft({ ...sampleDraft, id: "task-draft2", title: "Second" });
|
|
134
|
+
|
|
135
|
+
const drafts = await filesystem.listDrafts();
|
|
136
|
+
expect(drafts.map((d) => d.id)).toEqual(["task-draft", "task-draft2"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should promote a draft to tasks", async () => {
|
|
140
|
+
await filesystem.saveDraft(sampleDraft);
|
|
141
|
+
|
|
142
|
+
const promoted = await filesystem.promoteDraft("task-draft");
|
|
143
|
+
expect(promoted).toBe(true);
|
|
144
|
+
|
|
145
|
+
const task = await filesystem.loadTask("task-draft");
|
|
146
|
+
expect(task?.id).toBe("task-draft");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should archive a draft", async () => {
|
|
150
|
+
await filesystem.saveDraft(sampleDraft);
|
|
151
|
+
|
|
152
|
+
const archived = await filesystem.archiveDraft("task-draft");
|
|
153
|
+
expect(archived).toBe(true);
|
|
154
|
+
|
|
155
|
+
const draft = await filesystem.loadDraft("task-draft");
|
|
156
|
+
expect(draft).toBeNull();
|
|
157
|
+
|
|
158
|
+
const files = await readdir(join(TEST_DIR, ".backlog", "archive", "drafts"));
|
|
159
|
+
expect(files.some((f) => f.startsWith("task-draft"))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("config operations", () => {
|
|
164
|
+
const sampleConfig: BacklogConfig = {
|
|
165
|
+
projectName: "Test Project",
|
|
166
|
+
defaultAssignee: "@admin",
|
|
167
|
+
defaultStatus: "To Do",
|
|
168
|
+
defaultReporter: undefined,
|
|
169
|
+
statuses: ["To Do", "In Progress", "Done"],
|
|
170
|
+
labels: ["bug", "feature"],
|
|
171
|
+
milestones: ["v1.0", "v2.0"],
|
|
172
|
+
dateFormat: "yyyy-mm-dd",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
it("should save and load config", async () => {
|
|
176
|
+
await filesystem.saveConfig(sampleConfig);
|
|
177
|
+
|
|
178
|
+
const loadedConfig = await filesystem.loadConfig();
|
|
179
|
+
expect(loadedConfig).toEqual(sampleConfig);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should return null for missing config", async () => {
|
|
183
|
+
// Create a fresh filesystem without any config
|
|
184
|
+
const freshFilesystem = new FileSystem(join(TEST_DIR, "fresh"));
|
|
185
|
+
await freshFilesystem.ensureBacklogStructure();
|
|
186
|
+
|
|
187
|
+
const config = await freshFilesystem.loadConfig();
|
|
188
|
+
expect(config).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should handle defaultReporter field", async () => {
|
|
192
|
+
const cfg: BacklogConfig = {
|
|
193
|
+
projectName: "Reporter",
|
|
194
|
+
defaultReporter: "@author",
|
|
195
|
+
statuses: ["To Do"],
|
|
196
|
+
labels: [],
|
|
197
|
+
milestones: [],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await filesystem.saveConfig(cfg);
|
|
201
|
+
const loaded = await filesystem.loadConfig();
|
|
202
|
+
expect(loaded?.defaultReporter).toBe("@author");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("user config operations", () => {
|
|
207
|
+
it("should save and load local and global user settings", async () => {
|
|
208
|
+
await filesystem.setUserSetting("reporter", "local", false);
|
|
209
|
+
await filesystem.setUserSetting("reporter", "global", true);
|
|
210
|
+
|
|
211
|
+
const local = await filesystem.getUserSetting("reporter", false);
|
|
212
|
+
const global = await filesystem.getUserSetting("reporter", true);
|
|
213
|
+
|
|
214
|
+
expect(local).toBe("local");
|
|
215
|
+
expect(global).toBe("global");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("directory accessors", () => {
|
|
220
|
+
it("should provide correct directory paths", () => {
|
|
221
|
+
expect(filesystem.tasksDir).toBe(join(TEST_DIR, ".backlog", "tasks"));
|
|
222
|
+
expect(filesystem.archiveTasksDir).toBe(join(TEST_DIR, ".backlog", "archive", "tasks"));
|
|
223
|
+
expect(filesystem.decisionsDir).toBe(join(TEST_DIR, ".backlog", "decisions"));
|
|
224
|
+
expect(filesystem.docsDir).toBe(join(TEST_DIR, ".backlog", "docs"));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("decision log operations", () => {
|
|
229
|
+
const sampleDecision: DecisionLog = {
|
|
230
|
+
id: "decision-1",
|
|
231
|
+
title: "Use TypeScript",
|
|
232
|
+
date: "2025-06-07",
|
|
233
|
+
status: "accepted",
|
|
234
|
+
context: "Need type safety",
|
|
235
|
+
decision: "Use TypeScript",
|
|
236
|
+
consequences: "Better DX",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
it("should save and load a decision log", async () => {
|
|
240
|
+
await filesystem.saveDecisionLog(sampleDecision);
|
|
241
|
+
|
|
242
|
+
const loadedDecision = await filesystem.loadDecisionLog("decision-1");
|
|
243
|
+
expect(loadedDecision?.id).toBe(sampleDecision.id);
|
|
244
|
+
expect(loadedDecision?.title).toBe(sampleDecision.title);
|
|
245
|
+
expect(loadedDecision?.status).toBe(sampleDecision.status);
|
|
246
|
+
expect(loadedDecision?.context).toBe(sampleDecision.context);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should return null for non-existent decision log", async () => {
|
|
250
|
+
const decision = await filesystem.loadDecisionLog("non-existent");
|
|
251
|
+
expect(decision).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should save decision log with alternatives", async () => {
|
|
255
|
+
const decisionWithAlternatives: DecisionLog = {
|
|
256
|
+
...sampleDecision,
|
|
257
|
+
id: "decision-2",
|
|
258
|
+
alternatives: "Considered JavaScript",
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
await filesystem.saveDecisionLog(decisionWithAlternatives);
|
|
262
|
+
const loaded = await filesystem.loadDecisionLog("decision-2");
|
|
263
|
+
|
|
264
|
+
expect(loaded?.alternatives).toBe("Considered JavaScript");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should list decision logs", async () => {
|
|
268
|
+
await filesystem.saveDecisionLog(sampleDecision);
|
|
269
|
+
const list = await filesystem.listDecisionLogs();
|
|
270
|
+
expect(list).toHaveLength(1);
|
|
271
|
+
expect(list[0].id).toBe(sampleDecision.id);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("document operations", () => {
|
|
276
|
+
const sampleDocument: Document = {
|
|
277
|
+
id: "doc-1",
|
|
278
|
+
title: "API Guide",
|
|
279
|
+
type: "guide",
|
|
280
|
+
createdDate: "2025-06-07",
|
|
281
|
+
updatedDate: "2025-06-08",
|
|
282
|
+
content: "This is the API guide content.",
|
|
283
|
+
tags: ["api", "guide"],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
it("should save a document", async () => {
|
|
287
|
+
await filesystem.saveDocument(sampleDocument);
|
|
288
|
+
|
|
289
|
+
// Check that file was created
|
|
290
|
+
const docsFiles = await readdir(filesystem.docsDir);
|
|
291
|
+
expect(docsFiles.some((f) => f.includes("api-guide"))).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should save document without optional fields", async () => {
|
|
295
|
+
const minimalDoc: Document = {
|
|
296
|
+
id: "doc-2",
|
|
297
|
+
title: "Simple Doc",
|
|
298
|
+
type: "readme",
|
|
299
|
+
createdDate: "2025-06-07",
|
|
300
|
+
content: "Simple content.",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await filesystem.saveDocument(minimalDoc);
|
|
304
|
+
|
|
305
|
+
const docsFiles = await readdir(filesystem.docsDir);
|
|
306
|
+
expect(docsFiles.some((f) => f.includes("simple-doc"))).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should list documents", async () => {
|
|
310
|
+
await filesystem.saveDocument(sampleDocument);
|
|
311
|
+
const list = await filesystem.listDocuments();
|
|
312
|
+
expect(list.some((d) => d.id === sampleDocument.id)).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("edge cases", () => {
|
|
317
|
+
it("should handle task with task- prefix in id", async () => {
|
|
318
|
+
const taskWithPrefix: Task = {
|
|
319
|
+
id: "task-prefixed",
|
|
320
|
+
title: "Already Prefixed",
|
|
321
|
+
status: "To Do",
|
|
322
|
+
assignee: [],
|
|
323
|
+
createdDate: "2025-06-07",
|
|
324
|
+
labels: [],
|
|
325
|
+
dependencies: [],
|
|
326
|
+
description: "Task with task- prefix",
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
await filesystem.saveTask(taskWithPrefix);
|
|
330
|
+
const loaded = await filesystem.loadTask("task-prefixed");
|
|
331
|
+
|
|
332
|
+
expect(loaded?.id).toBe("task-prefixed");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should handle task without task- prefix in id", async () => {
|
|
336
|
+
const taskWithoutPrefix: Task = {
|
|
337
|
+
id: "no-prefix",
|
|
338
|
+
title: "No Prefix",
|
|
339
|
+
status: "To Do",
|
|
340
|
+
assignee: [],
|
|
341
|
+
createdDate: "2025-06-07",
|
|
342
|
+
labels: [],
|
|
343
|
+
dependencies: [],
|
|
344
|
+
description: "Task without prefix",
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
await filesystem.saveTask(taskWithoutPrefix);
|
|
348
|
+
const loaded = await filesystem.loadTask("no-prefix");
|
|
349
|
+
|
|
350
|
+
expect(loaded?.id).toBe("no-prefix");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should return empty array when listing tasks in empty directory", async () => {
|
|
354
|
+
const tasks = await filesystem.listTasks();
|
|
355
|
+
expect(tasks).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should return false when archiving non-existent task", async () => {
|
|
359
|
+
const result = await filesystem.archiveTask("non-existent");
|
|
360
|
+
expect(result).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should handle config with all optional fields", async () => {
|
|
364
|
+
const fullConfig: BacklogConfig = {
|
|
365
|
+
projectName: "Full Project",
|
|
366
|
+
defaultAssignee: "@admin",
|
|
367
|
+
defaultStatus: "To Do",
|
|
368
|
+
defaultReporter: undefined,
|
|
369
|
+
statuses: ["To Do", "In Progress", "Done"],
|
|
370
|
+
labels: ["bug", "feature", "enhancement"],
|
|
371
|
+
milestones: ["v1.0", "v1.1", "v2.0"],
|
|
372
|
+
dateFormat: "yyyy-mm-dd",
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
await filesystem.saveConfig(fullConfig);
|
|
376
|
+
const loaded = await filesystem.loadConfig();
|
|
377
|
+
|
|
378
|
+
expect(loaded).toEqual(fullConfig);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("should handle config with minimal fields", async () => {
|
|
382
|
+
const minimalConfig: BacklogConfig = {
|
|
383
|
+
projectName: "Minimal Project",
|
|
384
|
+
statuses: ["To Do", "Done"],
|
|
385
|
+
labels: [],
|
|
386
|
+
milestones: [],
|
|
387
|
+
dateFormat: "yyyy-mm-dd",
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await filesystem.saveConfig(minimalConfig);
|
|
391
|
+
const loaded = await filesystem.loadConfig();
|
|
392
|
+
|
|
393
|
+
expect(loaded?.projectName).toBe("Minimal Project");
|
|
394
|
+
expect(loaded?.defaultAssignee).toBeUndefined();
|
|
395
|
+
expect(loaded?.defaultStatus).toBeUndefined();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should sanitize filenames correctly", async () => {
|
|
399
|
+
const taskWithSpecialChars: Task = {
|
|
400
|
+
id: "task-special",
|
|
401
|
+
title: "Task/with\\special:chars?",
|
|
402
|
+
status: "To Do",
|
|
403
|
+
assignee: [],
|
|
404
|
+
createdDate: "2025-06-07",
|
|
405
|
+
labels: [],
|
|
406
|
+
dependencies: [],
|
|
407
|
+
description: "Task with special characters in title",
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
await filesystem.saveTask(taskWithSpecialChars);
|
|
411
|
+
const loaded = await filesystem.loadTask("task-special");
|
|
412
|
+
|
|
413
|
+
expect(loaded?.title).toBe("Task/with\\special:chars?");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should avoid double dashes in filenames", async () => {
|
|
417
|
+
const weirdTask: Task = {
|
|
418
|
+
id: "task-dashes",
|
|
419
|
+
title: "Task -- with -- multiple dashes",
|
|
420
|
+
status: "To Do",
|
|
421
|
+
assignee: [],
|
|
422
|
+
createdDate: "2025-06-07",
|
|
423
|
+
labels: [],
|
|
424
|
+
dependencies: [],
|
|
425
|
+
description: "Check double dashes",
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
await filesystem.saveTask(weirdTask);
|
|
429
|
+
const files = await readdir(filesystem.tasksDir);
|
|
430
|
+
const filename = files.find((f) => f.startsWith("task-dashes -"));
|
|
431
|
+
expect(filename).toBeDefined();
|
|
432
|
+
expect(filename?.includes("--")).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { GitOperations, isGitRepository } from "../git/operations.ts";
|
|
3
|
+
|
|
4
|
+
describe("Git Operations", () => {
|
|
5
|
+
describe("isGitRepository", () => {
|
|
6
|
+
it("should return true for current directory (which is a git repo)", async () => {
|
|
7
|
+
const result = await isGitRepository(process.cwd());
|
|
8
|
+
expect(result).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return false for /tmp directory", async () => {
|
|
12
|
+
const result = await isGitRepository("/tmp");
|
|
13
|
+
expect(result).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("GitOperations instantiation", () => {
|
|
18
|
+
it("should create GitOperations instance", () => {
|
|
19
|
+
const git = new GitOperations(process.cwd());
|
|
20
|
+
expect(git).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Note: Skipping integration tests that require git repository setup
|
|
25
|
+
// These tests can be enabled for local development but may timeout in CI
|
|
26
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type HeadingLevel, formatHeading, getHeadingStyle } from "../ui/heading.ts";
|
|
3
|
+
|
|
4
|
+
describe("Heading component", () => {
|
|
5
|
+
describe("getHeadingStyle", () => {
|
|
6
|
+
test("should return correct style for level 1", () => {
|
|
7
|
+
const style = getHeadingStyle(1);
|
|
8
|
+
expect(style.color).toBe("bright-white");
|
|
9
|
+
expect(style.bold).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("should return correct style for level 2", () => {
|
|
13
|
+
const style = getHeadingStyle(2);
|
|
14
|
+
expect(style.color).toBe("cyan");
|
|
15
|
+
expect(style.bold).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("should return correct style for level 3", () => {
|
|
19
|
+
const style = getHeadingStyle(3);
|
|
20
|
+
expect(style.color).toBe("white");
|
|
21
|
+
expect(style.bold).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("formatHeading", () => {
|
|
26
|
+
test("should format level 1 heading with bold and bright-white", () => {
|
|
27
|
+
const formatted = formatHeading("Main Title", 1);
|
|
28
|
+
expect(formatted).toBe("{bold}{brightwhite-fg}Main Title{/brightwhite-fg}{/bold}");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should format level 2 heading with cyan", () => {
|
|
32
|
+
const formatted = formatHeading("Section Title", 2);
|
|
33
|
+
expect(formatted).toBe("{cyan-fg}Section Title{/cyan-fg}");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should format level 3 heading with white", () => {
|
|
37
|
+
const formatted = formatHeading("Subsection Title", 3);
|
|
38
|
+
expect(formatted).toBe("{white-fg}Subsection Title{/white-fg}");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("should handle empty text", () => {
|
|
42
|
+
const formatted = formatHeading("", 1);
|
|
43
|
+
expect(formatted).toBe("{bold}{brightwhite-fg}{/brightwhite-fg}{/bold}");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should handle special characters", () => {
|
|
47
|
+
const formatted = formatHeading("Title with @#$%", 2);
|
|
48
|
+
expect(formatted).toBe("{cyan-fg}Title with @#$%{/cyan-fg}");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("heading levels", () => {
|
|
53
|
+
test("should accept valid heading levels", () => {
|
|
54
|
+
const levels: HeadingLevel[] = [1, 2, 3];
|
|
55
|
+
|
|
56
|
+
for (const level of levels) {
|
|
57
|
+
const style = getHeadingStyle(level);
|
|
58
|
+
expect(style).toBeDefined();
|
|
59
|
+
expect(typeof style.color).toBe("string");
|
|
60
|
+
expect(typeof style.bold).toBe("boolean");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("should have distinct styles for each level", () => {
|
|
65
|
+
const style1 = getHeadingStyle(1);
|
|
66
|
+
const style2 = getHeadingStyle(2);
|
|
67
|
+
const style3 = getHeadingStyle(3);
|
|
68
|
+
|
|
69
|
+
// Level 1 should be the only bold one
|
|
70
|
+
expect(style1.bold).toBe(true);
|
|
71
|
+
expect(style2.bold).toBe(false);
|
|
72
|
+
expect(style3.bold).toBe(false);
|
|
73
|
+
|
|
74
|
+
// Each level should have different colors
|
|
75
|
+
expect(style1.color).not.toBe(style2.color);
|
|
76
|
+
expect(style2.color).not.toBe(style3.color);
|
|
77
|
+
expect(style1.color).not.toBe(style3.color);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("blessed tag formatting", () => {
|
|
82
|
+
test("should produce valid blessed tags", () => {
|
|
83
|
+
const level1 = formatHeading("Test", 1);
|
|
84
|
+
const level2 = formatHeading("Test", 2);
|
|
85
|
+
const level3 = formatHeading("Test", 3);
|
|
86
|
+
|
|
87
|
+
// Should contain valid blessed tag syntax
|
|
88
|
+
expect(level1).toMatch(/^\{.*\}.*\{\/.*\}$/);
|
|
89
|
+
expect(level2).toMatch(/^\{.*\}.*\{\/.*\}$/);
|
|
90
|
+
expect(level3).toMatch(/^\{.*\}.*\{\/.*\}$/);
|
|
91
|
+
|
|
92
|
+
// Level 1 should have both bold and color tags
|
|
93
|
+
expect(level1).toContain("{bold}");
|
|
94
|
+
expect(level1).toContain("{/bold}");
|
|
95
|
+
expect(level1).toContain("-fg}");
|
|
96
|
+
|
|
97
|
+
// Level 2 and 3 should only have color tags
|
|
98
|
+
expect(level2).not.toContain("{bold}");
|
|
99
|
+
expect(level3).not.toContain("{bold}");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|