@nijaru/tk 0.0.5 → 0.1.1

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/src/lib/help.ts DELETED
@@ -1,266 +0,0 @@
1
- import { version } from "../../package.json";
2
-
3
- export const MAIN_HELP = `tk v${version} - Task tracker for AI agents
4
-
5
- USAGE:
6
- tk <command> [options]
7
-
8
- COMMANDS:
9
- init Initialize .tasks/ directory
10
- add Create task
11
- ls, list List tasks
12
- ready List ready tasks (active/open + unblocked)
13
- show Show task details
14
- start Start working (open -> active)
15
- done Complete task
16
- reopen Reopen task
17
- edit Edit task
18
- log Add log entry
19
- block Add blocker
20
- unblock Remove blocker
21
- mv, move Move task to different project
22
- rm, remove Delete task
23
- clean Remove old done tasks
24
- check Check for data issues
25
- config Show/set configuration
26
- completions Output shell completions
27
-
28
- GLOBAL OPTIONS:
29
- -C <dir> Run in different directory
30
- --json Output as JSON
31
- -h, --help Show help
32
- -V Show version
33
-
34
- Run 'tk <command> --help' for command-specific options.
35
- `;
36
-
37
- export const COMMAND_HELP: Record<string, string> = {
38
- init: `tk init - Initialize .tasks/ directory
39
-
40
- USAGE:
41
- tk init [options]
42
-
43
- OPTIONS:
44
- -P, --project <name> Set default project name
45
- `,
46
- add: `tk add - Create a new task
47
-
48
- USAGE:
49
- tk add <title> [options]
50
-
51
- OPTIONS:
52
- -p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
53
- -P, --project <name> Project prefix for ID
54
- -d, --description <text> Description
55
- -l, --labels <csv> Labels (comma-separated)
56
- -A, --assignees <csv> Assignees (comma-separated, @me for git user)
57
- --parent <id> Parent task ID
58
- --estimate <n> Estimate (user-defined units)
59
- --due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
60
-
61
- EXAMPLES:
62
- tk add "Fix login bug" -p 1
63
- tk add "New feature" -P api -l bug,urgent
64
- tk add "Sprint task" --due +7d
65
- `,
66
- ls: `tk ls - List tasks
67
-
68
- USAGE:
69
- tk ls [options]
70
-
71
- OPTIONS:
72
- -s, --status <status> Filter by status (open, active, done)
73
- -p, --priority <0-4> Filter by priority
74
- -P, --project <name> Filter by project
75
- -l, --label <label> Filter by label
76
- --assignee <name> Filter by assignee
77
- --parent <id> Filter by parent
78
- --roots Show only root tasks (no parent)
79
- --overdue Show only overdue tasks
80
- -n, --limit <n> Limit results (default: 20)
81
- -a, --all Show all (no limit)
82
-
83
- EXAMPLES:
84
- tk ls -s open -p 1 # Urgent open tasks
85
- tk ls --overdue # Overdue tasks
86
- tk ls -P api --roots # Root tasks in api project
87
- `,
88
- list: `tk list - List tasks (alias for 'ls')
89
-
90
- Run 'tk ls --help' for options.
91
- `,
92
- ready: `tk ready - List ready tasks (active/open + unblocked)
93
-
94
- USAGE:
95
- tk ready
96
-
97
- Shows active or open tasks that are not blocked by any incomplete task.
98
- `,
99
- show: `tk show - Show task details
100
-
101
- USAGE:
102
- tk show <id>
103
-
104
- EXAMPLES:
105
- tk show tk-1
106
- tk show 1 # If unambiguous
107
- `,
108
- start: `tk start - Start working on a task
109
-
110
- USAGE:
111
- tk start <id>
112
-
113
- Changes status from open to active.
114
- `,
115
- done: `tk done - Complete a task
116
-
117
- USAGE:
118
- tk done <id>
119
-
120
- Changes status to done.
121
- `,
122
- reopen: `tk reopen - Reopen a task
123
-
124
- USAGE:
125
- tk reopen <id>
126
-
127
- Changes status back to open.
128
- `,
129
- edit: `tk edit - Edit a task
130
-
131
- USAGE:
132
- tk edit <id> [options]
133
-
134
- OPTIONS:
135
- -t, --title <text> New title
136
- -d, --description <text> New description
137
- -p, --priority <0-4> New priority
138
- -l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
139
- -A, --assignees <csv> Replace assignees
140
- --parent <id> Set parent (use - to clear)
141
- --estimate <n> Set estimate (use - to clear)
142
- --due <date> Set due date (use - to clear)
143
-
144
- EXAMPLES:
145
- tk edit tk-1 -t "New title"
146
- tk edit tk-1 -l +urgent # Add label
147
- tk edit tk-1 --due - # Clear due date
148
- `,
149
- log: `tk log - Add a log entry to a task
150
-
151
- USAGE:
152
- tk log <id> "<message>"
153
-
154
- Message must be quoted.
155
-
156
- EXAMPLES:
157
- tk log tk-1 "Started implementation"
158
- tk log tk-1 "Blocked on API changes"
159
- `,
160
- block: `tk block - Add a blocker dependency
161
-
162
- USAGE:
163
- tk block <task> <blocker>
164
-
165
- The first task becomes blocked by the second.
166
-
167
- EXAMPLES:
168
- tk block tk-2 tk-1 # tk-2 is blocked by tk-1
169
- `,
170
- unblock: `tk unblock - Remove a blocker dependency
171
-
172
- USAGE:
173
- tk unblock <task> <blocker>
174
-
175
- EXAMPLES:
176
- tk unblock tk-2 tk-1
177
- `,
178
- mv: `tk mv - Move a task to a different project
179
-
180
- USAGE:
181
- tk mv <id> <project>
182
-
183
- Moves a task to a new project, keeping the same ref.
184
- Errors if the new ID already exists.
185
- Updates all blocked_by and parent references.
186
-
187
- EXAMPLES:
188
- tk mv api-a1b2 web # Moves api-a1b2 → web-a1b2
189
- tk mv a1b2 archive # Partial ID resolution
190
- `,
191
- move: `tk move - Move a task to a different project (alias for 'mv')
192
-
193
- USAGE:
194
- tk move <id> <project>
195
-
196
- Run 'tk mv --help' for options.
197
- `,
198
- rm: `tk rm - Delete a task
199
-
200
- USAGE:
201
- tk rm <id>
202
-
203
- EXAMPLES:
204
- tk rm tk-1
205
- `,
206
- remove: `tk remove - Delete a task (alias for 'rm')
207
-
208
- USAGE:
209
- tk remove <id>
210
- `,
211
- clean: `tk clean - Remove old completed tasks
212
-
213
- USAGE:
214
- tk clean [options]
215
-
216
- OPTIONS:
217
- --older-than <days> Age threshold in days (default: from config, 14)
218
- -f, --force Remove all done tasks (ignores age and disabled state)
219
-
220
- EXAMPLES:
221
- tk clean # Remove done tasks older than config.clean_after days
222
- tk clean --older-than 30 # Remove done tasks older than 30 days
223
- tk clean --force # Remove all done tasks regardless of age
224
- `,
225
- check: `tk check - Check for data issues
226
-
227
- USAGE:
228
- tk check
229
-
230
- Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
231
- mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
232
- reported for manual intervention.
233
-
234
- Note: Auto-fixing also happens during normal task operations (show, done,
235
- edit, etc.) - this command is for bulk cleanup or diagnostics.
236
-
237
- EXAMPLES:
238
- tk check # Scan and fix all tasks
239
- `,
240
- config: `tk config - Show or set configuration
241
-
242
- USAGE:
243
- tk config Show all config
244
- tk config project Show default project
245
- tk config project <name> Set default project
246
- tk config project <new> --rename <old> Rename project (old-* → new-*)
247
-
248
- EXAMPLES:
249
- tk config project api
250
- tk config project lsmvec --rename cloudlsmvec
251
- `,
252
- completions: `tk completions - Output shell completions
253
-
254
- USAGE:
255
- tk completions <shell>
256
-
257
- SHELLS:
258
- bash Bash completion script
259
- zsh Zsh completion script
260
- fish Fish completion script
261
-
262
- EXAMPLES:
263
- eval "$(tk completions bash)" # Add to ~/.bashrc
264
- eval "$(tk completions zsh)" # Add to ~/.zshrc
265
- `,
266
- };
@@ -1,105 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import { parsePriority, formatPriority, formatPriorityName } from "./priority";
3
-
4
- describe("parsePriority", () => {
5
- describe("valid inputs", () => {
6
- test("returns 3 (medium) for undefined", () => {
7
- expect(parsePriority(undefined)).toBe(3);
8
- });
9
-
10
- test("parses numeric strings 0-4", () => {
11
- expect(parsePriority("0")).toBe(0);
12
- expect(parsePriority("1")).toBe(1);
13
- expect(parsePriority("2")).toBe(2);
14
- expect(parsePriority("3")).toBe(3);
15
- expect(parsePriority("4")).toBe(4);
16
- });
17
-
18
- test("parses numbers 0-4", () => {
19
- expect(parsePriority(0)).toBe(0);
20
- expect(parsePriority(1)).toBe(1);
21
- expect(parsePriority(2)).toBe(2);
22
- expect(parsePriority(3)).toBe(3);
23
- expect(parsePriority(4)).toBe(4);
24
- });
25
-
26
- test("parses p0-p4 format (lowercase)", () => {
27
- expect(parsePriority("p0")).toBe(0);
28
- expect(parsePriority("p1")).toBe(1);
29
- expect(parsePriority("p2")).toBe(2);
30
- expect(parsePriority("p3")).toBe(3);
31
- expect(parsePriority("p4")).toBe(4);
32
- });
33
-
34
- test("parses P0-P4 format (uppercase)", () => {
35
- expect(parsePriority("P0")).toBe(0);
36
- expect(parsePriority("P1")).toBe(1);
37
- expect(parsePriority("P2")).toBe(2);
38
- expect(parsePriority("P3")).toBe(3);
39
- expect(parsePriority("P4")).toBe(4);
40
- });
41
-
42
- test("parses named priorities", () => {
43
- expect(parsePriority("none")).toBe(0);
44
- expect(parsePriority("urgent")).toBe(1);
45
- expect(parsePriority("high")).toBe(2);
46
- expect(parsePriority("medium")).toBe(3);
47
- expect(parsePriority("low")).toBe(4);
48
- });
49
-
50
- test("parses named priorities case-insensitively", () => {
51
- expect(parsePriority("URGENT")).toBe(1);
52
- expect(parsePriority("High")).toBe(2);
53
- expect(parsePriority("MEDIUM")).toBe(3);
54
- });
55
- });
56
-
57
- describe("invalid inputs", () => {
58
- test("throws for out of range numbers", () => {
59
- expect(() => parsePriority("5")).toThrow("Invalid priority");
60
- expect(() => parsePriority("-1")).toThrow("Invalid priority");
61
- expect(() => parsePriority("99")).toThrow("Invalid priority");
62
- });
63
-
64
- test("throws for out of range pX format", () => {
65
- expect(() => parsePriority("p5")).toThrow("Invalid priority");
66
- expect(() => parsePriority("p6")).toThrow("Invalid priority");
67
- expect(() => parsePriority("p-1")).toThrow("Invalid priority");
68
- });
69
-
70
- test("throws for invalid named priorities", () => {
71
- expect(() => parsePriority("critical")).toThrow("Invalid priority");
72
- expect(() => parsePriority("abc")).toThrow("Invalid priority");
73
- });
74
-
75
- test("throws for empty string", () => {
76
- expect(() => parsePriority("")).toThrow("Invalid priority");
77
- });
78
-
79
- test("error message includes valid formats", () => {
80
- expect(() => parsePriority("invalid")).toThrow(
81
- "Use 0-4, p0-p4, or none/urgent/high/medium/low",
82
- );
83
- });
84
- });
85
- });
86
-
87
- describe("formatPriority", () => {
88
- test("formats priorities as pX", () => {
89
- expect(formatPriority(0)).toBe("p0");
90
- expect(formatPriority(1)).toBe("p1");
91
- expect(formatPriority(2)).toBe("p2");
92
- expect(formatPriority(3)).toBe("p3");
93
- expect(formatPriority(4)).toBe("p4");
94
- });
95
- });
96
-
97
- describe("formatPriorityName", () => {
98
- test("formats priorities as names", () => {
99
- expect(formatPriorityName(0)).toBe("none");
100
- expect(formatPriorityName(1)).toBe("urgent");
101
- expect(formatPriorityName(2)).toBe("high");
102
- expect(formatPriorityName(3)).toBe("medium");
103
- expect(formatPriorityName(4)).toBe("low");
104
- });
105
- });
@@ -1,40 +0,0 @@
1
- import type { Priority } from "../types";
2
- import { PRIORITY_FROM_NAME, PRIORITY_LABELS } from "../types";
3
-
4
- /**
5
- * Parse priority from various formats:
6
- * - Number: 0-4
7
- * - Prefixed: p0-p4
8
- * - Named: none, urgent, high, medium, low
9
- */
10
- export function parsePriority(input: string | number | undefined): Priority {
11
- if (input === undefined) return 3; // default: medium
12
-
13
- const str = String(input).toLowerCase();
14
-
15
- // Handle named format
16
- const named = PRIORITY_FROM_NAME[str];
17
- if (named !== undefined) {
18
- return named;
19
- }
20
-
21
- // Handle pX format
22
- if (str.startsWith("p")) {
23
- const num = parseInt(str.slice(1), 10);
24
- if (num >= 0 && num <= 4) return num as Priority;
25
- }
26
-
27
- // Handle numeric format
28
- const num = parseInt(str, 10);
29
- if (num >= 0 && num <= 4) return num as Priority;
30
-
31
- throw new Error(`Invalid priority: ${input}. Use 0-4, p0-p4, or none/urgent/high/medium/low.`);
32
- }
33
-
34
- export function formatPriority(p: Priority): string {
35
- return `p${p}`;
36
- }
37
-
38
- export function formatPriorityName(p: Priority): string {
39
- return PRIORITY_LABELS[p];
40
- }
package/src/lib/root.ts DELETED
@@ -1,79 +0,0 @@
1
- import { existsSync } from "fs";
2
- import { join, dirname, resolve } from "path";
3
-
4
- const TASKS_DIR = ".tasks";
5
- const GIT_DIR = ".git";
6
-
7
- // Global override for working directory (set via -C flag)
8
- let workingDir: string | null = null;
9
-
10
- export function setWorkingDir(dir: string | null): void {
11
- workingDir = dir ? resolve(dir) : null;
12
- }
13
-
14
- export function getWorkingDir(): string {
15
- return workingDir ?? process.cwd();
16
- }
17
-
18
- export interface RootInfo {
19
- root: string;
20
- tasksDir: string;
21
- exists: boolean;
22
- }
23
-
24
- /**
25
- * Find the project root by walking up directories.
26
- * Looks for existing .tasks/ or .git/ directory.
27
- * Returns cwd if neither found.
28
- */
29
- export function findRoot(): RootInfo {
30
- let current = getWorkingDir();
31
-
32
- while (true) {
33
- const tasksPath = join(current, TASKS_DIR);
34
- if (existsSync(tasksPath)) {
35
- return { root: current, tasksDir: tasksPath, exists: true };
36
- }
37
-
38
- const gitPath = join(current, GIT_DIR);
39
- if (existsSync(gitPath)) {
40
- return {
41
- root: current,
42
- tasksDir: join(current, TASKS_DIR),
43
- exists: false,
44
- };
45
- }
46
-
47
- const parent = dirname(current);
48
- if (parent === current) break; // Reached filesystem root
49
- current = parent;
50
- }
51
-
52
- // No .git or .tasks found, use working dir
53
- const wd = getWorkingDir();
54
- return {
55
- root: wd,
56
- tasksDir: join(wd, TASKS_DIR),
57
- exists: false,
58
- };
59
- }
60
-
61
- /**
62
- * Get the tasks directory path, optionally requiring it to exist.
63
- */
64
- export function getTasksDir(requireExists = false): string {
65
- const info = findRoot();
66
- if (requireExists && !info.exists) {
67
- throw new TasksNotFoundError(info.root);
68
- }
69
- return info.tasksDir;
70
- }
71
-
72
- export class TasksNotFoundError extends Error {
73
- constructor(searchedFrom: string) {
74
- super(
75
- `No .tasks/ directory found. Run 'tk add' to create one, or initialize with 'tk init'.\nSearched from: ${searchedFrom}`,
76
- );
77
- this.name = "TasksNotFoundError";
78
- }
79
- }
@@ -1,222 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import {
3
- isTaskOverdue,
4
- daysUntilDue,
5
- formatRelativeTime,
6
- formatDate,
7
- formatLocalDate,
8
- parseDueDate,
9
- parseEstimate,
10
- DUE_SOON_THRESHOLD,
11
- } from "./time";
12
-
13
- describe("isTaskOverdue", () => {
14
- test("returns false when no due date", () => {
15
- expect(isTaskOverdue(null, "open")).toBe(false);
16
- });
17
-
18
- test("returns false for done tasks regardless of date", () => {
19
- expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
20
- });
21
-
22
- test("returns false for active done tasks", () => {
23
- expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
24
- });
25
-
26
- test("returns true for past dates (open)", () => {
27
- expect(isTaskOverdue("2020-01-01", "open")).toBe(true);
28
- });
29
-
30
- test("returns true for past dates (active)", () => {
31
- expect(isTaskOverdue("2020-06-15", "active")).toBe(true);
32
- });
33
-
34
- test("returns false for today", () => {
35
- const today = new Date().toISOString().split("T")[0]!;
36
- expect(isTaskOverdue(today, "open")).toBe(false);
37
- });
38
-
39
- test("returns false for future dates", () => {
40
- const future = new Date();
41
- future.setDate(future.getDate() + 7);
42
- const dateStr = future.toISOString().split("T")[0]!;
43
- expect(isTaskOverdue(dateStr, "open")).toBe(false);
44
- });
45
-
46
- test("returns false for invalid date string", () => {
47
- expect(isTaskOverdue("not-a-date", "open")).toBe(false);
48
- });
49
- });
50
-
51
- describe("daysUntilDue", () => {
52
- test("returns null when no due date", () => {
53
- expect(daysUntilDue(null, "open")).toBeNull();
54
- });
55
-
56
- test("returns null when no due date (active)", () => {
57
- expect(daysUntilDue(null, "active")).toBeNull();
58
- });
59
-
60
- test("returns null for done tasks", () => {
61
- const future = new Date();
62
- future.setDate(future.getDate() + 3);
63
- const dateStr = future.toISOString().split("T")[0]!;
64
- expect(daysUntilDue(dateStr, "done")).toBeNull();
65
- });
66
-
67
- test("returns 0 for due today (open)", () => {
68
- const today = new Date().toISOString().split("T")[0]!;
69
- expect(daysUntilDue(today, "open")).toBe(0);
70
- });
71
-
72
- test("returns 0 for due today (active)", () => {
73
- const today = new Date().toISOString().split("T")[0]!;
74
- expect(daysUntilDue(today, "active")).toBe(0);
75
- });
76
-
77
- test("returns N for due in N days", () => {
78
- const future = new Date();
79
- future.setDate(future.getDate() + 3);
80
- const dateStr = future.toISOString().split("T")[0]!;
81
- expect(daysUntilDue(dateStr, "open")).toBe(3);
82
- });
83
-
84
- test("returns null for past dates (already overdue)", () => {
85
- expect(daysUntilDue("2020-01-01", "open")).toBeNull();
86
- });
87
-
88
- test("returns null for invalid date string", () => {
89
- expect(daysUntilDue("not-a-date", "open")).toBeNull();
90
- });
91
-
92
- test("DUE_SOON_THRESHOLD is 7", () => {
93
- expect(DUE_SOON_THRESHOLD).toBe(7);
94
- });
95
- });
96
-
97
- describe("formatRelativeTime", () => {
98
- test("returns 'now' for very recent timestamps", () => {
99
- const ts = new Date().toISOString();
100
- expect(formatRelativeTime(ts)).toBe("now");
101
- });
102
-
103
- test("returns minutes for timestamps minutes ago", () => {
104
- const ts = new Date(Date.now() - 5 * 60 * 1000).toISOString();
105
- expect(formatRelativeTime(ts)).toBe("5m");
106
- });
107
-
108
- test("returns hours for timestamps hours ago", () => {
109
- const ts = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
110
- expect(formatRelativeTime(ts)).toBe("3h");
111
- });
112
-
113
- test("returns days for timestamps days ago", () => {
114
- const ts = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
115
- expect(formatRelativeTime(ts)).toBe("2d");
116
- });
117
- });
118
-
119
- describe("formatDate", () => {
120
- test("returns a non-empty string for a valid ISO timestamp", () => {
121
- const result = formatDate("2024-01-15T10:30:00.000Z");
122
- expect(typeof result).toBe("string");
123
- expect(result.length).toBeGreaterThan(0);
124
- });
125
- });
126
-
127
- describe("formatLocalDate", () => {
128
- test("formats date as YYYY-MM-DD", () => {
129
- const date = new Date(2026, 0, 15); // Jan 15 2026
130
- expect(formatLocalDate(date)).toBe("2026-01-15");
131
- });
132
-
133
- test("zero-pads month and day", () => {
134
- const date = new Date(2026, 1, 5); // Feb 5 2026
135
- expect(formatLocalDate(date)).toBe("2026-02-05");
136
- });
137
- });
138
-
139
- describe("parseDueDate", () => {
140
- test("returns undefined for undefined input", () => {
141
- expect(parseDueDate(undefined)).toBeUndefined();
142
- });
143
-
144
- test("returns undefined for '-' (clear sentinel)", () => {
145
- expect(parseDueDate("-")).toBeUndefined();
146
- });
147
-
148
- test("returns valid YYYY-MM-DD unchanged", () => {
149
- expect(parseDueDate("2026-06-15")).toBe("2026-06-15");
150
- });
151
-
152
- test("parses +Nd relative date", () => {
153
- const result = parseDueDate("+7d");
154
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
155
- const resultDate = new Date(result!);
156
- const today = new Date();
157
- const diffDays = Math.round((resultDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
158
- expect(diffDays).toBeGreaterThanOrEqual(6);
159
- expect(diffDays).toBeLessThanOrEqual(8);
160
- });
161
-
162
- test("parses +Nw relative date", () => {
163
- const result = parseDueDate("+2w");
164
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
165
- });
166
-
167
- test("parses +Nm relative date", () => {
168
- const result = parseDueDate("+1m");
169
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
170
- });
171
-
172
- test("parses +Nh relative date", () => {
173
- const result = parseDueDate("+3h");
174
- expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
175
- });
176
-
177
- test("throws for invalid relative format", () => {
178
- expect(() => parseDueDate("+7x")).toThrow("Invalid relative date");
179
- });
180
-
181
- test("throws for bare + with no content", () => {
182
- expect(() => parseDueDate("+")).toThrow("Invalid relative date");
183
- });
184
-
185
- test("throws for non-date string", () => {
186
- expect(() => parseDueDate("not-a-date")).toThrow("Invalid date format");
187
- });
188
-
189
- test("throws for invalid calendar date (month 13)", () => {
190
- expect(() => parseDueDate("2026-13-01")).toThrow("Invalid date format");
191
- });
192
-
193
- test("throws for invalid calendar date (day 32)", () => {
194
- expect(() => parseDueDate("2026-01-32")).toThrow("Invalid date format");
195
- });
196
- });
197
-
198
- describe("parseEstimate", () => {
199
- test("returns undefined for undefined input", () => {
200
- expect(parseEstimate(undefined)).toBeUndefined();
201
- });
202
-
203
- test("parses valid integer string", () => {
204
- expect(parseEstimate("42")).toBe(42);
205
- });
206
-
207
- test("parses zero", () => {
208
- expect(parseEstimate("0")).toBe(0);
209
- });
210
-
211
- test("throws for non-numeric string", () => {
212
- expect(() => parseEstimate("abc")).toThrow("Invalid estimate");
213
- });
214
-
215
- test("throws for decimal", () => {
216
- expect(() => parseEstimate("3.5")).toThrow("Invalid estimate");
217
- });
218
-
219
- test("throws for negative number", () => {
220
- expect(() => parseEstimate("-1")).toThrow("Invalid estimate");
221
- });
222
- });