@@ -632,5 +632,180 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
632632 expect ( checkpointHandler ) . not . toHaveBeenCalled ( )
633633 } )
634634 } )
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+ } )
635810 } ,
636811)
0 commit comments