@@ -671,4 +671,343 @@ describe('ProjectService', () => {
671
671
expect ( duplicateMetadata . namespace ) . toBe ( originalMetadata . namespace )
672
672
} )
673
673
} )
674
+
675
+ describe ( 'renameProject' , ( ) => {
676
+ test ( 'should successfully rename a project when language server is not running' , async ( ) => {
677
+ // Create a project
678
+ const originalName = 'OriginalProjectName'
679
+ const createResult = await projectService . createProject ( originalName , projectsDirectory )
680
+
681
+ // Add some content to verify it's preserved after rename
682
+ const testFilePath = path . join ( createResult . projectPath , 'src' , 'Main.enso' )
683
+ await fs . mkdir ( path . dirname ( testFilePath ) , { recursive : true } )
684
+ await fs . writeFile ( testFilePath , 'main = "Hello from project"' )
685
+
686
+ const newName = 'RenamedProjectName'
687
+
688
+ // Rename the project
689
+ await projectService . renameProject ( createResult . projectId , newName , projectsDirectory )
690
+
691
+ // Verify the directory was renamed
692
+ const oldDirectoryPath = createResult . projectPath
693
+ const newDirectoryPath = path . join ( path . dirname ( oldDirectoryPath ) , 'RenamedProjectName' )
694
+
695
+ // Verify metadata is preserved
696
+ const metadataPath = path . join ( newDirectoryPath , '.enso' , 'project.json' )
697
+ const metadataContent = await fs . readFile ( metadataPath , 'utf-8' )
698
+ const metadata = JSON . parse ( metadataContent )
699
+ expect ( metadata . id ) . toBe ( createResult . projectId )
700
+
701
+ // Verify package name is updated
702
+ const packagePath = path . join ( newDirectoryPath , 'package.yaml' )
703
+ const packageContent = await fs . readFile ( packagePath , 'utf-8' )
704
+ expect ( packageContent ) . contain ( newName )
705
+
706
+ const oldDirExists = await fs
707
+ . access ( oldDirectoryPath )
708
+ . then ( ( ) => true )
709
+ . catch ( ( ) => false )
710
+ const newDirExists = await fs
711
+ . access ( newDirectoryPath )
712
+ . then ( ( ) => true )
713
+ . catch ( ( ) => false )
714
+
715
+ expect ( oldDirExists ) . toBe ( false )
716
+ expect ( newDirExists ) . toBe ( true )
717
+
718
+ // Verify content was preserved
719
+ const renamedTestFilePath = path . join ( newDirectoryPath , 'src' , 'Main.enso' )
720
+ const content = await fs . readFile ( renamedTestFilePath , 'utf-8' )
721
+ expect ( content ) . toBe ( 'main = "Hello from project"' )
722
+ } )
723
+
724
+ test (
725
+ 'should defer directory rename when language server is running' ,
726
+ async ( ) => {
727
+ // Create a project
728
+ const originalName = 'RunningProjectToRename'
729
+ const createResult = await projectService . createProject ( originalName , projectsDirectory )
730
+
731
+ // Open the project to start the language server
732
+ await projectService . openProject ( createResult . projectId , projectsDirectory )
733
+
734
+ // Rename the project while it's running
735
+ const newName = 'RenamedRunningProject'
736
+ try {
737
+ await projectService . renameProject ( createResult . projectId , newName , projectsDirectory )
738
+ } catch { /* Expected error for uninitialized test project */ }
739
+
740
+ const oldDirectoryPath = createResult . projectPath
741
+ // Verify metadata is preserved
742
+ const metadataPath = path . join ( oldDirectoryPath , '.enso' , 'project.json' )
743
+ const metadataContent = await fs . readFile ( metadataPath , 'utf-8' )
744
+ const metadata = JSON . parse ( metadataContent )
745
+ expect ( metadata . id ) . toBe ( createResult . projectId )
746
+
747
+ // Verify package name is updated
748
+ const packagePath = path . join ( oldDirectoryPath , 'package.yaml' )
749
+ const packageContent = await fs . readFile ( packagePath , 'utf-8' )
750
+ expect ( packageContent ) . contain ( newName )
751
+
752
+ // Verify directory has NOT been renamed yet (deferred)
753
+ const oldDirExists = await fs
754
+ . access ( createResult . projectPath )
755
+ . then ( ( ) => true )
756
+ . catch ( ( ) => false )
757
+ expect ( oldDirExists ) . toBe ( true )
758
+
759
+ // Close the project to trigger the deferred rename
760
+ await projectService . closeProject ( createResult . projectId )
761
+
762
+ // Now verify the directory was renamed after closing
763
+ const newDirectoryPath = path . join ( path . dirname ( createResult . projectPath ) , newName )
764
+ const oldDirExistsAfter = await fs
765
+ . access ( createResult . projectPath )
766
+ . then ( ( ) => true )
767
+ . catch ( ( ) => false )
768
+ const newDirExistsAfter = await fs
769
+ . access ( newDirectoryPath )
770
+ . then ( ( ) => true )
771
+ . catch ( ( ) => false )
772
+
773
+ expect ( oldDirExistsAfter ) . toBe ( false )
774
+ expect ( newDirExistsAfter ) . toBe ( true )
775
+ } ,
776
+ LANGUAGE_SERVER_TEST_TIMEOUT ,
777
+ )
778
+
779
+ test ( 'should fail when renaming to an existing project name' , async ( ) => {
780
+ // Create two projects
781
+ const _project1 = await projectService . createProject ( 'Project1' , projectsDirectory )
782
+ const project2 = await projectService . createProject ( 'Project2' , projectsDirectory )
783
+
784
+ // Try to rename project2 to project1's name
785
+ await expect (
786
+ projectService . renameProject ( project2 . projectId , 'Project1' , projectsDirectory ) ,
787
+ ) . rejects . toThrow ( "Project with name 'Project1' already exists." )
788
+ } )
789
+
790
+ test ( 'should fail when renaming non-existent project' , async ( ) => {
791
+ const nonExistentId = crypto . randomUUID ( ) as UUID
792
+
793
+ await expect (
794
+ projectService . renameProject ( nonExistentId , 'NewName' , projectsDirectory ) ,
795
+ ) . rejects . toThrow ( `Project not found: ${ nonExistentId } ` )
796
+ } )
797
+
798
+ test ( 'should reject empty new name' , async ( ) => {
799
+ // Create a project
800
+ const createResult = await projectService . createProject ( 'ProjectToRename' , projectsDirectory )
801
+
802
+ // Try to rename with empty name
803
+ await expect (
804
+ projectService . renameProject ( createResult . projectId , '' , projectsDirectory ) ,
805
+ ) . rejects . toThrow ( 'Project name cannot be empty' )
806
+
807
+ await expect (
808
+ projectService . renameProject ( createResult . projectId , ' ' , projectsDirectory ) ,
809
+ ) . rejects . toThrow ( 'Project name cannot be empty' )
810
+ } )
811
+
812
+ test ( 'should handle special characters in new name' , async ( ) => {
813
+ // Create a project
814
+ const originalName = 'SimpleProject'
815
+ const createResult = await projectService . createProject ( originalName , projectsDirectory )
816
+
817
+ const newName = 'Project #1 & Special'
818
+
819
+ // Rename the project
820
+ await projectService . renameProject ( createResult . projectId , newName , projectsDirectory )
821
+
822
+ const newDirectoryPath = path . join ( path . dirname ( createResult . projectPath ) , 'Project1Special' )
823
+ // Verify metadata is preserved
824
+ const metadataPath = path . join ( newDirectoryPath , '.enso' , 'project.json' )
825
+ const metadataContent = await fs . readFile ( metadataPath , 'utf-8' )
826
+ const metadata = JSON . parse ( metadataContent )
827
+ expect ( metadata . id ) . toBe ( createResult . projectId )
828
+
829
+ // Verify package name is updated
830
+ const packagePath = path . join ( newDirectoryPath , 'package.yaml' )
831
+ const packageContent = await fs . readFile ( packagePath , 'utf-8' )
832
+ expect ( packageContent ) . contain ( newName )
833
+
834
+ // Verify the directory was renamed with normalized name
835
+ const newDirExists = await fs
836
+ . access ( newDirectoryPath )
837
+ . then ( ( ) => true )
838
+ . catch ( ( ) => false )
839
+ expect ( newDirExists ) . toBe ( true )
840
+ } )
841
+
842
+ test ( 'should preserve project content after rename' , async ( ) => {
843
+ // Create a project with content
844
+ const originalName = 'ProjectWithContent'
845
+ const createResult = await projectService . createProject ( originalName , projectsDirectory )
846
+
847
+ // Add various files and directories
848
+ const srcDir = path . join ( createResult . projectPath , 'src' )
849
+ const testDir = path . join ( createResult . projectPath , 'test' )
850
+ await fs . mkdir ( srcDir , { recursive : true } )
851
+ await fs . mkdir ( testDir , { recursive : true } )
852
+ await fs . writeFile ( path . join ( srcDir , 'Main.enso' ) , 'main = "Main content"' )
853
+ await fs . writeFile ( path . join ( srcDir , 'Utils.enso' ) , 'utils = "Utils content"' )
854
+ await fs . writeFile ( path . join ( testDir , 'Test.enso' ) , 'test = "Test content"' )
855
+
856
+ const newName = 'RenamedProjectWithContent'
857
+
858
+ // Rename the project
859
+ await projectService . renameProject ( createResult . projectId , newName , projectsDirectory )
860
+
861
+ // Verify all content is preserved
862
+ const newDirectoryPath = path . join (
863
+ path . dirname ( createResult . projectPath ) ,
864
+ 'RenamedProjectWithContent' ,
865
+ )
866
+ const mainContent = await fs . readFile (
867
+ path . join ( newDirectoryPath , 'src' , 'Main.enso' ) ,
868
+ 'utf-8' ,
869
+ )
870
+ const utilsContent = await fs . readFile (
871
+ path . join ( newDirectoryPath , 'src' , 'Utils.enso' ) ,
872
+ 'utf-8' ,
873
+ )
874
+ const testContent = await fs . readFile (
875
+ path . join ( newDirectoryPath , 'test' , 'Test.enso' ) ,
876
+ 'utf-8' ,
877
+ )
878
+
879
+ expect ( mainContent ) . toBe ( 'main = "Main content"' )
880
+ expect ( utilsContent ) . toBe ( 'utils = "Utils content"' )
881
+ expect ( testContent ) . toBe ( 'test = "Test content"' )
882
+ } )
883
+
884
+ test (
885
+ 'should allow renaming an opened project multiple times' ,
886
+ async ( ) => {
887
+ // Create a project
888
+ const originalName = 'ProjectToRenameMultipleTimes'
889
+ const createResult = await projectService . createProject ( originalName , projectsDirectory )
890
+
891
+ // Open the project to start the language server
892
+ await projectService . openProject ( createResult . projectId , projectsDirectory )
893
+
894
+ // First rename while it's running
895
+ const firstName = 'FirstRename'
896
+ try {
897
+ await projectService . renameProject ( createResult . projectId , firstName , projectsDirectory )
898
+ } catch { /* Expected error for uninitialized test project */ }
899
+
900
+ // Verify first rename was applied to package.yaml
901
+ const packagePath1 = path . join ( createResult . projectPath , 'package.yaml' )
902
+ const packageContent1 = await fs . readFile ( packagePath1 , 'utf-8' )
903
+ expect ( packageContent1 ) . contain ( firstName )
904
+
905
+ // Second rename while still running
906
+ const secondName = 'SecondRename'
907
+ try {
908
+ await projectService . renameProject ( createResult . projectId , secondName , projectsDirectory )
909
+ } catch { /* Expected error for uninitialized test project */ }
910
+
911
+ // Verify second rename was applied
912
+ const packageContent2 = await fs . readFile ( packagePath1 , 'utf-8' )
913
+ expect ( packageContent2 ) . contain ( secondName )
914
+ expect ( packageContent2 ) . not . contain ( firstName )
915
+
916
+ // Third rename while still running
917
+ const thirdName = 'ThirdRename'
918
+ try {
919
+ await projectService . renameProject ( createResult . projectId , thirdName , projectsDirectory )
920
+ } catch { /* Expected error for uninitialized test project */ }
921
+
922
+ // Verify third rename was applied
923
+ const packageContent3 = await fs . readFile ( packagePath1 , 'utf-8' )
924
+ expect ( packageContent3 ) . contain ( thirdName )
925
+ expect ( packageContent3 ) . not . contain ( secondName )
926
+
927
+ // Close the project to trigger the final deferred rename
928
+ await projectService . closeProject ( createResult . projectId )
929
+
930
+ // Verify the directory was renamed to the final name
931
+ const finalDirectoryPath = path . join ( path . dirname ( createResult . projectPath ) , thirdName )
932
+ const finalDirExists = await fs
933
+ . access ( finalDirectoryPath )
934
+ . then ( ( ) => true )
935
+ . catch ( ( ) => false )
936
+ expect ( finalDirExists ) . toBe ( true )
937
+
938
+ // Verify the original directory no longer exists
939
+ const originalDirExists = await fs
940
+ . access ( createResult . projectPath )
941
+ . then ( ( ) => true )
942
+ . catch ( ( ) => false )
943
+ expect ( originalDirExists ) . toBe ( false )
944
+
945
+ // Verify metadata is preserved with correct ID
946
+ const metadataPath = path . join ( finalDirectoryPath , '.enso' , 'project.json' )
947
+ const metadataContent = await fs . readFile ( metadataPath , 'utf-8' )
948
+ const metadata = JSON . parse ( metadataContent )
949
+ expect ( metadata . id ) . toBe ( createResult . projectId )
950
+ } ,
951
+ LANGUAGE_SERVER_TEST_TIMEOUT ,
952
+ )
953
+
954
+ test (
955
+ 'should handle renaming multiple running projects independently' ,
956
+ async ( ) => {
957
+ // Create two projects
958
+ const project1 = await projectService . createProject ( 'RunningProject1' , projectsDirectory )
959
+ const project2 = await projectService . createProject ( 'RunningProject2' , projectsDirectory )
960
+
961
+ // Open both projects
962
+ await projectService . openProject ( project1 . projectId , projectsDirectory )
963
+ await projectService . openProject ( project2 . projectId , projectsDirectory )
964
+
965
+ // Rename both projects while they're running
966
+ try {
967
+ await projectService . renameProject (
968
+ project1 . projectId ,
969
+ 'RenamedRunning1' ,
970
+ projectsDirectory ,
971
+ )
972
+ } catch { /* Expected error for uninitialized test project */ }
973
+ try {
974
+ await projectService . renameProject (
975
+ project2 . projectId ,
976
+ 'RenamedRunning2' ,
977
+ projectsDirectory ,
978
+ )
979
+ } catch { /* Expected error for uninitialized test project */ }
980
+
981
+ // Verify package name is updated
982
+ const package1Path = path . join ( project1 . projectPath , 'package.yaml' )
983
+ const package1Content = await fs . readFile ( package1Path , 'utf-8' )
984
+ expect ( package1Content ) . contain ( 'RenamedRunning1' )
985
+
986
+ const package2Path = path . join ( project2 . projectPath , 'package.yaml' )
987
+ const package2Content = await fs . readFile ( package2Path , 'utf-8' )
988
+ expect ( package2Content ) . contain ( 'RenamedRunning2' )
989
+
990
+ // Close both projects
991
+ await projectService . closeProject ( project1 . projectId )
992
+ await projectService . closeProject ( project2 . projectId )
993
+
994
+ // Verify both directories were renamed
995
+ const newPath1 = path . join ( path . dirname ( project1 . projectPath ) , 'RenamedRunning1' )
996
+ const newPath2 = path . join ( path . dirname ( project2 . projectPath ) , 'RenamedRunning2' )
997
+
998
+ const exists1 = await fs
999
+ . access ( newPath1 )
1000
+ . then ( ( ) => true )
1001
+ . catch ( ( ) => false )
1002
+ const exists2 = await fs
1003
+ . access ( newPath2 )
1004
+ . then ( ( ) => true )
1005
+ . catch ( ( ) => false )
1006
+
1007
+ expect ( exists1 ) . toBe ( true )
1008
+ expect ( exists2 ) . toBe ( true )
1009
+ } ,
1010
+ LANGUAGE_SERVER_TEST_TIMEOUT * 2 ,
1011
+ )
1012
+ } )
674
1013
} )
0 commit comments