digital-tasks 2.0.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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +10 -0
- package/dist/function-task.d.ts +319 -0
- package/dist/function-task.d.ts.map +1 -0
- package/dist/function-task.js +286 -0
- package/dist/function-task.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +112 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +510 -0
- package/dist/markdown.js.map +1 -0
- package/dist/project.d.ts +259 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +397 -0
- package/dist/project.js.map +1 -0
- package/dist/queue.d.ts +17 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +347 -0
- package/dist/queue.js.map +1 -0
- package/dist/task.d.ts +69 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +321 -0
- package/dist/task.js.map +1 -0
- package/dist/types.d.ts +292 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +161 -0
- package/src/markdown.ts +622 -0
- package/src/project.ts +571 -0
- package/src/queue.ts +424 -0
- package/src/task.ts +389 -0
- package/src/types.ts +403 -0
- package/test/markdown.test.ts +550 -0
- package/test/project.test.ts +562 -0
- package/test/queue.test.ts +482 -0
- package/test/task.test.ts +464 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
parseMarkdown,
|
|
4
|
+
toMarkdown,
|
|
5
|
+
syncStatusFromMarkdown,
|
|
6
|
+
task,
|
|
7
|
+
parallel,
|
|
8
|
+
sequential,
|
|
9
|
+
createProject,
|
|
10
|
+
} from '../src/index.js'
|
|
11
|
+
import type { TaskDefinition, ParallelGroup, SequentialGroup } from '../src/index.js'
|
|
12
|
+
|
|
13
|
+
describe('Markdown Parser', () => {
|
|
14
|
+
describe('parseMarkdown()', () => {
|
|
15
|
+
it('should parse project name from h1', () => {
|
|
16
|
+
const markdown = `# My Project
|
|
17
|
+
|
|
18
|
+
- [ ] Task 1
|
|
19
|
+
`
|
|
20
|
+
const project = parseMarkdown(markdown)
|
|
21
|
+
|
|
22
|
+
expect(project.name).toBe('My Project')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should parse parallel tasks from unordered list', () => {
|
|
26
|
+
const markdown = `# Project
|
|
27
|
+
|
|
28
|
+
- [ ] Task A
|
|
29
|
+
- [ ] Task B
|
|
30
|
+
- [ ] Task C
|
|
31
|
+
`
|
|
32
|
+
const project = parseMarkdown(markdown)
|
|
33
|
+
|
|
34
|
+
expect(project.tasks).toHaveLength(1)
|
|
35
|
+
expect((project.tasks[0] as ParallelGroup).__type).toBe('parallel')
|
|
36
|
+
expect((project.tasks[0] as ParallelGroup).tasks).toHaveLength(3)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should parse sequential tasks from ordered list', () => {
|
|
40
|
+
const markdown = `# Project
|
|
41
|
+
|
|
42
|
+
1. [ ] Step 1
|
|
43
|
+
2. [ ] Step 2
|
|
44
|
+
3. [ ] Step 3
|
|
45
|
+
`
|
|
46
|
+
const project = parseMarkdown(markdown)
|
|
47
|
+
|
|
48
|
+
expect(project.tasks).toHaveLength(1)
|
|
49
|
+
expect((project.tasks[0] as SequentialGroup).__type).toBe('sequential')
|
|
50
|
+
expect((project.tasks[0] as SequentialGroup).tasks).toHaveLength(3)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should parse task status from checkboxes', () => {
|
|
54
|
+
const markdown = `# Project
|
|
55
|
+
|
|
56
|
+
- [ ] Pending
|
|
57
|
+
- [x] Completed
|
|
58
|
+
- [-] In progress
|
|
59
|
+
- [~] Blocked
|
|
60
|
+
- [!] Failed
|
|
61
|
+
`
|
|
62
|
+
const project = parseMarkdown(markdown)
|
|
63
|
+
|
|
64
|
+
const tasks = (project.tasks[0] as ParallelGroup).tasks as TaskDefinition[]
|
|
65
|
+
expect(tasks[0].metadata?._originalStatus).toBe('pending')
|
|
66
|
+
expect(tasks[1].metadata?._originalStatus).toBe('completed')
|
|
67
|
+
expect(tasks[2].metadata?._originalStatus).toBe('in_progress')
|
|
68
|
+
expect(tasks[3].metadata?._originalStatus).toBe('blocked')
|
|
69
|
+
expect(tasks[4].metadata?._originalStatus).toBe('failed')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should parse priority markers', () => {
|
|
73
|
+
const markdown = `# Project
|
|
74
|
+
|
|
75
|
+
- [ ] !!Critical task
|
|
76
|
+
- [ ] !Urgent task
|
|
77
|
+
- [ ] ^High priority
|
|
78
|
+
- [ ] Normal task
|
|
79
|
+
- [ ] vLow priority
|
|
80
|
+
`
|
|
81
|
+
const project = parseMarkdown(markdown)
|
|
82
|
+
|
|
83
|
+
const tasks = (project.tasks[0] as ParallelGroup).tasks as TaskDefinition[]
|
|
84
|
+
expect(tasks[0].priority).toBe('critical')
|
|
85
|
+
expect(tasks[0].title).toBe('Critical task')
|
|
86
|
+
expect(tasks[1].priority).toBe('urgent')
|
|
87
|
+
expect(tasks[2].priority).toBe('high')
|
|
88
|
+
expect(tasks[3].priority).toBe('normal')
|
|
89
|
+
expect(tasks[4].priority).toBe('low')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should parse nested subtasks', () => {
|
|
93
|
+
const markdown = `# Project
|
|
94
|
+
|
|
95
|
+
- [ ] Parent task
|
|
96
|
+
- [ ] Subtask 1
|
|
97
|
+
- [ ] Subtask 2
|
|
98
|
+
`
|
|
99
|
+
const project = parseMarkdown(markdown)
|
|
100
|
+
|
|
101
|
+
const parent = ((project.tasks[0] as ParallelGroup).tasks[0]) as TaskDefinition
|
|
102
|
+
expect(parent.title).toBe('Parent task')
|
|
103
|
+
expect(parent.subtasks).toHaveLength(2)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should parse sections with h2 headings', () => {
|
|
107
|
+
const markdown = `# Project
|
|
108
|
+
|
|
109
|
+
## Planning
|
|
110
|
+
- [ ] Design
|
|
111
|
+
- [ ] Spec
|
|
112
|
+
|
|
113
|
+
## Implementation (sequential)
|
|
114
|
+
1. [ ] Backend
|
|
115
|
+
2. [ ] Frontend
|
|
116
|
+
`
|
|
117
|
+
const project = parseMarkdown(markdown)
|
|
118
|
+
|
|
119
|
+
// Parser creates groups based on task types, sections affect mode detection
|
|
120
|
+
expect(project.tasks.length).toBeGreaterThanOrEqual(1)
|
|
121
|
+
// First section tasks should be parsed
|
|
122
|
+
let foundDesign = false
|
|
123
|
+
let foundBackend = false
|
|
124
|
+
|
|
125
|
+
function findTasks(tasks: any[]): void {
|
|
126
|
+
for (const t of tasks) {
|
|
127
|
+
if (t.__type === 'task') {
|
|
128
|
+
if (t.title === 'Design') foundDesign = true
|
|
129
|
+
if (t.title === 'Backend') foundBackend = true
|
|
130
|
+
} else if (t.tasks) {
|
|
131
|
+
findTasks(t.tasks)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
findTasks(project.tasks)
|
|
136
|
+
|
|
137
|
+
expect(foundDesign).toBe(true)
|
|
138
|
+
expect(foundBackend).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should detect execution mode from section name', () => {
|
|
142
|
+
const markdown = `# Project
|
|
143
|
+
|
|
144
|
+
## Tasks (parallel)
|
|
145
|
+
- [ ] Task 1
|
|
146
|
+
- [ ] Task 2
|
|
147
|
+
`
|
|
148
|
+
const project = parseMarkdown(markdown)
|
|
149
|
+
|
|
150
|
+
// Section name with (parallel) should create parallel group
|
|
151
|
+
expect(project.tasks.length).toBeGreaterThanOrEqual(1)
|
|
152
|
+
// Find the tasks
|
|
153
|
+
let foundTask1 = false
|
|
154
|
+
function findTasks(tasks: any[]): void {
|
|
155
|
+
for (const t of tasks) {
|
|
156
|
+
if (t.__type === 'task' && t.title === 'Task 1') foundTask1 = true
|
|
157
|
+
else if (t.tasks) findTasks(t.tasks)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
findTasks(project.tasks)
|
|
161
|
+
expect(foundTask1).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should handle empty markdown', () => {
|
|
165
|
+
const project = parseMarkdown('')
|
|
166
|
+
expect(project.name).toBe('Untitled Project')
|
|
167
|
+
expect(project.tasks).toEqual([])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should handle markdown with only headings', () => {
|
|
171
|
+
const markdown = `# My Project
|
|
172
|
+
|
|
173
|
+
## Section 1
|
|
174
|
+
|
|
175
|
+
## Section 2
|
|
176
|
+
`
|
|
177
|
+
const project = parseMarkdown(markdown)
|
|
178
|
+
expect(project.name).toBe('My Project')
|
|
179
|
+
expect(project.tasks).toEqual([])
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('toMarkdown()', () => {
|
|
184
|
+
it('should serialize project name as h1', () => {
|
|
185
|
+
const project = createProject({
|
|
186
|
+
name: 'Test Project',
|
|
187
|
+
tasks: [],
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const markdown = toMarkdown(project)
|
|
191
|
+
expect(markdown).toContain('# Test Project')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should serialize project description', () => {
|
|
195
|
+
const project = createProject({
|
|
196
|
+
name: 'Test Project',
|
|
197
|
+
description: 'This is a description',
|
|
198
|
+
tasks: [],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const markdown = toMarkdown(project)
|
|
202
|
+
expect(markdown).toContain('This is a description')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should serialize parallel tasks as unordered list', () => {
|
|
206
|
+
const project = createProject({
|
|
207
|
+
name: 'Test',
|
|
208
|
+
tasks: [
|
|
209
|
+
parallel(
|
|
210
|
+
task('Task A'),
|
|
211
|
+
task('Task B'),
|
|
212
|
+
),
|
|
213
|
+
],
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const markdown = toMarkdown(project)
|
|
217
|
+
expect(markdown).toContain('- [ ] Task A')
|
|
218
|
+
expect(markdown).toContain('- [ ] Task B')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should serialize sequential tasks as ordered list', () => {
|
|
222
|
+
const project = createProject({
|
|
223
|
+
name: 'Test',
|
|
224
|
+
tasks: [
|
|
225
|
+
sequential(
|
|
226
|
+
task('Step 1'),
|
|
227
|
+
task('Step 2'),
|
|
228
|
+
),
|
|
229
|
+
],
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const markdown = toMarkdown(project)
|
|
233
|
+
expect(markdown).toContain('1. [ ] Step 1')
|
|
234
|
+
expect(markdown).toContain('2. [ ] Step 2')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should serialize task status', () => {
|
|
238
|
+
const project = createProject({
|
|
239
|
+
name: 'Test',
|
|
240
|
+
tasks: [
|
|
241
|
+
parallel(
|
|
242
|
+
task('Pending', { metadata: { _originalStatus: 'pending' } }),
|
|
243
|
+
task('Completed', { metadata: { _originalStatus: 'completed' } }),
|
|
244
|
+
task('In Progress', { metadata: { _originalStatus: 'in_progress' } }),
|
|
245
|
+
),
|
|
246
|
+
],
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const markdown = toMarkdown(project)
|
|
250
|
+
expect(markdown).toContain('- [ ] Pending')
|
|
251
|
+
expect(markdown).toContain('- [x] Completed')
|
|
252
|
+
expect(markdown).toContain('- [-] In Progress')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should serialize priority markers when enabled', () => {
|
|
256
|
+
const project = createProject({
|
|
257
|
+
name: 'Test',
|
|
258
|
+
tasks: [
|
|
259
|
+
parallel(
|
|
260
|
+
task('Critical', { priority: 'critical' }),
|
|
261
|
+
task('Urgent', { priority: 'urgent' }),
|
|
262
|
+
task('High', { priority: 'high' }),
|
|
263
|
+
task('Normal', { priority: 'normal' }),
|
|
264
|
+
task('Low', { priority: 'low' }),
|
|
265
|
+
),
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const markdown = toMarkdown(project, { includePriority: true })
|
|
270
|
+
expect(markdown).toContain('!!Critical')
|
|
271
|
+
expect(markdown).toContain('!Urgent')
|
|
272
|
+
expect(markdown).toContain('^High')
|
|
273
|
+
expect(markdown).toContain('vLow')
|
|
274
|
+
expect(markdown).toMatch(/\[ \] Normal/) // No marker for normal
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should serialize subtasks with indentation', () => {
|
|
278
|
+
const project = createProject({
|
|
279
|
+
name: 'Test',
|
|
280
|
+
tasks: [
|
|
281
|
+
parallel(
|
|
282
|
+
task('Parent', {
|
|
283
|
+
subtasks: [
|
|
284
|
+
task('Child 1'),
|
|
285
|
+
task('Child 2'),
|
|
286
|
+
],
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
289
|
+
],
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const markdown = toMarkdown(project)
|
|
293
|
+
expect(markdown).toContain('- [ ] Parent')
|
|
294
|
+
expect(markdown).toContain(' - [ ] Child 1')
|
|
295
|
+
expect(markdown).toContain(' - [ ] Child 2')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should handle nested parallel and sequential groups', () => {
|
|
299
|
+
const project = createProject({
|
|
300
|
+
name: 'Test',
|
|
301
|
+
tasks: [
|
|
302
|
+
sequential(
|
|
303
|
+
parallel(
|
|
304
|
+
task('A'),
|
|
305
|
+
task('B'),
|
|
306
|
+
),
|
|
307
|
+
task('C'),
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const markdown = toMarkdown(project)
|
|
313
|
+
// Nested parallel should be unordered
|
|
314
|
+
expect(markdown).toContain('- [ ] A')
|
|
315
|
+
expect(markdown).toContain('- [ ] B')
|
|
316
|
+
// Following task should be sequential
|
|
317
|
+
expect(markdown).toContain('[ ] C')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should respect indentSize option', () => {
|
|
321
|
+
const project = createProject({
|
|
322
|
+
name: 'Test',
|
|
323
|
+
tasks: [
|
|
324
|
+
parallel(
|
|
325
|
+
task('Parent', {
|
|
326
|
+
subtasks: [task('Child')],
|
|
327
|
+
}),
|
|
328
|
+
),
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const markdown = toMarkdown(project, { indentSize: 4 })
|
|
333
|
+
expect(markdown).toContain(' - [ ] Child')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Round-trip conversion', () => {
|
|
338
|
+
it('should preserve basic structure through round-trip', () => {
|
|
339
|
+
const original = `# Project Name
|
|
340
|
+
|
|
341
|
+
- [ ] Task A
|
|
342
|
+
- [ ] Task B
|
|
343
|
+
- [ ] Task C
|
|
344
|
+
`
|
|
345
|
+
const project = parseMarkdown(original)
|
|
346
|
+
const regenerated = toMarkdown(project)
|
|
347
|
+
|
|
348
|
+
expect(regenerated).toContain('# Project Name')
|
|
349
|
+
expect(regenerated).toContain('- [ ] Task A')
|
|
350
|
+
expect(regenerated).toContain('- [ ] Task B')
|
|
351
|
+
expect(regenerated).toContain('- [ ] Task C')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should preserve sequential structure through round-trip', () => {
|
|
355
|
+
const original = `# Sequential Project
|
|
356
|
+
|
|
357
|
+
1. [ ] Step 1
|
|
358
|
+
2. [ ] Step 2
|
|
359
|
+
3. [ ] Step 3
|
|
360
|
+
`
|
|
361
|
+
const project = parseMarkdown(original)
|
|
362
|
+
const regenerated = toMarkdown(project)
|
|
363
|
+
|
|
364
|
+
expect(regenerated).toContain('1. [ ] Step 1')
|
|
365
|
+
expect(regenerated).toContain('2. [ ] Step 2')
|
|
366
|
+
expect(regenerated).toContain('3. [ ] Step 3')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should preserve task status through round-trip', () => {
|
|
370
|
+
const original = `# Status Project
|
|
371
|
+
|
|
372
|
+
- [x] Completed
|
|
373
|
+
- [-] In progress
|
|
374
|
+
- [ ] Pending
|
|
375
|
+
`
|
|
376
|
+
const project = parseMarkdown(original)
|
|
377
|
+
const regenerated = toMarkdown(project)
|
|
378
|
+
|
|
379
|
+
expect(regenerated).toContain('[x] Completed')
|
|
380
|
+
expect(regenerated).toContain('[-] In progress')
|
|
381
|
+
expect(regenerated).toContain('[ ] Pending')
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe('syncStatusFromMarkdown()', () => {
|
|
386
|
+
it('should update task statuses from markdown', () => {
|
|
387
|
+
const project = createProject({
|
|
388
|
+
name: 'Test',
|
|
389
|
+
tasks: [
|
|
390
|
+
parallel(
|
|
391
|
+
task('Task A', { metadata: { _originalStatus: 'pending' } }),
|
|
392
|
+
task('Task B', { metadata: { _originalStatus: 'pending' } }),
|
|
393
|
+
),
|
|
394
|
+
],
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const markdown = `# Test
|
|
398
|
+
|
|
399
|
+
- [x] Task A
|
|
400
|
+
- [-] Task B
|
|
401
|
+
`
|
|
402
|
+
const updated = syncStatusFromMarkdown(project, markdown)
|
|
403
|
+
|
|
404
|
+
const tasks = (updated.tasks[0] as ParallelGroup).tasks as TaskDefinition[]
|
|
405
|
+
expect(tasks[0].metadata?._originalStatus).toBe('completed')
|
|
406
|
+
expect(tasks[1].metadata?._originalStatus).toBe('in_progress')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should preserve existing status if task not in markdown', () => {
|
|
410
|
+
const project = createProject({
|
|
411
|
+
name: 'Test',
|
|
412
|
+
tasks: [
|
|
413
|
+
parallel(
|
|
414
|
+
task('Existing Task', { metadata: { _originalStatus: 'in_progress' } }),
|
|
415
|
+
),
|
|
416
|
+
],
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const markdown = `# Test
|
|
420
|
+
|
|
421
|
+
- [ ] Different Task
|
|
422
|
+
`
|
|
423
|
+
const updated = syncStatusFromMarkdown(project, markdown)
|
|
424
|
+
|
|
425
|
+
const tasks = (updated.tasks[0] as ParallelGroup).tasks as TaskDefinition[]
|
|
426
|
+
expect(tasks[0].metadata?._originalStatus).toBe('in_progress')
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should update nested subtask statuses', () => {
|
|
430
|
+
const project = createProject({
|
|
431
|
+
name: 'Test',
|
|
432
|
+
tasks: [
|
|
433
|
+
parallel(
|
|
434
|
+
task('Parent', {
|
|
435
|
+
metadata: { _originalStatus: 'pending' },
|
|
436
|
+
subtasks: [
|
|
437
|
+
task('Child', { metadata: { _originalStatus: 'pending' } }),
|
|
438
|
+
],
|
|
439
|
+
}),
|
|
440
|
+
),
|
|
441
|
+
],
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const markdown = `# Test
|
|
445
|
+
|
|
446
|
+
- [x] Parent
|
|
447
|
+
- [x] Child
|
|
448
|
+
`
|
|
449
|
+
const updated = syncStatusFromMarkdown(project, markdown)
|
|
450
|
+
|
|
451
|
+
const parent = (updated.tasks[0] as ParallelGroup).tasks[0] as TaskDefinition
|
|
452
|
+
expect(parent.metadata?._originalStatus).toBe('completed')
|
|
453
|
+
expect((parent.subtasks![0] as TaskDefinition).metadata?._originalStatus).toBe('completed')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('should update the updatedAt timestamp', () => {
|
|
457
|
+
const project = createProject({
|
|
458
|
+
name: 'Test',
|
|
459
|
+
tasks: [],
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const originalUpdatedAt = project.updatedAt
|
|
463
|
+
const markdown = `# Test`
|
|
464
|
+
|
|
465
|
+
// Small delay to ensure different timestamp
|
|
466
|
+
const updated = syncStatusFromMarkdown(project, markdown)
|
|
467
|
+
|
|
468
|
+
expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(originalUpdatedAt.getTime())
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe('Edge cases', () => {
|
|
473
|
+
it('should handle tasks with special characters', () => {
|
|
474
|
+
const markdown = `# Project
|
|
475
|
+
|
|
476
|
+
- [ ] Task with "quotes"
|
|
477
|
+
- [ ] Task with \`backticks\`
|
|
478
|
+
- [ ] Task with & ampersand
|
|
479
|
+
`
|
|
480
|
+
const project = parseMarkdown(markdown)
|
|
481
|
+
const tasks = (project.tasks[0] as ParallelGroup).tasks as TaskDefinition[]
|
|
482
|
+
|
|
483
|
+
expect(tasks[0].title).toBe('Task with "quotes"')
|
|
484
|
+
expect(tasks[1].title).toBe('Task with `backticks`')
|
|
485
|
+
expect(tasks[2].title).toBe('Task with & ampersand')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('should handle deeply nested subtasks', () => {
|
|
489
|
+
const markdown = `# Project
|
|
490
|
+
|
|
491
|
+
- [ ] Level 1
|
|
492
|
+
- [ ] Level 2
|
|
493
|
+
- [ ] Level 3
|
|
494
|
+
- [ ] Level 4
|
|
495
|
+
`
|
|
496
|
+
const project = parseMarkdown(markdown)
|
|
497
|
+
const level1 = (project.tasks[0] as ParallelGroup).tasks[0] as TaskDefinition
|
|
498
|
+
const level2 = level1.subtasks![0] as TaskDefinition
|
|
499
|
+
const level3 = level2.subtasks![0] as TaskDefinition
|
|
500
|
+
const level4 = level3.subtasks![0] as TaskDefinition
|
|
501
|
+
|
|
502
|
+
expect(level1.title).toBe('Level 1')
|
|
503
|
+
expect(level2.title).toBe('Level 2')
|
|
504
|
+
expect(level3.title).toBe('Level 3')
|
|
505
|
+
expect(level4.title).toBe('Level 4')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('should handle mixed ordered and unordered in same section', () => {
|
|
509
|
+
const markdown = `# Project
|
|
510
|
+
|
|
511
|
+
- [ ] Unordered
|
|
512
|
+
1. [ ] Ordered
|
|
513
|
+
- [ ] Unordered again
|
|
514
|
+
`
|
|
515
|
+
const project = parseMarkdown(markdown)
|
|
516
|
+
// Should treat first set as parallel based on first task
|
|
517
|
+
expect(project.tasks.length).toBeGreaterThan(0)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('should handle empty lines between tasks', () => {
|
|
521
|
+
const markdown = `# Project
|
|
522
|
+
|
|
523
|
+
- [ ] Task 1
|
|
524
|
+
|
|
525
|
+
- [ ] Task 2
|
|
526
|
+
|
|
527
|
+
- [ ] Task 3
|
|
528
|
+
`
|
|
529
|
+
const project = parseMarkdown(markdown)
|
|
530
|
+
// Should still parse all tasks
|
|
531
|
+
const allTasks: string[] = []
|
|
532
|
+
function collectTasks(tasks: any[]) {
|
|
533
|
+
for (const t of tasks) {
|
|
534
|
+
if (t.__type === 'task') allTasks.push(t.title)
|
|
535
|
+
else if (t.tasks) collectTasks(t.tasks)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
collectTasks(project.tasks)
|
|
539
|
+
expect(allTasks).toContain('Task 1')
|
|
540
|
+
expect(allTasks).toContain('Task 2')
|
|
541
|
+
expect(allTasks).toContain('Task 3')
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('should handle Windows-style line endings', () => {
|
|
545
|
+
const markdown = '# Project\r\n\r\n- [ ] Task 1\r\n- [ ] Task 2\r\n'
|
|
546
|
+
const project = parseMarkdown(markdown)
|
|
547
|
+
expect(project.name).toBe('Project')
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
})
|