@@ -42,8 +42,14 @@ const mocks = vi.hoisted(() => ({
4242 deleteFileVfsItems : vi . fn ( ) ,
4343 renameTableVfs : vi . fn ( ) ,
4444 deleteTableVfs : vi . fn ( ) ,
45+ transferTableVfs : vi . fn ( ) ,
46+ createTableFolders : vi . fn ( ) ,
47+ deleteTableFolders : vi . fn ( ) ,
4548 renameKnowledgeVfs : vi . fn ( ) ,
4649 deleteKnowledgeVfs : vi . fn ( ) ,
50+ transferKnowledgeVfs : vi . fn ( ) ,
51+ createKnowledgeFolders : vi . fn ( ) ,
52+ deleteKnowledgeFolders : vi . fn ( ) ,
4753} ) )
4854
4955vi . mock ( '@sim/db' , ( ) => ( { ...dbChainMock , ...schemaMock } ) )
@@ -163,6 +169,18 @@ vi.mock('@/lib/table/application/table-vfs', () => ({
163169 operation : tableOperations . deleteByVfsPath ,
164170 execute : mocks . deleteTableVfs ,
165171 } ,
172+ transferTableVfsItems : {
173+ operation : tableOperations . moveByVfsPath ,
174+ execute : mocks . transferTableVfs ,
175+ } ,
176+ createTableVfsFolders : {
177+ operation : tableOperations . createFolder ,
178+ execute : mocks . createTableFolders ,
179+ } ,
180+ deleteTableVfsFolders : {
181+ operation : tableOperations . deleteFolder ,
182+ execute : mocks . deleteTableFolders ,
183+ } ,
166184} ) )
167185
168186vi . mock ( '@/lib/knowledge/application/knowledge-vfs' , ( ) => ( {
@@ -174,6 +192,18 @@ vi.mock('@/lib/knowledge/application/knowledge-vfs', () => ({
174192 operation : knowledgeOperations . deleteByVfsPath ,
175193 execute : mocks . deleteKnowledgeVfs ,
176194 } ,
195+ transferKnowledgeVfsItems : {
196+ operation : knowledgeOperations . moveByVfsPath ,
197+ execute : mocks . transferKnowledgeVfs ,
198+ } ,
199+ createKnowledgeVfsFolders : {
200+ operation : knowledgeOperations . manageVfsFolders ,
201+ execute : mocks . createKnowledgeFolders ,
202+ } ,
203+ deleteKnowledgeVfsFolders : {
204+ operation : knowledgeOperations . manageVfsFolders ,
205+ execute : mocks . deleteKnowledgeFolders ,
206+ } ,
177207} ) )
178208
179209vi . mock ( '@/lib/table/service' , ( ) => ( {
@@ -776,15 +806,41 @@ describe('vfs mv/cp', () => {
776806 } )
777807 } )
778808
779- it ( 'rejects flat namespaces' , async ( ) => {
809+ it ( 'creates table folders through the table application operation' , async ( ) => {
810+ mocks . createTableFolders . mockResolvedValue ( {
811+ outcomes : [
812+ { source : 'tables/CRM' , kind : 'folder' , resourceId : 'fld-1' , targetSegments : [ 'CRM' ] } ,
813+ ] ,
814+ } )
815+
780816 const result = await executeVfsMkdir ( { paths : [ 'tables/CRM' ] } , context )
781- expect ( result . success ) . toBe ( false )
817+
818+ expect ( mocks . createTableFolders ) . toHaveBeenCalledWith (
819+ expect . objectContaining ( {
820+ input : {
821+ workspaceId : 'ws-1' ,
822+ paths : [ { source : 'tables/CRM' , segments : [ 'CRM' ] } ] ,
823+ } ,
824+ } )
825+ )
826+ expect ( result . success ) . toBe ( true )
782827 expect ( result . output ) . toMatchObject ( {
783- results : [ { from : 'tables/CRM' , error : expect . stringContaining ( 'flat namespace' ) } ] ,
828+ results : [ { from : 'tables/CRM' , to : 'tables/CRM' , kind : 'table_folder' , id : 'fld-1' } ] ,
784829 } )
785830 expect ( mocks . ensureCopilotFileFolderPath ) . not . toHaveBeenCalled ( )
786831 } )
787832
833+ it ( 'rejects the reserved knowledgebases/connectors folder path' , async ( ) => {
834+ const result = await executeVfsMkdir ( { paths : [ 'knowledgebases/connectors/sub' ] } , context )
835+ expect ( result . success ) . toBe ( false )
836+ expect ( result . output ) . toMatchObject ( {
837+ results : [
838+ { from : 'knowledgebases/connectors/sub' , error : expect . stringContaining ( 'reserved' ) } ,
839+ ] ,
840+ } )
841+ expect ( mocks . createKnowledgeFolders ) . not . toHaveBeenCalled ( )
842+ } )
843+
788844 it ( 'rejects creation inside a locked workflow folder' , async ( ) => {
789845 mocks . createWorkflowVfsFolders . mockResolvedValue ( {
790846 outcomes : [
@@ -804,30 +860,65 @@ describe('vfs mv/cp', () => {
804860 } )
805861 } )
806862
807- describe ( 'tables and knowledge bases (flat namespaces)' , ( ) => {
808- it ( 'renames a table' , async ( ) => {
863+ describe ( 'tables and knowledge bases (foldered)' , ( ) => {
864+ it ( 'renames a table through the transfer application operation' , async ( ) => {
865+ mocks . transferTableVfs . mockResolvedValue ( {
866+ outcomes : [
867+ {
868+ source : 'tables/Leads' ,
869+ kind : 'resource' ,
870+ resourceId : 'tbl-1' ,
871+ targetSegments : [ 'Customers' ] ,
872+ } ,
873+ ] ,
874+ } )
875+
809876 const result = await executeVfsMv (
810877 { sources : [ 'tables/Leads' ] , destination : 'tables/Customers' } ,
811878 context
812879 )
813880
814- expect ( mocks . renameTableVfs ) . toHaveBeenCalledWith (
881+ expect ( mocks . transferTableVfs ) . toHaveBeenCalledWith (
815882 expect . objectContaining ( {
816- input : { workspaceId : 'ws-1' , sourceName : 'Leads' , newName : 'Customers' } ,
883+ input : {
884+ workspaceId : 'ws-1' ,
885+ sources : [ { source : 'tables/Leads' , segments : [ 'Leads' ] } ] ,
886+ destination : { segments : [ 'Customers' ] , trailingSlash : false } ,
887+ } ,
817888 } )
818889 )
819890 expect ( result . success ) . toBe ( true )
820891 expect ( result . output ) . toMatchObject ( { results : [ { to : 'tables/Customers' , kind : 'table' } ] } )
821892 } )
822893
823- it ( 'rejects nested table destinations as flat-namespace violations' , async ( ) => {
894+ it ( 'moves a table into a folder, folders auto-created server-side' , async ( ) => {
895+ mocks . transferTableVfs . mockResolvedValue ( {
896+ outcomes : [
897+ {
898+ source : 'tables/Leads' ,
899+ kind : 'resource' ,
900+ resourceId : 'tbl-1' ,
901+ targetSegments : [ 'CRM' , 'Leads' ] ,
902+ } ,
903+ ] ,
904+ } )
905+
824906 const result = await executeVfsMv (
825- { sources : [ 'tables/Leads' ] , destination : 'tables/CRM/Leads ' } ,
907+ { sources : [ 'tables/Leads' ] , destination : 'tables/CRM/' } ,
826908 context
827909 )
828- expect ( result . success ) . toBe ( false )
829- expect ( result . error ) . toContain ( 'flat namespace' )
830- expect ( mocks . renameTable ) . not . toHaveBeenCalled ( )
910+
911+ expect ( mocks . transferTableVfs ) . toHaveBeenCalledWith (
912+ expect . objectContaining ( {
913+ input : expect . objectContaining ( {
914+ destination : { segments : [ 'CRM' ] , trailingSlash : true } ,
915+ } ) ,
916+ } )
917+ )
918+ expect ( result . success ) . toBe ( true )
919+ expect ( result . output ) . toMatchObject ( {
920+ results : [ { to : 'tables/CRM/Leads' , kind : 'table' } ] ,
921+ } )
831922 } )
832923
833924 it ( 'rejects copying tables' , async ( ) => {
@@ -840,12 +931,23 @@ describe('vfs mv/cp', () => {
840931 } )
841932
842933 it ( 'renames a knowledge base through trusted application operations' , async ( ) => {
934+ mocks . transferKnowledgeVfs . mockResolvedValue ( {
935+ outcomes : [
936+ {
937+ source : 'knowledgebases/Docs' ,
938+ kind : 'resource' ,
939+ resourceId : 'kb-1' ,
940+ targetSegments : [ 'Product Docs' ] ,
941+ } ,
942+ ] ,
943+ } )
944+
843945 const result = await executeVfsMv (
844946 { sources : [ 'knowledgebases/Docs' ] , destination : 'knowledgebases/Product Docs' } ,
845947 context
846948 )
847949
848- expect ( mocks . renameKnowledgeVfs ) . toHaveBeenCalledWith (
950+ expect ( mocks . transferKnowledgeVfs ) . toHaveBeenCalledWith (
849951 expect . objectContaining ( {
850952 principal : expect . objectContaining ( {
851953 kind : 'delegated' ,
@@ -855,16 +957,16 @@ describe('vfs mv/cp', () => {
855957 } ) ,
856958 input : {
857959 workspaceId : 'ws-1' ,
858- sourceName : ' Docs',
859- newName : 'Product Docs' ,
960+ sources : [ { source : 'knowledgebases/ Docs', segments : [ 'Docs' ] } ] ,
961+ destination : { segments : [ 'Product Docs' ] , trailingSlash : false } ,
860962 } ,
861963 } )
862964 )
863965 expect ( result . success ) . toBe ( true )
864966 } )
865967
866968 it ( 'propagates knowledge application infrastructure failures' , async ( ) => {
867- mocks . renameKnowledgeVfs . mockRejectedValueOnce ( new Error ( 'knowledge database unavailable' ) )
969+ mocks . transferKnowledgeVfs . mockRejectedValueOnce ( new Error ( 'knowledge database unavailable' ) )
868970
869971 await expect (
870972 executeVfsMv (
@@ -875,7 +977,7 @@ describe('vfs mv/cp', () => {
875977 } )
876978
877979 it ( 'preserves an actionable knowledge rename conflict' , async ( ) => {
878- mocks . renameKnowledgeVfs . mockRejectedValue (
980+ mocks . transferKnowledgeVfs . mockRejectedValue (
879981 new OrchestrationError ( 'conflict' , 'A knowledge base named Product Docs already exists' )
880982 )
881983
@@ -912,11 +1014,71 @@ describe('vfs mv/cp', () => {
9121014 input : {
9131015 workspaceId : 'ws-1' ,
9141016 sourceName : 'Docs' ,
1017+ sourceSegments : [ 'Docs' ] ,
9151018 } ,
9161019 } )
9171020 )
9181021 } )
9191022
1023+ it ( 'moves a whole table folder through the transfer operation' , async ( ) => {
1024+ mocks . transferTableVfs . mockResolvedValue ( {
1025+ outcomes : [
1026+ {
1027+ source : 'tables/CRM' ,
1028+ kind : 'folder' ,
1029+ resourceId : 'fld-1' ,
1030+ targetSegments : [ 'Archive' , 'CRM' ] ,
1031+ } ,
1032+ ] ,
1033+ } )
1034+
1035+ const result = await executeVfsMv (
1036+ { sources : [ 'tables/CRM' ] , destination : 'tables/Archive/' } ,
1037+ context
1038+ )
1039+
1040+ expect ( result . success ) . toBe ( true )
1041+ expect ( result . output ) . toMatchObject ( {
1042+ results : [ { to : 'tables/Archive/CRM' , kind : 'table_folder' } ] ,
1043+ } )
1044+ } )
1045+
1046+ it ( 'rm retargets to the folder cascade when the path is a folder' , async ( ) => {
1047+ mocks . deleteTableVfs . mockRejectedValue (
1048+ new OrchestrationError ( 'invalid' , 'tables/CRM is a folder; this operation takes a table.' )
1049+ )
1050+ mocks . deleteTableFolders . mockResolvedValue ( {
1051+ outcomes : [ { source : 'tables/CRM' , kind : 'folder' , resourceId : 'fld-1' } ] ,
1052+ } )
1053+
1054+ const result = await executeVfsRm ( { paths : [ 'tables/CRM' ] } , context )
1055+
1056+ expect ( mocks . deleteTableFolders ) . toHaveBeenCalledWith (
1057+ expect . objectContaining ( {
1058+ input : { workspaceId : 'ws-1' , paths : [ { source : 'tables/CRM' , segments : [ 'CRM' ] } ] } ,
1059+ } )
1060+ )
1061+ expect ( result . success ) . toBe ( true )
1062+ expect ( result . output ) . toMatchObject ( {
1063+ results : [ { from : 'tables/CRM' , kind : 'table_folder' , id : 'fld-1' } ] ,
1064+ } )
1065+ } )
1066+
1067+ it ( 'deletes a nested knowledge base by its folder path' , async ( ) => {
1068+ const result = await executeVfsRm ( { paths : [ 'knowledgebases/Legal/Contracts' ] } , context )
1069+
1070+ expect ( mocks . deleteKnowledgeVfs ) . toHaveBeenCalledWith (
1071+ expect . objectContaining ( {
1072+ input : {
1073+ workspaceId : 'ws-1' ,
1074+ sourceName : 'Contracts' ,
1075+ sourceSegments : [ 'Legal' , 'Contracts' ] ,
1076+ } ,
1077+ } )
1078+ )
1079+ expect ( result . success ) . toBe ( true )
1080+ } )
1081+
9201082 it ( 'preserves an actionable knowledge delete failure' , async ( ) => {
9211083 mocks . deleteKnowledgeVfs . mockRejectedValue (
9221084 new OrchestrationError ( 'not_found' , 'Knowledge base no longer exists' )
0 commit comments