@@ -822,5 +822,137 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
822822 expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Hello, world!" )
823823 } )
824824 } )
825+
826+ describe ( `${ klass . name } #restoreCheckpoint safety` , ( ) => {
827+ it ( "does not use dangerous git clean command" , async ( ) => {
828+ // This test verifies that we don't use git clean -d -f which would
829+ // delete untracked files and potentially cause data loss
830+
831+ // Create initial checkpoint
832+ await fs . writeFile ( testFile , "Initial content" )
833+ const commit1 = await service . saveCheckpoint ( "Initial checkpoint" )
834+ expect ( commit1 ?. commit ) . toBeTruthy ( )
835+
836+ // Make changes and create another checkpoint
837+ await fs . writeFile ( testFile , "Modified content" )
838+ const commit2 = await service . saveCheckpoint ( "Second checkpoint" )
839+ expect ( commit2 ?. commit ) . toBeTruthy ( )
840+
841+ // Create a spy to monitor git commands
842+ const gitSpy = vitest . spyOn ( service [ "git" ] ! , "clean" )
843+
844+ // Restore to first checkpoint
845+ await service . restoreCheckpoint ( commit1 ! . commit )
846+
847+ // Verify tracked file was restored
848+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Initial content" )
849+
850+ // Verify that git clean was NOT called
851+ expect ( gitSpy ) . not . toHaveBeenCalled ( )
852+
853+ gitSpy . mockRestore ( )
854+ } )
855+
856+ it ( "validates worktree configuration before restoration" , async ( ) => {
857+ // Create a checkpoint
858+ await fs . writeFile ( testFile , "Test content" )
859+ const commit = await service . saveCheckpoint ( "Test checkpoint" )
860+ expect ( commit ?. commit ) . toBeTruthy ( )
861+
862+ // Create a new service instance with corrupted worktree
863+ const corruptedShadowDir = path . join ( tmpDir , `corrupted-${ Date . now ( ) } ` )
864+ const corruptedService = await klass . create ( {
865+ taskId : "corrupted-test" ,
866+ shadowDir : corruptedShadowDir ,
867+ workspaceDir : service . workspaceDir ,
868+ log : ( ) => { } ,
869+ } )
870+ await corruptedService . initShadowGit ( )
871+
872+ // Manually corrupt the worktree configuration by modifying the internal state
873+ // This simulates a corrupted git config without actually setting an invalid path
874+ corruptedService [ "shadowGitConfigWorktree" ] = "/some/wrong/path"
875+
876+ // Attempt to restore should fail with worktree mismatch error
877+ await expect ( corruptedService . restoreCheckpoint ( commit ! . commit ) ) . rejects . toThrow (
878+ "Worktree mismatch detected" ,
879+ )
880+
881+ // Using the original service (with correct worktree) should succeed
882+ await service . restoreCheckpoint ( commit ! . commit )
883+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Test content" )
884+
885+ // Clean up
886+ await fs . rm ( corruptedShadowDir , { recursive : true , force : true } )
887+ } )
888+
889+ it ( "validates commit hash before attempting restoration" , async ( ) => {
890+ // Try to restore with an invalid commit hash
891+ const invalidHash = "invalid-commit-hash-12345"
892+
893+ await expect ( service . restoreCheckpoint ( invalidHash ) ) . rejects . toThrow (
894+ "Invalid commit hash: " + invalidHash ,
895+ )
896+ } )
897+
898+ it ( "safely restores without data loss" , async ( ) => {
899+ // Create initial checkpoint
900+ await fs . writeFile ( testFile , "Initial content" )
901+ const commit1 = await service . saveCheckpoint ( "Initial checkpoint" )
902+ expect ( commit1 ?. commit ) . toBeTruthy ( )
903+
904+ // Make changes and save another checkpoint
905+ await fs . writeFile ( testFile , "Modified content" )
906+ const newFile = path . join ( service . workspaceDir , "new-file.txt" )
907+ await fs . writeFile ( newFile , "New file content" )
908+ const commit2 = await service . saveCheckpoint ( "Second checkpoint" )
909+ expect ( commit2 ?. commit ) . toBeTruthy ( )
910+
911+ // Restore to initial checkpoint
912+ await service . restoreCheckpoint ( commit1 ! . commit )
913+
914+ // Verify restoration worked correctly
915+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Initial content" )
916+
917+ // The new file should not exist in the first checkpoint
918+ await expect ( fs . readFile ( newFile , "utf-8" ) ) . rejects . toThrow ( )
919+ } )
920+
921+ it ( "handles restoration when no changes need to be stashed" , async ( ) => {
922+ // Create and immediately restore a checkpoint (no changes to stash)
923+ await fs . writeFile ( testFile , "Test content" )
924+ const commit = await service . saveCheckpoint ( "Test checkpoint" )
925+ expect ( commit ?. commit ) . toBeTruthy ( )
926+
927+ // Restore immediately without making any changes
928+ await expect ( service . restoreCheckpoint ( commit ! . commit ) ) . resolves . not . toThrow ( )
929+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Test content" )
930+ } )
931+
932+ it ( "properly removes files that should not exist in restored checkpoint" , async ( ) => {
933+ // Create initial checkpoint
934+ await fs . writeFile ( testFile , "Initial content" )
935+ const commit1 = await service . saveCheckpoint ( "Initial checkpoint" )
936+ expect ( commit1 ?. commit ) . toBeTruthy ( )
937+
938+ // Add a new tracked file
939+ const trackedFile = path . join ( service . workspaceDir , "tracked-file.txt" )
940+ await fs . writeFile ( trackedFile , "This file will be tracked" )
941+ const commit2 = await service . saveCheckpoint ( "Added tracked file" )
942+ expect ( commit2 ?. commit ) . toBeTruthy ( )
943+
944+ // Verify file exists
945+ expect ( await fs . readFile ( trackedFile , "utf-8" ) ) . toBe ( "This file will be tracked" )
946+
947+ // Restore to initial checkpoint (before the file existed)
948+ await service . restoreCheckpoint ( commit1 ! . commit )
949+
950+ // Verify the tracked file was removed
951+ await expect ( fs . readFile ( trackedFile , "utf-8" ) ) . rejects . toThrow ( )
952+
953+ // Verify original file is still correct
954+ expect ( await fs . readFile ( testFile , "utf-8" ) ) . toBe ( "Initial content" )
955+ } )
956+ } )
825957 } ,
826958)
0 commit comments