@@ -632,5 +632,180 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
632
632
expect ( checkpointHandler ) . not . toHaveBeenCalled ( )
633
633
} )
634
634
} )
635
+
636
+ describe ( `${ klass . name } #saveCheckpoint with allowEmpty option` , ( ) => {
637
+ it ( "creates checkpoint with allowEmpty=true even when no changes" , async ( ) => {
638
+ // No changes made, but force checkpoint creation
639
+ const result = await service . saveCheckpoint ( "Empty checkpoint" , { allowEmpty : true } )
640
+
641
+ expect ( result ) . toBeDefined ( )
642
+ expect ( result ?. commit ) . toBeTruthy ( )
643
+ expect ( typeof result ?. commit ) . toBe ( "string" )
644
+ } )
645
+
646
+ it ( "does not create checkpoint with allowEmpty=false when no changes" , async ( ) => {
647
+ const result = await service . saveCheckpoint ( "No changes checkpoint" , { allowEmpty : false } )
648
+
649
+ expect ( result ) . toBeUndefined ( )
650
+ } )
651
+
652
+ it ( "does not create checkpoint by default when no changes" , async ( ) => {
653
+ const result = await service . saveCheckpoint ( "Default behavior checkpoint" )
654
+
655
+ expect ( result ) . toBeUndefined ( )
656
+ } )
657
+
658
+ it ( "creates checkpoint with changes regardless of allowEmpty setting" , async ( ) => {
659
+ await fs . writeFile ( testFile , "Modified content for allowEmpty test" )
660
+
661
+ const resultWithAllowEmpty = await service . saveCheckpoint ( "With changes and allowEmpty" , { allowEmpty : true } )
662
+ expect ( resultWithAllowEmpty ?. commit ) . toBeTruthy ( )
663
+
664
+ await fs . writeFile ( testFile , "Another modification for allowEmpty test" )
665
+
666
+ const resultWithoutAllowEmpty = await service . saveCheckpoint ( "With changes, no allowEmpty" )
667
+ expect ( resultWithoutAllowEmpty ?. commit ) . toBeTruthy ( )
668
+ } )
669
+
670
+ it ( "emits checkpoint event for empty commits when allowEmpty=true" , async ( ) => {
671
+ const checkpointHandler = jest . fn ( )
672
+ service . on ( "checkpoint" , checkpointHandler )
673
+
674
+ const result = await service . saveCheckpoint ( "Empty checkpoint event test" , { allowEmpty : true } )
675
+
676
+ expect ( checkpointHandler ) . toHaveBeenCalledTimes ( 1 )
677
+ const eventData = checkpointHandler . mock . calls [ 0 ] [ 0 ]
678
+ expect ( eventData . type ) . toBe ( "checkpoint" )
679
+ expect ( eventData . toHash ) . toBe ( result ?. commit )
680
+ expect ( typeof eventData . duration ) . toBe ( "number" )
681
+ expect ( typeof eventData . isFirst ) . toBe ( "boolean" ) // Can be true or false depending on checkpoint history
682
+ } )
683
+
684
+ it ( "does not emit checkpoint event when no changes and allowEmpty=false" , async ( ) => {
685
+ // First, create a checkpoint to ensure we're not in the initial state
686
+ await fs . writeFile ( testFile , "Setup content" )
687
+ await service . saveCheckpoint ( "Setup checkpoint" )
688
+
689
+ // Reset the file to original state
690
+ await fs . writeFile ( testFile , "Hello, world!" )
691
+ await service . saveCheckpoint ( "Reset to original" )
692
+
693
+ // Now test with no changes and allowEmpty=false
694
+ const checkpointHandler = jest . fn ( )
695
+ service . on ( "checkpoint" , checkpointHandler )
696
+
697
+ const result = await service . saveCheckpoint ( "No changes, no event" , { allowEmpty : false } )
698
+
699
+ expect ( result ) . toBeUndefined ( )
700
+ expect ( checkpointHandler ) . not . toHaveBeenCalled ( )
701
+ } )
702
+
703
+ it ( "handles multiple empty checkpoints correctly" , async ( ) => {
704
+ const commit1 = await service . saveCheckpoint ( "First empty checkpoint" , { allowEmpty : true } )
705
+ expect ( commit1 ?. commit ) . toBeTruthy ( )
706
+
707
+ const commit2 = await service . saveCheckpoint ( "Second empty checkpoint" , { allowEmpty : true } )
708
+ expect ( commit2 ?. commit ) . toBeTruthy ( )
709
+
710
+ // Commits should be different
711
+ expect ( commit1 ?. commit ) . not . toBe ( commit2 ?. commit )
712
+ } )
713
+
714
+ it ( "logs correct message for allowEmpty option" , async ( ) => {
715
+ const logMessages : string [ ] = [ ]
716
+ const testService = await klass . create ( {
717
+ taskId : "log-test" ,
718
+ shadowDir : path . join ( tmpDir , `log-test-${ Date . now ( ) } ` ) ,
719
+ workspaceDir : service . workspaceDir ,
720
+ log : ( message : string ) => logMessages . push ( message ) ,
721
+ } )
722
+ await testService . initShadowGit ( )
723
+
724
+ await testService . saveCheckpoint ( "Test logging with allowEmpty" , { allowEmpty : true } )
725
+
726
+ const saveCheckpointLogs = logMessages . filter ( msg =>
727
+ msg . includes ( "starting checkpoint save" ) && msg . includes ( "allowEmpty: true" )
728
+ )
729
+ expect ( saveCheckpointLogs ) . toHaveLength ( 1 )
730
+
731
+ await testService . saveCheckpoint ( "Test logging without allowEmpty" )
732
+
733
+ const defaultLogs = logMessages . filter ( msg =>
734
+ msg . includes ( "starting checkpoint save" ) && msg . includes ( "allowEmpty: false" )
735
+ )
736
+ expect ( defaultLogs ) . toHaveLength ( 1 )
737
+ } )
738
+
739
+ it ( "maintains checkpoint history with empty commits" , async ( ) => {
740
+ // Create a regular checkpoint
741
+ await fs . writeFile ( testFile , "Regular change" )
742
+ const regularCommit = await service . saveCheckpoint ( "Regular checkpoint" )
743
+ expect ( regularCommit ?. commit ) . toBeTruthy ( )
744
+
745
+ // Create an empty checkpoint
746
+ const emptyCommit = await service . saveCheckpoint ( "Empty checkpoint" , { allowEmpty : true } )
747
+ expect ( emptyCommit ?. commit ) . toBeTruthy ( )
748
+
749
+ // Create another regular checkpoint
750
+ await fs . writeFile ( testFile , "Another regular change" )
751
+ const anotherCommit = await service . saveCheckpoint ( "Another regular checkpoint" )
752
+ expect ( anotherCommit ?. commit ) . toBeTruthy ( )
753
+
754
+ // Verify we can restore to the empty checkpoint
755
+ await service . restoreCheckpoint ( emptyCommit ! . commit )
756
+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Regular change" )
757
+
758
+ // Verify we can restore to other checkpoints
759
+ await service . restoreCheckpoint ( regularCommit ! . commit )
760
+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Regular change" )
761
+
762
+ await service . restoreCheckpoint ( anotherCommit ! . commit )
763
+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Another regular change" )
764
+ } )
765
+
766
+ it ( "handles getDiff correctly with empty commits" , async ( ) => {
767
+ // Create a regular checkpoint
768
+ await fs . writeFile ( testFile , "Content before empty" )
769
+ const beforeEmpty = await service . saveCheckpoint ( "Before empty" )
770
+ expect ( beforeEmpty ?. commit ) . toBeTruthy ( )
771
+
772
+ // Create an empty checkpoint
773
+ const emptyCommit = await service . saveCheckpoint ( "Empty checkpoint" , { allowEmpty : true } )
774
+ expect ( emptyCommit ?. commit ) . toBeTruthy ( )
775
+
776
+ // Get diff between regular commit and empty commit
777
+ const diff = await service . getDiff ( {
778
+ from : beforeEmpty ! . commit ,
779
+ to : emptyCommit ! . commit
780
+ } )
781
+
782
+ // Should have no differences since empty commit doesn't change anything
783
+ expect ( diff ) . toHaveLength ( 0 )
784
+ } )
785
+
786
+ it ( "works correctly in integration with new task workflow" , async ( ) => {
787
+ // Simulate the new task workflow where we force a checkpoint even with no changes
788
+ // This tests the specific use case mentioned in the git commit
789
+
790
+ // Start with a clean state (no pending changes)
791
+ const initialState = await service . saveCheckpoint ( "Check initial state" )
792
+ expect ( initialState ) . toBeUndefined ( ) // No changes, so no commit
793
+
794
+ // Force a checkpoint for new task (this is the new functionality)
795
+ const newTaskCheckpoint = await service . saveCheckpoint ( "New task checkpoint" , { allowEmpty : true } )
796
+ expect ( newTaskCheckpoint ?. commit ) . toBeTruthy ( )
797
+
798
+ // Verify the checkpoint was created and can be restored
799
+ await fs . writeFile ( testFile , "Work done in new task" )
800
+ const workCommit = await service . saveCheckpoint ( "Work in new task" )
801
+ expect ( workCommit ?. commit ) . toBeTruthy ( )
802
+
803
+ // Restore to the new task checkpoint
804
+ await service . restoreCheckpoint ( newTaskCheckpoint ! . commit )
805
+
806
+ // File should be back to original state
807
+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Hello, world!" )
808
+ } )
809
+ } )
635
810
} ,
636
811
)
0 commit comments