handoff-mcp-server 0.2.0 → 0.3.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.
@@ -406,3 +406,360 @@ fn hierarchical_id_numbering() {
406
406
  let text2 = get_text(&resp2);
407
407
  assert!(text2.contains("t2.2"), "should be t2.2, got: {text2}");
408
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
+ }