@@ -1235,6 +1235,145 @@ describe("CustomModesManager", () => {
12351235 const newRulePath = Object . keys ( writtenFiles ) . find ( ( p ) => p . includes ( "new-rule.md" ) )
12361236 expect ( writtenFiles [ newRulePath ! ] ) . toBe ( "New rule content" )
12371237 } )
1238+
1239+ it ( "should handle slug renaming during import (backwards compatibility)" , async ( ) => {
1240+ // Export YAML with old format (includes rules-old-slug prefix)
1241+ const exportYamlOldFormat = yaml . stringify ( {
1242+ customModes : [
1243+ {
1244+ slug : "new-slug" , // User changed this from "old-slug"
1245+ name : "Renamed Mode" ,
1246+ roleDefinition : "Test Role" ,
1247+ groups : [ "read" ] ,
1248+ rulesFiles : [
1249+ {
1250+ // Old format: includes rules-old-slug prefix
1251+ relativePath : "rules-old-slug/rule1.md" ,
1252+ content : "Rule 1 content" ,
1253+ } ,
1254+ {
1255+ relativePath : "rules-old-slug/subfolder/rule2.md" ,
1256+ content : "Rule 2 content" ,
1257+ } ,
1258+ ] ,
1259+ } ,
1260+ ] ,
1261+ } )
1262+
1263+ let roomodesContent : any = null
1264+ let writtenFiles : Record < string , string > = { }
1265+ ; ( fs . readFile as Mock ) . mockImplementation ( async ( path : string ) => {
1266+ if ( path === mockSettingsPath ) {
1267+ return yaml . stringify ( { customModes : [ ] } )
1268+ }
1269+ if ( path === mockRoomodes && roomodesContent ) {
1270+ return yaml . stringify ( roomodesContent )
1271+ }
1272+ throw new Error ( "File not found" )
1273+ } )
1274+ ; ( fs . writeFile as Mock ) . mockImplementation ( async ( path : string , content : string ) => {
1275+ if ( path === mockRoomodes ) {
1276+ roomodesContent = yaml . parse ( content )
1277+ } else {
1278+ writtenFiles [ path ] = content
1279+ }
1280+ return Promise . resolve ( )
1281+ } )
1282+ ; ( fs . mkdir as Mock ) . mockResolvedValue ( undefined )
1283+ ; ( fs . rm as Mock ) . mockResolvedValue ( undefined )
1284+
1285+ const result = await manager . importModeWithRules ( exportYamlOldFormat )
1286+
1287+ expect ( result . success ) . toBe ( true )
1288+
1289+ // Verify mode was imported with new slug
1290+ expect ( roomodesContent . customModes [ 0 ] . slug ) . toBe ( "new-slug" )
1291+
1292+ // Verify rules files were created in the NEW slug's directory
1293+ expect ( fs . mkdir ) . toHaveBeenCalledWith ( expect . stringContaining ( path . join ( ".roo" , "rules-new-slug" ) ) , {
1294+ recursive : true ,
1295+ } )
1296+
1297+ // Verify files are in the correct location (rules-new-slug, not rules-old-slug)
1298+ const rule1Path = Object . keys ( writtenFiles ) . find ( ( p ) => p . includes ( "rule1.md" ) )
1299+ const rule2Path = Object . keys ( writtenFiles ) . find ( ( p ) => p . includes ( "rule2.md" ) )
1300+
1301+ expect ( rule1Path ) . toContain ( "rules-new-slug" )
1302+ expect ( rule1Path ) . not . toContain ( "rules-old-slug" )
1303+ expect ( rule2Path ) . toContain ( "rules-new-slug" )
1304+ expect ( rule2Path ) . not . toContain ( "rules-old-slug" )
1305+
1306+ // Verify content is preserved
1307+ expect ( writtenFiles [ rule1Path ! ] ) . toBe ( "Rule 1 content" )
1308+ expect ( writtenFiles [ rule2Path ! ] ) . toBe ( "Rule 2 content" )
1309+ } )
1310+
1311+ it ( "should handle new export format without rules prefix" , async ( ) => {
1312+ // Export YAML with new format (no rules-slug prefix)
1313+ const exportYamlNewFormat = yaml . stringify ( {
1314+ customModes : [
1315+ {
1316+ slug : "test-mode" ,
1317+ name : "Test Mode" ,
1318+ roleDefinition : "Test Role" ,
1319+ groups : [ "read" ] ,
1320+ rulesFiles : [
1321+ {
1322+ // New format: no prefix, just relative path within rules directory
1323+ relativePath : "rule1.md" ,
1324+ content : "Rule 1 content" ,
1325+ } ,
1326+ {
1327+ relativePath : "subfolder/rule2.md" ,
1328+ content : "Rule 2 content" ,
1329+ } ,
1330+ ] ,
1331+ } ,
1332+ ] ,
1333+ } )
1334+
1335+ let roomodesContent : any = null
1336+ let writtenFiles : Record < string , string > = { }
1337+ ; ( fs . readFile as Mock ) . mockImplementation ( async ( path : string ) => {
1338+ if ( path === mockSettingsPath ) {
1339+ return yaml . stringify ( { customModes : [ ] } )
1340+ }
1341+ if ( path === mockRoomodes && roomodesContent ) {
1342+ return yaml . stringify ( roomodesContent )
1343+ }
1344+ throw new Error ( "File not found" )
1345+ } )
1346+ ; ( fs . writeFile as Mock ) . mockImplementation ( async ( path : string , content : string ) => {
1347+ if ( path === mockRoomodes ) {
1348+ roomodesContent = yaml . parse ( content )
1349+ } else {
1350+ writtenFiles [ path ] = content
1351+ }
1352+ return Promise . resolve ( )
1353+ } )
1354+ ; ( fs . mkdir as Mock ) . mockResolvedValue ( undefined )
1355+ ; ( fs . rm as Mock ) . mockResolvedValue ( undefined )
1356+
1357+ const result = await manager . importModeWithRules ( exportYamlNewFormat )
1358+
1359+ expect ( result . success ) . toBe ( true )
1360+
1361+ // Verify rules files were created in the correct directory
1362+ expect ( fs . mkdir ) . toHaveBeenCalledWith ( expect . stringContaining ( path . join ( ".roo" , "rules-test-mode" ) ) , {
1363+ recursive : true ,
1364+ } )
1365+
1366+ // Verify files are in the correct location
1367+ const rule1Path = Object . keys ( writtenFiles ) . find ( ( p ) => p . includes ( "rule1.md" ) )
1368+ const rule2Path = Object . keys ( writtenFiles ) . find ( ( p ) => p . includes ( "rule2.md" ) )
1369+
1370+ expect ( rule1Path ) . toContain ( "rules-test-mode" )
1371+ expect ( rule2Path ) . toContain ( path . join ( "rules-test-mode" , "subfolder" ) )
1372+
1373+ // Verify content is preserved
1374+ expect ( writtenFiles [ rule1Path ! ] ) . toBe ( "Rule 1 content" )
1375+ expect ( writtenFiles [ rule2Path ! ] ) . toBe ( "Rule 2 content" )
1376+ } )
12381377 } )
12391378 } )
12401379
@@ -1491,6 +1630,13 @@ describe("CustomModesManager", () => {
14911630 expect ( result . yaml ) . toContain ( "test-mode" )
14921631 expect ( result . yaml ) . toContain ( "Existing instructions" )
14931632 expect ( result . yaml ) . toContain ( "New rule content from files" )
1633+
1634+ // Parse the YAML to check the relativePath format
1635+ const exportedData = yaml . parse ( result . yaml ! )
1636+ const rulesFiles = exportedData . customModes [ 0 ] . rulesFiles
1637+ expect ( rulesFiles ) . toBeDefined ( )
1638+ expect ( rulesFiles [ 0 ] . relativePath ) . toBe ( "rule1.md" ) // Should NOT include "rules-test-mode/" prefix
1639+
14941640 // Should NOT delete the rules directory
14951641 expect ( fs . rm ) . not . toHaveBeenCalled ( )
14961642 } )
@@ -1616,6 +1762,12 @@ describe("CustomModesManager", () => {
16161762 expect ( result . yaml ) . toContain ( "global-test-mode" )
16171763 expect ( result . yaml ) . toContain ( "Global Test Mode" )
16181764 expect ( result . yaml ) . toContain ( "Global rule content" )
1765+
1766+ // Parse the YAML to check the relativePath format
1767+ const exportedData = yaml . parse ( result . yaml ! )
1768+ const rulesFiles = exportedData . customModes [ 0 ] . rulesFiles
1769+ expect ( rulesFiles ) . toBeDefined ( )
1770+ expect ( rulesFiles [ 0 ] . relativePath ) . toBe ( "rule1.md" ) // Should NOT include "rules-global-test-mode/" prefix
16191771 } )
16201772
16211773 it ( "should successfully export global mode without rules when global rules directory doesn't exist" , async ( ) => {
0 commit comments