handoff-mcp-server 0.7.1 → 0.7.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.
@@ -1,765 +0,0 @@
1
- use serde_json::{json, Value};
2
- use tempfile::TempDir;
3
-
4
- fn send(input: &str) -> Option<Value> {
5
- let result = handoff_mcp::mcp::protocol::process_line(input)?;
6
- Some(serde_json::from_str(&result).expect("response should be valid JSON"))
7
- }
8
-
9
- fn setup_project() -> TempDir {
10
- let dir = tempfile::tempdir().expect("failed to create temp dir");
11
- let req = json!({
12
- "jsonrpc": "2.0", "id": 0,
13
- "method": "tools/call",
14
- "params": {
15
- "name": "handoff_init",
16
- "arguments": {
17
- "project_dir": dir.path().to_string_lossy(),
18
- "project_name": "test"
19
- }
20
- }
21
- });
22
- send(&req.to_string()).unwrap();
23
- dir
24
- }
25
-
26
- fn call_tool(name: &str, arguments: Value) -> Value {
27
- let req = json!({
28
- "jsonrpc": "2.0", "id": 1,
29
- "method": "tools/call",
30
- "params": { "name": name, "arguments": arguments }
31
- });
32
- send(&req.to_string()).unwrap()
33
- }
34
-
35
- fn get_text(resp: &Value) -> String {
36
- resp["result"]["content"][0]["text"]
37
- .as_str()
38
- .unwrap_or("")
39
- .to_string()
40
- }
41
-
42
- fn is_error(resp: &Value) -> bool {
43
- resp["result"]["isError"].as_bool().unwrap_or(false)
44
- }
45
-
46
- #[test]
47
- fn create_top_level_task() {
48
- let dir = setup_project();
49
- let resp = call_tool(
50
- "handoff_update_task",
51
- json!({
52
- "project_dir": dir.path().to_string_lossy(),
53
- "task": { "title": "First task" }
54
- }),
55
- );
56
-
57
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
58
- let text = get_text(&resp);
59
- assert!(text.contains("t1"));
60
- assert!(text.contains("First task"));
61
-
62
- let tasks = std::fs::read_dir(dir.path().join(".handoff/tasks"))
63
- .unwrap()
64
- .filter_map(|e| e.ok())
65
- .filter(|e| e.file_type().unwrap().is_dir())
66
- .count();
67
- assert_eq!(tasks, 1);
68
- }
69
-
70
- #[test]
71
- fn create_child_task() {
72
- let dir = setup_project();
73
- let pd = dir.path().to_string_lossy().to_string();
74
-
75
- call_tool(
76
- "handoff_update_task",
77
- json!({ "project_dir": &pd, "task": { "title": "Parent" } }),
78
- );
79
-
80
- let resp = call_tool(
81
- "handoff_update_task",
82
- json!({
83
- "project_dir": &pd,
84
- "task": { "title": "Child task" },
85
- "parent_id": "t1"
86
- }),
87
- );
88
-
89
- assert!(!is_error(&resp));
90
- let text = get_text(&resp);
91
- assert!(text.contains("t1.1"));
92
- }
93
-
94
- #[test]
95
- fn update_existing_task() {
96
- let dir = setup_project();
97
- let pd = dir.path().to_string_lossy().to_string();
98
-
99
- call_tool(
100
- "handoff_update_task",
101
- json!({ "project_dir": &pd, "task": { "title": "Original" } }),
102
- );
103
-
104
- let resp = call_tool(
105
- "handoff_update_task",
106
- json!({
107
- "project_dir": &pd,
108
- "task": {
109
- "id": "t1",
110
- "title": "Updated title",
111
- "status": "in_progress",
112
- "notes": "Working on it"
113
- }
114
- }),
115
- );
116
-
117
- assert!(!is_error(&resp));
118
- let text = get_text(&resp);
119
- assert!(text.contains("Updated title"));
120
- assert!(text.contains("in_progress"));
121
- }
122
-
123
- #[test]
124
- fn status_change_renames_file() {
125
- let dir = setup_project();
126
- let pd = dir.path().to_string_lossy().to_string();
127
-
128
- call_tool(
129
- "handoff_update_task",
130
- json!({ "project_dir": &pd, "task": { "title": "Task" } }),
131
- );
132
-
133
- call_tool(
134
- "handoff_update_task",
135
- json!({
136
- "project_dir": &pd,
137
- "task": { "id": "t1", "title": "Task", "status": "in_progress" }
138
- }),
139
- );
140
-
141
- let task_dir = dir.path().join(".handoff/tasks");
142
- let found: Vec<String> = walkdir(task_dir.to_str().unwrap());
143
- assert!(
144
- found.iter().any(|f| f.contains("_task.in_progress.json")),
145
- "expected in_progress file, found: {found:?}"
146
- );
147
- assert!(
148
- !found.iter().any(|f| f.contains("_task.todo.json")),
149
- "old status file should be gone"
150
- );
151
- }
152
-
153
- fn walkdir(dir: &str) -> Vec<String> {
154
- let mut result = Vec::new();
155
- if let Ok(entries) = std::fs::read_dir(dir) {
156
- for entry in entries.flatten() {
157
- let path = entry.path();
158
- if path.is_dir() {
159
- result.extend(walkdir(path.to_str().unwrap()));
160
- } else {
161
- result.push(path.to_string_lossy().to_string());
162
- }
163
- }
164
- }
165
- result
166
- }
167
-
168
- #[test]
169
- fn done_with_unchecked_criteria_fails() {
170
- let dir = setup_project();
171
- let pd = dir.path().to_string_lossy().to_string();
172
-
173
- call_tool(
174
- "handoff_update_task",
175
- json!({
176
- "project_dir": &pd,
177
- "task": {
178
- "title": "Task with criteria",
179
- "done_criteria": [
180
- { "item": "test passes", "checked": false }
181
- ]
182
- }
183
- }),
184
- );
185
-
186
- let resp = call_tool(
187
- "handoff_update_task",
188
- json!({
189
- "project_dir": &pd,
190
- "task": { "id": "t1", "title": "Task with criteria", "status": "done" }
191
- }),
192
- );
193
-
194
- assert!(is_error(&resp), "should fail: {}", get_text(&resp));
195
- assert!(get_text(&resp).contains("done_criteria"));
196
- }
197
-
198
- #[test]
199
- fn done_with_non_terminal_children_fails() {
200
- let dir = setup_project();
201
- let pd = dir.path().to_string_lossy().to_string();
202
-
203
- call_tool(
204
- "handoff_update_task",
205
- json!({ "project_dir": &pd, "task": { "title": "Parent" } }),
206
- );
207
-
208
- call_tool(
209
- "handoff_update_task",
210
- json!({
211
- "project_dir": &pd,
212
- "task": { "title": "Child in progress", "status": "in_progress" },
213
- "parent_id": "t1"
214
- }),
215
- );
216
-
217
- let resp = call_tool(
218
- "handoff_update_task",
219
- json!({
220
- "project_dir": &pd,
221
- "task": { "id": "t1", "title": "Parent", "status": "done" }
222
- }),
223
- );
224
-
225
- assert!(is_error(&resp));
226
- }
227
-
228
- #[test]
229
- fn done_with_all_terminal_children_succeeds() {
230
- let dir = setup_project();
231
- let pd = dir.path().to_string_lossy().to_string();
232
-
233
- call_tool(
234
- "handoff_update_task",
235
- json!({ "project_dir": &pd, "task": { "title": "Parent" } }),
236
- );
237
-
238
- call_tool(
239
- "handoff_update_task",
240
- json!({
241
- "project_dir": &pd,
242
- "task": { "title": "Child done", "status": "done" },
243
- "parent_id": "t1"
244
- }),
245
- );
246
-
247
- let resp = call_tool(
248
- "handoff_update_task",
249
- json!({
250
- "project_dir": &pd,
251
- "task": { "id": "t1", "title": "Parent", "status": "done" }
252
- }),
253
- );
254
-
255
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
256
- }
257
-
258
- #[test]
259
- fn move_task_to_new_parent() {
260
- let dir = setup_project();
261
- let pd = dir.path().to_string_lossy().to_string();
262
-
263
- call_tool(
264
- "handoff_update_task",
265
- json!({ "project_dir": &pd, "task": { "title": "Task A" } }),
266
- );
267
- call_tool(
268
- "handoff_update_task",
269
- json!({ "project_dir": &pd, "task": { "title": "Task B" } }),
270
- );
271
-
272
- let resp = call_tool(
273
- "handoff_update_task",
274
- json!({
275
- "project_dir": &pd,
276
- "task": { "id": "t1", "title": "Task A" },
277
- "move_to": "t2"
278
- }),
279
- );
280
-
281
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
282
- assert!(get_text(&resp).contains("Moved"));
283
-
284
- let t2_children: Vec<_> = std::fs::read_dir(
285
- dir.path()
286
- .join(".handoff/tasks")
287
- .read_dir()
288
- .unwrap()
289
- .find(|e| {
290
- e.as_ref()
291
- .unwrap()
292
- .file_name()
293
- .to_string_lossy()
294
- .starts_with("t2-")
295
- })
296
- .unwrap()
297
- .unwrap()
298
- .path(),
299
- )
300
- .unwrap()
301
- .filter_map(|e| e.ok())
302
- .filter(|e| e.file_type().unwrap().is_dir())
303
- .collect();
304
-
305
- assert!(!t2_children.is_empty(), "t1 should be moved under t2");
306
- }
307
-
308
- #[test]
309
- fn list_tasks_returns_tree() {
310
- let dir = setup_project();
311
- let pd = dir.path().to_string_lossy().to_string();
312
-
313
- call_tool(
314
- "handoff_update_task",
315
- json!({ "project_dir": &pd, "task": { "title": "Task 1" } }),
316
- );
317
- call_tool(
318
- "handoff_update_task",
319
- json!({ "project_dir": &pd, "task": { "title": "Task 2", "status": "in_progress" } }),
320
- );
321
-
322
- let resp = call_tool("handoff_list_tasks", json!({ "project_dir": &pd }));
323
-
324
- assert!(!is_error(&resp));
325
- let text = get_text(&resp);
326
- let parsed: Value = serde_json::from_str(&text).unwrap();
327
-
328
- assert_eq!(parsed["task_summary"]["total"], 2);
329
- assert!(parsed["task_tree"].as_array().unwrap().len() >= 2);
330
- }
331
-
332
- #[test]
333
- fn list_tasks_with_status_filter() {
334
- let dir = setup_project();
335
- let pd = dir.path().to_string_lossy().to_string();
336
-
337
- call_tool(
338
- "handoff_update_task",
339
- json!({ "project_dir": &pd, "task": { "title": "Todo task" } }),
340
- );
341
- call_tool(
342
- "handoff_update_task",
343
- json!({ "project_dir": &pd, "task": { "title": "Active task", "status": "in_progress" } }),
344
- );
345
-
346
- let resp = call_tool(
347
- "handoff_list_tasks",
348
- json!({ "project_dir": &pd, "status_filter": "in_progress" }),
349
- );
350
-
351
- let text = get_text(&resp);
352
- let parsed: Value = serde_json::from_str(&text).unwrap();
353
- let tree = parsed["task_tree"].as_array().unwrap();
354
-
355
- assert!(tree.iter().all(|t| t["status"] == "in_progress"));
356
- }
357
-
358
- #[test]
359
- fn list_tasks_uninitialized_project_returns_error() {
360
- let dir = tempfile::tempdir().unwrap();
361
- let resp = call_tool(
362
- "handoff_list_tasks",
363
- json!({ "project_dir": dir.path().to_string_lossy() }),
364
- );
365
- assert!(is_error(&resp));
366
- }
367
-
368
- #[test]
369
- fn hierarchical_id_numbering() {
370
- let dir = setup_project();
371
- let pd = dir.path().to_string_lossy().to_string();
372
-
373
- call_tool(
374
- "handoff_update_task",
375
- json!({ "project_dir": &pd, "task": { "title": "T1" } }),
376
- );
377
- call_tool(
378
- "handoff_update_task",
379
- json!({ "project_dir": &pd, "task": { "title": "T2" } }),
380
- );
381
- call_tool(
382
- "handoff_update_task",
383
- json!({ "project_dir": &pd, "task": { "title": "T3" } }),
384
- );
385
-
386
- let resp = call_tool(
387
- "handoff_update_task",
388
- json!({
389
- "project_dir": &pd,
390
- "task": { "title": "Child of T2" },
391
- "parent_id": "t2"
392
- }),
393
- );
394
-
395
- let text = get_text(&resp);
396
- assert!(text.contains("t2.1"), "should be t2.1, got: {text}");
397
-
398
- let resp2 = call_tool(
399
- "handoff_update_task",
400
- json!({
401
- "project_dir": &pd,
402
- "task": { "title": "Second child of T2" },
403
- "parent_id": "t2"
404
- }),
405
- );
406
- let text2 = get_text(&resp2);
407
- assert!(text2.contains("t2.2"), "should be t2.2, got: {text2}");
408
- }
409
-
410
- #[test]
411
- fn invalid_priority_rejected_on_create() {
412
- let dir = setup_project();
413
- let resp = call_tool(
414
- "handoff_update_task",
415
- json!({
416
- "project_dir": dir.path().to_string_lossy(),
417
- "task": { "title": "Bad priority", "priority": "critical" }
418
- }),
419
- );
420
-
421
- assert!(is_error(&resp), "should reject invalid priority");
422
- assert!(get_text(&resp).contains("Invalid priority"));
423
- }
424
-
425
- #[test]
426
- fn valid_priority_accepted_on_create() {
427
- let dir = setup_project();
428
- let resp = call_tool(
429
- "handoff_update_task",
430
- json!({
431
- "project_dir": dir.path().to_string_lossy(),
432
- "task": { "title": "Good priority", "priority": "high" }
433
- }),
434
- );
435
-
436
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
437
- }
438
-
439
- #[test]
440
- fn null_priority_accepted_on_create() {
441
- let dir = setup_project();
442
- let resp = call_tool(
443
- "handoff_update_task",
444
- json!({
445
- "project_dir": dir.path().to_string_lossy(),
446
- "task": { "title": "No priority" }
447
- }),
448
- );
449
-
450
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
451
- }
452
-
453
- #[test]
454
- fn invalid_priority_rejected_on_update() {
455
- let dir = setup_project();
456
- let pd = dir.path().to_string_lossy().to_string();
457
-
458
- call_tool(
459
- "handoff_update_task",
460
- json!({ "project_dir": &pd, "task": { "title": "Task" } }),
461
- );
462
-
463
- let resp = call_tool(
464
- "handoff_update_task",
465
- json!({
466
- "project_dir": &pd,
467
- "task": { "id": "t1", "title": "Task", "priority": "urgent" }
468
- }),
469
- );
470
-
471
- assert!(is_error(&resp), "should reject invalid priority on update");
472
- assert!(get_text(&resp).contains("Invalid priority"));
473
- }
474
-
475
- #[test]
476
- fn update_without_title_works() {
477
- let dir = setup_project();
478
- let pd = dir.path().to_string_lossy().to_string();
479
-
480
- call_tool(
481
- "handoff_update_task",
482
- json!({ "project_dir": &pd, "task": { "title": "Original title" } }),
483
- );
484
-
485
- let resp = call_tool(
486
- "handoff_update_task",
487
- json!({
488
- "project_dir": &pd,
489
- "task": { "id": "t1", "status": "in_progress" }
490
- }),
491
- );
492
-
493
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
494
- let text = get_text(&resp);
495
- assert!(text.contains("Original title"));
496
- assert!(text.contains("in_progress"));
497
- }
498
-
499
- #[test]
500
- fn create_without_title_fails() {
501
- let dir = setup_project();
502
- let resp = call_tool(
503
- "handoff_update_task",
504
- json!({
505
- "project_dir": dir.path().to_string_lossy(),
506
- "task": { "status": "todo" }
507
- }),
508
- );
509
-
510
- assert!(is_error(&resp), "should fail without title for new task");
511
- assert!(get_text(&resp).contains("title"));
512
- }
513
-
514
- // --- get_task tests ---
515
-
516
- #[test]
517
- fn get_task_returns_full_details() {
518
- let dir = setup_project();
519
- let pd = dir.path().to_string_lossy().to_string();
520
-
521
- call_tool(
522
- "handoff_update_task",
523
- json!({
524
- "project_dir": &pd,
525
- "task": {
526
- "title": "Detailed task",
527
- "status": "in_progress",
528
- "notes": "Some important notes",
529
- "priority": "high",
530
- "labels": ["bug", "urgent"],
531
- "links": ["https://example.com/issue/1"],
532
- "done_criteria": [
533
- { "item": "Fix the bug", "checked": false },
534
- { "item": "Add tests", "checked": true }
535
- ]
536
- }
537
- }),
538
- );
539
-
540
- let resp = call_tool(
541
- "handoff_get_task",
542
- json!({ "project_dir": &pd, "task_id": "t1" }),
543
- );
544
-
545
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
546
- let parsed: Value = serde_json::from_str(&get_text(&resp)).unwrap();
547
-
548
- assert_eq!(parsed["id"], "t1");
549
- assert_eq!(parsed["title"], "Detailed task");
550
- assert_eq!(parsed["status"], "in_progress");
551
- assert_eq!(parsed["notes"], "Some important notes");
552
- assert_eq!(parsed["priority"], "high");
553
- assert_eq!(parsed["labels"].as_array().unwrap().len(), 2);
554
- assert_eq!(parsed["links"].as_array().unwrap().len(), 1);
555
- assert_eq!(parsed["done_criteria"].as_array().unwrap().len(), 2);
556
- assert!(parsed["created_at"].is_string());
557
- }
558
-
559
- #[test]
560
- fn get_task_not_found() {
561
- let dir = setup_project();
562
- let resp = call_tool(
563
- "handoff_get_task",
564
- json!({ "project_dir": dir.path().to_string_lossy(), "task_id": "t999" }),
565
- );
566
- assert!(is_error(&resp));
567
- assert!(get_text(&resp).contains("not found"));
568
- }
569
-
570
- #[test]
571
- fn get_task_missing_id() {
572
- let dir = setup_project();
573
- let resp = call_tool(
574
- "handoff_get_task",
575
- json!({ "project_dir": dir.path().to_string_lossy() }),
576
- );
577
- assert!(is_error(&resp));
578
- assert!(get_text(&resp).contains("task_id"));
579
- }
580
-
581
- #[test]
582
- fn get_task_nested() {
583
- let dir = setup_project();
584
- let pd = dir.path().to_string_lossy().to_string();
585
-
586
- call_tool(
587
- "handoff_update_task",
588
- json!({ "project_dir": &pd, "task": { "title": "Parent" } }),
589
- );
590
- call_tool(
591
- "handoff_update_task",
592
- json!({
593
- "project_dir": &pd,
594
- "task": { "title": "Nested child", "notes": "Deep detail" },
595
- "parent_id": "t1"
596
- }),
597
- );
598
-
599
- let resp = call_tool(
600
- "handoff_get_task",
601
- json!({ "project_dir": &pd, "task_id": "t1.1" }),
602
- );
603
-
604
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
605
- let parsed: Value = serde_json::from_str(&get_text(&resp)).unwrap();
606
- assert_eq!(parsed["id"], "t1.1");
607
- assert_eq!(parsed["notes"], "Deep detail");
608
- }
609
-
610
- // --- check_criterion tests ---
611
-
612
- #[test]
613
- fn check_criterion_toggles_item() {
614
- let dir = setup_project();
615
- let pd = dir.path().to_string_lossy().to_string();
616
-
617
- call_tool(
618
- "handoff_update_task",
619
- json!({
620
- "project_dir": &pd,
621
- "task": {
622
- "title": "Task with criteria",
623
- "done_criteria": [
624
- { "item": "Step 1", "checked": false },
625
- { "item": "Step 2", "checked": false },
626
- { "item": "Step 3", "checked": false }
627
- ]
628
- }
629
- }),
630
- );
631
-
632
- let resp = call_tool(
633
- "handoff_check_criterion",
634
- json!({ "project_dir": &pd, "task_id": "t1", "criterion_index": 1, "checked": true }),
635
- );
636
-
637
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
638
- let parsed: Value = serde_json::from_str(&get_text(&resp)).unwrap();
639
-
640
- assert_eq!(parsed["task_id"], "t1");
641
- assert_eq!(parsed["criterion_index"], 1);
642
- assert_eq!(parsed["item"], "Step 2");
643
- assert_eq!(parsed["checked"], true);
644
- assert_eq!(parsed["done_criteria_summary"]["total"], 3);
645
- assert_eq!(parsed["done_criteria_summary"]["checked"], 1);
646
- }
647
-
648
- #[test]
649
- fn check_criterion_uncheck() {
650
- let dir = setup_project();
651
- let pd = dir.path().to_string_lossy().to_string();
652
-
653
- call_tool(
654
- "handoff_update_task",
655
- json!({
656
- "project_dir": &pd,
657
- "task": {
658
- "title": "Task",
659
- "done_criteria": [
660
- { "item": "Step 1", "checked": true }
661
- ]
662
- }
663
- }),
664
- );
665
-
666
- let resp = call_tool(
667
- "handoff_check_criterion",
668
- json!({ "project_dir": &pd, "task_id": "t1", "criterion_index": 0, "checked": false }),
669
- );
670
-
671
- assert!(!is_error(&resp), "error: {}", get_text(&resp));
672
- let parsed: Value = serde_json::from_str(&get_text(&resp)).unwrap();
673
- assert_eq!(parsed["checked"], false);
674
- assert_eq!(parsed["done_criteria_summary"]["checked"], 0);
675
- }
676
-
677
- #[test]
678
- fn check_criterion_out_of_range() {
679
- let dir = setup_project();
680
- let pd = dir.path().to_string_lossy().to_string();
681
-
682
- call_tool(
683
- "handoff_update_task",
684
- json!({
685
- "project_dir": &pd,
686
- "task": {
687
- "title": "Task",
688
- "done_criteria": [{ "item": "Only one", "checked": false }]
689
- }
690
- }),
691
- );
692
-
693
- let resp = call_tool(
694
- "handoff_check_criterion",
695
- json!({ "project_dir": &pd, "task_id": "t1", "criterion_index": 5, "checked": true }),
696
- );
697
-
698
- assert!(is_error(&resp));
699
- assert!(get_text(&resp).contains("out of range"));
700
- }
701
-
702
- #[test]
703
- fn check_criterion_task_not_found() {
704
- let dir = setup_project();
705
- let resp = call_tool(
706
- "handoff_check_criterion",
707
- json!({ "project_dir": dir.path().to_string_lossy(), "task_id": "t999", "criterion_index": 0, "checked": true }),
708
- );
709
- assert!(is_error(&resp));
710
- assert!(get_text(&resp).contains("not found"));
711
- }
712
-
713
- #[test]
714
- fn check_criterion_no_criteria() {
715
- let dir = setup_project();
716
- let pd = dir.path().to_string_lossy().to_string();
717
-
718
- call_tool(
719
- "handoff_update_task",
720
- json!({ "project_dir": &pd, "task": { "title": "No criteria" } }),
721
- );
722
-
723
- let resp = call_tool(
724
- "handoff_check_criterion",
725
- json!({ "project_dir": &pd, "task_id": "t1", "criterion_index": 0, "checked": true }),
726
- );
727
-
728
- assert!(is_error(&resp));
729
- assert!(get_text(&resp).contains("out of range"));
730
- }
731
-
732
- #[test]
733
- fn check_criterion_persists() {
734
- let dir = setup_project();
735
- let pd = dir.path().to_string_lossy().to_string();
736
-
737
- call_tool(
738
- "handoff_update_task",
739
- json!({
740
- "project_dir": &pd,
741
- "task": {
742
- "title": "Persist test",
743
- "done_criteria": [
744
- { "item": "A", "checked": false },
745
- { "item": "B", "checked": false }
746
- ]
747
- }
748
- }),
749
- );
750
-
751
- call_tool(
752
- "handoff_check_criterion",
753
- json!({ "project_dir": &pd, "task_id": "t1", "criterion_index": 0, "checked": true }),
754
- );
755
-
756
- let resp = call_tool(
757
- "handoff_get_task",
758
- json!({ "project_dir": &pd, "task_id": "t1" }),
759
- );
760
-
761
- let parsed: Value = serde_json::from_str(&get_text(&resp)).unwrap();
762
- let criteria = parsed["done_criteria"].as_array().unwrap();
763
- assert_eq!(criteria[0]["checked"], true);
764
- assert_eq!(criteria[1]["checked"], false);
765
- }