@nijaru/tk 0.0.1 → 0.0.2
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/README.md +30 -17
- package/package.json +4 -4
- package/src/cli.test.ts +265 -5
- package/src/cli.ts +227 -106
- package/src/db/storage.ts +305 -90
- package/src/lib/completions.ts +15 -8
- package/src/lib/format.ts +26 -0
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# tk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimal task tracker. Simple, fast, git-friendly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Plain JSON files in `.tasks/`
|
|
6
|
+
- No daemons, no merge conflicts, no viruses
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
10
|
+
Requires [Bun](https://bun.sh) runtime.
|
|
11
|
+
|
|
9
12
|
```bash
|
|
10
13
|
# Install Bun first (if not installed)
|
|
11
14
|
curl -fsSL https://bun.sh/install | bash
|
|
12
15
|
|
|
13
16
|
# Then install tk globally
|
|
14
17
|
bun add -g @nijaru/tk
|
|
15
|
-
|
|
16
|
-
# Or run from source
|
|
17
|
-
git clone https://github.com/nijaru/tk.git
|
|
18
|
-
cd tk && bun install
|
|
19
|
-
bun run src/cli.ts --help
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
## Quick Start
|
|
@@ -35,9 +33,9 @@ myapp-x9k2
|
|
|
35
33
|
$ tk block x9k2 a7b3 # tests blocked by auth (just use ref)
|
|
36
34
|
|
|
37
35
|
$ tk ready # what can I work on?
|
|
38
|
-
ID
|
|
36
|
+
ID | PRIO | STATUS | TITLE
|
|
39
37
|
------------------------------------------------------------
|
|
40
|
-
myapp-a7b3
|
|
38
|
+
myapp-a7b3 | p1 | open | Implement auth
|
|
41
39
|
|
|
42
40
|
$ tk start a7b3 # just the ref works everywhere
|
|
43
41
|
Started: myapp-a7b3
|
|
@@ -49,9 +47,9 @@ $ tk done a7b3
|
|
|
49
47
|
Completed: myapp-a7b3
|
|
50
48
|
|
|
51
49
|
$ tk ready # tests now unblocked
|
|
52
|
-
ID
|
|
50
|
+
ID | PRIO | STATUS | TITLE
|
|
53
51
|
------------------------------------------------------------
|
|
54
|
-
myapp-x9k2
|
|
52
|
+
myapp-x9k2 | p2 | open | Write tests
|
|
55
53
|
```
|
|
56
54
|
|
|
57
55
|
## Commands
|
|
@@ -67,11 +65,12 @@ myapp-x9k2 p2 open Write tests
|
|
|
67
65
|
| `tk done <id>` | Complete task |
|
|
68
66
|
| `tk reopen <id>` | Reopen task |
|
|
69
67
|
| `tk edit <id>` | Edit task |
|
|
70
|
-
| `tk log <id> <msg
|
|
68
|
+
| `tk log <id> "<msg>"` | Add log entry |
|
|
71
69
|
| `tk block <id> <blocker>` | Add dependency (id blocked by blocker) |
|
|
72
70
|
| `tk unblock <id> <blocker>` | Remove dependency |
|
|
73
71
|
| `tk rm` / `tk remove <id>` | Delete task |
|
|
74
|
-
| `tk clean` | Remove old done tasks (default:
|
|
72
|
+
| `tk clean` | Remove old done tasks (default: 14 days) |
|
|
73
|
+
| `tk check` | Check task integrity |
|
|
75
74
|
| `tk config` | Show/set configuration |
|
|
76
75
|
| `tk completions <shell>` | Output shell completions (bash, zsh, fish) |
|
|
77
76
|
| `tk help [command]` | Show help (or command-specific help) |
|
|
@@ -120,9 +119,9 @@ tk edit a7b3 --parent - # Clear parent
|
|
|
120
119
|
## Clean Options
|
|
121
120
|
|
|
122
121
|
```bash
|
|
123
|
-
tk clean # Remove done tasks older than
|
|
124
|
-
tk clean --older-than
|
|
125
|
-
tk clean
|
|
122
|
+
tk clean # Remove done tasks older than config (default: 14 days)
|
|
123
|
+
tk clean --older-than 30 # Custom threshold (days)
|
|
124
|
+
tk clean --force # Force clean even if disabled in config
|
|
126
125
|
```
|
|
127
126
|
|
|
128
127
|
## Config
|
|
@@ -137,6 +136,20 @@ tk config alias web src/web # Add alias
|
|
|
137
136
|
tk config alias --rm web # Remove alias
|
|
138
137
|
```
|
|
139
138
|
|
|
139
|
+
Config file (`.tasks/config.json`):
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"version": 1,
|
|
144
|
+
"project": "myapp",
|
|
145
|
+
"clean_after": 14,
|
|
146
|
+
"defaults": { "priority": 3, "labels": [], "assignees": [] },
|
|
147
|
+
"aliases": {}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `clean_after`: Days to keep done tasks (default: 14). Set to `false` to disable cleaning.
|
|
152
|
+
|
|
140
153
|
## Task IDs
|
|
141
154
|
|
|
142
155
|
Format: `project-ref` (e.g., `myapp-a7b3`, `api-x9k2`, `web-3m8p`)
|
|
@@ -226,4 +239,4 @@ tk completions fish | source
|
|
|
226
239
|
|
|
227
240
|
## License
|
|
228
241
|
|
|
229
|
-
MIT
|
|
242
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nijaru/tk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Minimal task tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tk": "
|
|
7
|
+
"tk": "src/cli.ts"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/nijaru/tk"
|
|
40
|
+
"url": "git+https://github.com/nijaru/tk.git"
|
|
41
41
|
},
|
|
42
42
|
"author": "nijaru",
|
|
43
43
|
"homepage": "https://github.com/nijaru/tk#readme",
|
package/src/cli.test.ts
CHANGED
|
@@ -298,13 +298,24 @@ describe("tk CLI", () => {
|
|
|
298
298
|
expect(task.completed_at).toBeNull();
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
-
test("start errors if
|
|
301
|
+
test("start errors if already active", async () => {
|
|
302
302
|
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
303
303
|
await run(["start", id.trim()], testDir);
|
|
304
304
|
|
|
305
305
|
const { stderr, exitCode } = await run(["start", id.trim()], testDir);
|
|
306
306
|
expect(exitCode).toBe(1);
|
|
307
|
-
expect(stderr).toContain("
|
|
307
|
+
expect(stderr).toContain("already active");
|
|
308
|
+
expect(stderr).toContain("tk done"); // suggests next action
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("start errors if already done", async () => {
|
|
312
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
313
|
+
await run(["done", id.trim()], testDir);
|
|
314
|
+
|
|
315
|
+
const { stderr, exitCode } = await run(["start", id.trim()], testDir);
|
|
316
|
+
expect(exitCode).toBe(1);
|
|
317
|
+
expect(stderr).toContain("already done");
|
|
318
|
+
expect(stderr).toContain("tk reopen"); // suggests next action
|
|
308
319
|
});
|
|
309
320
|
});
|
|
310
321
|
|
|
@@ -458,13 +469,29 @@ describe("tk CLI", () => {
|
|
|
458
469
|
const task = JSON.parse(stdout);
|
|
459
470
|
expect(task.blocked_by).not.toContain(id1.trim());
|
|
460
471
|
});
|
|
472
|
+
|
|
473
|
+
test("clears parent references on child tasks", async () => {
|
|
474
|
+
const { stdout: parentId } = await run(["add", "Parent"], testDir);
|
|
475
|
+
const { stdout: childId } = await run(["add", "Child", "--parent", parentId.trim()], testDir);
|
|
476
|
+
|
|
477
|
+
// Verify child has parent
|
|
478
|
+
const { stdout: before } = await run(["show", childId.trim(), "--json"], testDir);
|
|
479
|
+
expect(JSON.parse(before).parent).toBe(parentId.trim());
|
|
480
|
+
|
|
481
|
+
// Delete parent
|
|
482
|
+
await run(["rm", parentId.trim()], testDir);
|
|
483
|
+
|
|
484
|
+
// Child's parent should be cleared
|
|
485
|
+
const { stdout: after } = await run(["show", childId.trim(), "--json"], testDir);
|
|
486
|
+
expect(JSON.parse(after).parent).toBeNull();
|
|
487
|
+
});
|
|
461
488
|
});
|
|
462
489
|
|
|
463
490
|
describe("clean", () => {
|
|
464
491
|
test("removes completed tasks", async () => {
|
|
465
492
|
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
466
493
|
await run(["done", id.trim()], testDir);
|
|
467
|
-
await run(["clean", "--
|
|
494
|
+
await run(["clean", "--force"], testDir);
|
|
468
495
|
|
|
469
496
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
470
497
|
expect(exitCode).toBe(1);
|
|
@@ -472,11 +499,26 @@ describe("tk CLI", () => {
|
|
|
472
499
|
|
|
473
500
|
test("keeps open tasks", async () => {
|
|
474
501
|
const { stdout: id } = await run(["add", "Open task"], testDir);
|
|
475
|
-
await run(["clean", "--
|
|
502
|
+
await run(["clean", "--force"], testDir);
|
|
476
503
|
|
|
477
504
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
478
505
|
expect(exitCode).toBe(0);
|
|
479
506
|
});
|
|
507
|
+
|
|
508
|
+
test("--older-than respects age threshold", async () => {
|
|
509
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
510
|
+
await run(["done", id.trim()], testDir);
|
|
511
|
+
|
|
512
|
+
// Task was just completed, so --older-than 1 should keep it
|
|
513
|
+
await run(["clean", "--older-than", "1"], testDir);
|
|
514
|
+
const { exitCode: stillExists } = await run(["show", id.trim()], testDir);
|
|
515
|
+
expect(stillExists).toBe(0);
|
|
516
|
+
|
|
517
|
+
// --older-than 0 should remove it (0 days = remove all)
|
|
518
|
+
await run(["clean", "--older-than", "0"], testDir);
|
|
519
|
+
const { exitCode: gone } = await run(["show", id.trim()], testDir);
|
|
520
|
+
expect(gone).toBe(1);
|
|
521
|
+
});
|
|
480
522
|
});
|
|
481
523
|
|
|
482
524
|
describe("init", () => {
|
|
@@ -547,6 +589,38 @@ describe("tk CLI", () => {
|
|
|
547
589
|
expect(stdout).toContain("api");
|
|
548
590
|
expect(stdout).toContain("packages/api");
|
|
549
591
|
});
|
|
592
|
+
|
|
593
|
+
test("renames project and updates references", async () => {
|
|
594
|
+
await run(["init", "-P", "old"], testDir);
|
|
595
|
+
|
|
596
|
+
// Create tasks with references
|
|
597
|
+
const { stdout: id1 } = await run(["add", "Task 1"], testDir);
|
|
598
|
+
const { stdout: id2 } = await run(["add", "Task 2"], testDir);
|
|
599
|
+
await run(["block", id2.trim(), id1.trim()], testDir);
|
|
600
|
+
|
|
601
|
+
// Rename project
|
|
602
|
+
const { stdout, exitCode } = await run(
|
|
603
|
+
["config", "project", "new", "--rename", "old"],
|
|
604
|
+
testDir,
|
|
605
|
+
);
|
|
606
|
+
expect(exitCode).toBe(0);
|
|
607
|
+
expect(stdout).toContain("Renamed 2 tasks");
|
|
608
|
+
expect(stdout).toContain("old-*");
|
|
609
|
+
expect(stdout).toContain("new-*");
|
|
610
|
+
|
|
611
|
+
// Verify new IDs work
|
|
612
|
+
const newId1 = id1.trim().replace("old-", "new-");
|
|
613
|
+
const { exitCode: showCode } = await run(["show", newId1], testDir);
|
|
614
|
+
expect(showCode).toBe(0);
|
|
615
|
+
|
|
616
|
+
// Verify references updated
|
|
617
|
+
const { stdout: showOut } = await run(
|
|
618
|
+
["show", id2.trim().replace("old-", "new-"), "--json"],
|
|
619
|
+
testDir,
|
|
620
|
+
);
|
|
621
|
+
const task = JSON.parse(showOut);
|
|
622
|
+
expect(task.blocked_by[0]).toBe(newId1);
|
|
623
|
+
});
|
|
550
624
|
});
|
|
551
625
|
|
|
552
626
|
describe("error handling", () => {
|
|
@@ -586,7 +660,7 @@ describe("tk CLI", () => {
|
|
|
586
660
|
test("rejects invalid ID format", async () => {
|
|
587
661
|
const { stderr, exitCode } = await run(["show", "invalid"], testDir);
|
|
588
662
|
expect(exitCode).toBe(1);
|
|
589
|
-
expect(stderr).toContain("
|
|
663
|
+
expect(stderr).toContain("not found");
|
|
590
664
|
});
|
|
591
665
|
});
|
|
592
666
|
|
|
@@ -615,6 +689,22 @@ describe("tk CLI", () => {
|
|
|
615
689
|
const tasks = JSON.parse(stdout);
|
|
616
690
|
expect(Array.isArray(tasks)).toBe(true);
|
|
617
691
|
});
|
|
692
|
+
|
|
693
|
+
test("log requires quoted message", async () => {
|
|
694
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
695
|
+
const taskId = id.trim();
|
|
696
|
+
|
|
697
|
+
// Unquoted multiple words should error
|
|
698
|
+
const { stderr, exitCode } = await run(["log", taskId, "word1", "word2"], testDir);
|
|
699
|
+
expect(exitCode).toBe(1);
|
|
700
|
+
expect(stderr).toContain("must be quoted");
|
|
701
|
+
|
|
702
|
+
// Quoted message works (shell passes as single arg)
|
|
703
|
+
await run(["log", taskId, "Quoted message works"], testDir);
|
|
704
|
+
const { stdout } = await run(["show", "--json", taskId], testDir);
|
|
705
|
+
const task = JSON.parse(stdout);
|
|
706
|
+
expect(task.logs[0].msg).toBe("Quoted message works");
|
|
707
|
+
});
|
|
618
708
|
});
|
|
619
709
|
|
|
620
710
|
describe("ID resolution", () => {
|
|
@@ -632,5 +722,175 @@ describe("tk CLI", () => {
|
|
|
632
722
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
633
723
|
expect(exitCode).toBe(0);
|
|
634
724
|
});
|
|
725
|
+
|
|
726
|
+
test("ambiguous ID shows matching tasks", async () => {
|
|
727
|
+
// Create tasks with same ref prefix in different projects
|
|
728
|
+
const { stdout: id1 } = await run(["add", "Task 1", "-P", "api"], testDir);
|
|
729
|
+
const { stdout: id2 } = await run(["add", "Task 2", "-P", "web"], testDir);
|
|
730
|
+
const ref1 = id1.trim().split("-")[1] ?? "";
|
|
731
|
+
const ref2 = id2.trim().split("-")[1] ?? "";
|
|
732
|
+
|
|
733
|
+
// Use first char which might match both
|
|
734
|
+
const prefix = ref1[0] ?? "";
|
|
735
|
+
|
|
736
|
+
// Only test if both refs start with same char (otherwise not ambiguous)
|
|
737
|
+
if (ref2.startsWith(prefix)) {
|
|
738
|
+
const { stderr, exitCode } = await run(["show", prefix], testDir);
|
|
739
|
+
expect(exitCode).toBe(1);
|
|
740
|
+
expect(stderr).toContain("Ambiguous");
|
|
741
|
+
expect(stderr).toContain("matches");
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("not found shows clear error", async () => {
|
|
746
|
+
const { stderr, exitCode } = await run(["show", "zzzz"], testDir);
|
|
747
|
+
expect(exitCode).toBe(1);
|
|
748
|
+
expect(stderr).toContain("not found");
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe("tk check", () => {
|
|
753
|
+
test("reports all OK when no issues", async () => {
|
|
754
|
+
await run(["add", "Task 1"], testDir);
|
|
755
|
+
await run(["add", "Task 2"], testDir);
|
|
756
|
+
|
|
757
|
+
const { stdout, exitCode } = await run(["check"], testDir);
|
|
758
|
+
expect(exitCode).toBe(0);
|
|
759
|
+
expect(stdout).toContain("2 tasks OK");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("auto-fixes orphaned blocker reference", async () => {
|
|
763
|
+
// Create two tasks and block one by the other
|
|
764
|
+
const { stdout: id1 } = await run(["add", "Blocker"], testDir);
|
|
765
|
+
const { stdout: id2 } = await run(["add", "Blocked"], testDir);
|
|
766
|
+
const blockerId = id1.trim();
|
|
767
|
+
const blockedId = id2.trim();
|
|
768
|
+
|
|
769
|
+
await run(["block", blockedId, blockerId], testDir);
|
|
770
|
+
|
|
771
|
+
// Manually delete blocker file to simulate merge
|
|
772
|
+
const fs = await import("fs");
|
|
773
|
+
const path = await import("path");
|
|
774
|
+
const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
|
|
775
|
+
fs.unlinkSync(blockerFile);
|
|
776
|
+
|
|
777
|
+
// Check should fix it
|
|
778
|
+
const { stdout } = await run(["check"], testDir);
|
|
779
|
+
expect(stdout).toContain("cleaned");
|
|
780
|
+
expect(stdout).toContain("orphaned");
|
|
781
|
+
|
|
782
|
+
// Verify the blocked_by is now empty
|
|
783
|
+
const { stdout: showOut } = await run(["show", "--json", blockedId], testDir);
|
|
784
|
+
const task = JSON.parse(showOut);
|
|
785
|
+
expect(task.blocked_by).toEqual([]);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("auto-fixes orphaned parent reference", async () => {
|
|
789
|
+
// Create parent and child
|
|
790
|
+
const { stdout: parentId } = await run(["add", "Parent"], testDir);
|
|
791
|
+
const parent = parentId.trim();
|
|
792
|
+
const { stdout: childId } = await run(["add", "Child", "--parent", parent], testDir);
|
|
793
|
+
const child = childId.trim();
|
|
794
|
+
|
|
795
|
+
// Manually delete parent file
|
|
796
|
+
const fs = await import("fs");
|
|
797
|
+
const path = await import("path");
|
|
798
|
+
const parentFile = path.join(testDir, ".tasks", `${parent}.json`);
|
|
799
|
+
fs.unlinkSync(parentFile);
|
|
800
|
+
|
|
801
|
+
// Check should fix it
|
|
802
|
+
const { stdout } = await run(["check"], testDir);
|
|
803
|
+
expect(stdout).toContain("cleaned");
|
|
804
|
+
expect(stdout).toContain("orphaned parent");
|
|
805
|
+
|
|
806
|
+
// Verify parent is now null
|
|
807
|
+
const { stdout: showOut } = await run(["show", "--json", child], testDir);
|
|
808
|
+
const task = JSON.parse(showOut);
|
|
809
|
+
expect(task.parent).toBeNull();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("auto-fix happens on show command", async () => {
|
|
813
|
+
// Create two tasks and block one
|
|
814
|
+
const { stdout: id1 } = await run(["add", "Blocker"], testDir);
|
|
815
|
+
const { stdout: id2 } = await run(["add", "Blocked"], testDir);
|
|
816
|
+
const blockerId = id1.trim();
|
|
817
|
+
const blockedId = id2.trim();
|
|
818
|
+
|
|
819
|
+
await run(["block", blockedId, blockerId], testDir);
|
|
820
|
+
|
|
821
|
+
// Manually delete blocker file
|
|
822
|
+
const fs = await import("fs");
|
|
823
|
+
const path = await import("path");
|
|
824
|
+
const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
|
|
825
|
+
fs.unlinkSync(blockerFile);
|
|
826
|
+
|
|
827
|
+
// Show should auto-fix and output cleanup message
|
|
828
|
+
const { stderr } = await run(["show", blockedId], testDir);
|
|
829
|
+
expect(stderr).toContain("cleaned");
|
|
830
|
+
expect(stderr).toContain("orphaned");
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("reports unfixable corrupted JSON", async () => {
|
|
834
|
+
await run(["add", "Good task"], testDir);
|
|
835
|
+
|
|
836
|
+
// Create a corrupted JSON file
|
|
837
|
+
const fs = await import("fs");
|
|
838
|
+
const path = await import("path");
|
|
839
|
+
const corruptFile = path.join(testDir, ".tasks", "test-bad1.json");
|
|
840
|
+
fs.writeFileSync(corruptFile, "{ invalid json");
|
|
841
|
+
|
|
842
|
+
const { stdout } = await run(["check"], testDir);
|
|
843
|
+
expect(stdout).toContain("Unfixable");
|
|
844
|
+
expect(stdout).toContain("test-bad1.json");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("reports unfixable invalid task structure", async () => {
|
|
848
|
+
await run(["add", "Good task"], testDir);
|
|
849
|
+
|
|
850
|
+
// Create valid JSON but invalid task structure
|
|
851
|
+
const fs = await import("fs");
|
|
852
|
+
const path = await import("path");
|
|
853
|
+
const badFile = path.join(testDir, ".tasks", "test-bad2.json");
|
|
854
|
+
fs.writeFileSync(badFile, '{"foo": "bar"}');
|
|
855
|
+
|
|
856
|
+
const { stdout } = await run(["check"], testDir);
|
|
857
|
+
expect(stdout).toContain("Unfixable");
|
|
858
|
+
expect(stdout).toContain("test-bad2.json");
|
|
859
|
+
expect(stdout).toContain("Invalid task structure");
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test("auto-fixes ID mismatch (filename vs content)", async () => {
|
|
863
|
+
// Create a task
|
|
864
|
+
const { stdout: id } = await run(["add", "Test task", "-P", "api"], testDir);
|
|
865
|
+
const taskId = id.trim();
|
|
866
|
+
|
|
867
|
+
// Manually rename the file to create a mismatch
|
|
868
|
+
const fs = await import("fs");
|
|
869
|
+
const path = await import("path");
|
|
870
|
+
const oldPath = path.join(testDir, ".tasks", `${taskId}.json`);
|
|
871
|
+
const newPath = path.join(testDir, ".tasks", "web-x1y2.json");
|
|
872
|
+
fs.renameSync(oldPath, newPath);
|
|
873
|
+
|
|
874
|
+
// Check should fix it
|
|
875
|
+
const { stdout } = await run(["check"], testDir);
|
|
876
|
+
expect(stdout).toContain("cleaned");
|
|
877
|
+
expect(stdout).toContain("ID mismatch");
|
|
878
|
+
|
|
879
|
+
// Verify the content was updated to match filename
|
|
880
|
+
const { stdout: showOut } = await run(["show", "--json", "web-x1y2"], testDir);
|
|
881
|
+
const task = JSON.parse(showOut);
|
|
882
|
+
expect(task.project).toBe("web");
|
|
883
|
+
expect(task.ref).toBe("x1y2");
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("check --json returns structured output", async () => {
|
|
887
|
+
await run(["add", "Task"], testDir);
|
|
888
|
+
|
|
889
|
+
const { stdout } = await run(["check", "--json"], testDir);
|
|
890
|
+
const result = JSON.parse(stdout);
|
|
891
|
+
expect(result).toHaveProperty("totalTasks");
|
|
892
|
+
expect(result).toHaveProperty("cleaned");
|
|
893
|
+
expect(result).toHaveProperty("unfixable");
|
|
894
|
+
});
|
|
635
895
|
});
|
|
636
896
|
});
|