11// Mocks must come first, before imports
22
33vi . mock ( "fs/promises" )
4+ vi . mock ( "os" )
45
56// Then imports
67import type { Mock } from "vitest"
78import path from "path"
9+ import os from "os"
810import { readFile } from "fs/promises"
911import type { Mode } from "../../../../shared/modes" // Type-only import
10- import { loadSystemPromptFile , PromptVariables } from "../custom-system-prompt"
12+ import { loadSystemPromptFile , PromptVariables , getGlobalSystemPromptFilePath } from "../custom-system-prompt"
1113
1214// Cast the mocked readFile to the correct Mock type
1315const mockedReadFile = readFile as Mock < typeof readFile >
16+ const mockedHomedir = os . homedir as Mock < typeof os . homedir >
1417
1518describe ( "loadSystemPromptFile" , ( ) => {
1619 // Corrected PromptVariables type and added mockMode
@@ -19,15 +22,18 @@ describe("loadSystemPromptFile", () => {
1922 }
2023 const mockCwd = "/mock/cwd"
2124 const mockMode : Mode = "test" // Use Mode type, e.g., 'test'
25+ const mockHomeDir = "/home/user"
2226 // Corrected expected file path format
23- const expectedFilePath = path . join ( mockCwd , ".roo" , `system-prompt-${ mockMode } ` )
27+ const expectedLocalFilePath = path . join ( mockCwd , ".roo" , `system-prompt-${ mockMode } ` )
28+ const expectedGlobalFilePath = path . join ( mockHomeDir , ".roo" , `system-prompt-${ mockMode } ` )
2429
2530 beforeEach ( ( ) => {
2631 // Clear mocks before each test
2732 mockedReadFile . mockClear ( )
33+ mockedHomedir . mockReturnValue ( mockHomeDir )
2834 } )
2935
30- it ( "should return an empty string if the file does not exist (ENOENT)" , async ( ) => {
36+ it ( "should return an empty string if neither local nor global file exists (ENOENT)" , async ( ) => {
3137 const error : NodeJS . ErrnoException = new Error ( "File not found" )
3238 error . code = "ENOENT"
3339 mockedReadFile . mockRejectedValue ( error )
@@ -36,8 +42,9 @@ describe("loadSystemPromptFile", () => {
3642 const result = await loadSystemPromptFile ( mockCwd , mockMode , mockVariables )
3743
3844 expect ( result ) . toBe ( "" )
39- expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
40- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
45+ expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 2 )
46+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
47+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedGlobalFilePath , "utf-8" )
4148 } )
4249
4350 // Updated test: should re-throw unexpected errors
@@ -50,18 +57,23 @@ describe("loadSystemPromptFile", () => {
5057
5158 // Verify readFile was still called correctly
5259 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
53- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
60+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
5461 } )
5562
56- it ( "should return an empty string if the file content is empty" , async ( ) => {
57- mockedReadFile . mockResolvedValue ( "" )
63+ it ( "should return an empty string if the local file content is empty and check global" , async ( ) => {
64+ const error : NodeJS . ErrnoException = new Error ( "File not found" )
65+ error . code = "ENOENT"
66+
67+ // Local file is empty, global file doesn't exist
68+ mockedReadFile . mockResolvedValueOnce ( "" ) . mockRejectedValueOnce ( error )
5869
5970 // Added mockMode argument
6071 const result = await loadSystemPromptFile ( mockCwd , mockMode , mockVariables )
6172
6273 expect ( result ) . toBe ( "" )
63- expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
64- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
74+ expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 2 )
75+ expect ( mockedReadFile ) . toHaveBeenNthCalledWith ( 1 , expectedLocalFilePath , "utf-8" )
76+ expect ( mockedReadFile ) . toHaveBeenNthCalledWith ( 2 , expectedGlobalFilePath , "utf-8" )
6577 } )
6678
6779 // Updated test to only check workspace interpolation
@@ -74,7 +86,7 @@ describe("loadSystemPromptFile", () => {
7486
7587 expect ( result ) . toBe ( "Workspace is: /path/to/workspace" )
7688 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
77- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
89+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
7890 } )
7991
8092 // Updated test for multiple occurrences of workspace
@@ -87,7 +99,7 @@ describe("loadSystemPromptFile", () => {
8799
88100 expect ( result ) . toBe ( "Path: /path/to/workspace//path/to/workspace" )
89101 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
90- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
102+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
91103 } )
92104
93105 // Updated test for mixed used/unused
@@ -101,7 +113,7 @@ describe("loadSystemPromptFile", () => {
101113 // Unused variables should remain untouched
102114 expect ( result ) . toBe ( "Workspace: /path/to/workspace, Unused: {{unusedVar}}, Another: {{another}}" )
103115 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
104- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
116+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
105117 } )
106118
107119 // Test remains valid, just needs the mode argument and updated template
@@ -114,7 +126,7 @@ describe("loadSystemPromptFile", () => {
114126
115127 expect ( result ) . toBe ( "Workspace: /path/to/workspace, Missing: {{missingPlaceholder}}" )
116128 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
117- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
129+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
118130 } )
119131
120132 // Removed the test for extra keys as PromptVariables is simple now
@@ -129,6 +141,39 @@ describe("loadSystemPromptFile", () => {
129141
130142 expect ( result ) . toBe ( "This is a static prompt." )
131143 expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
132- expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedFilePath , "utf-8" )
144+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
145+ } )
146+
147+ it ( "should use global system prompt when local file doesn't exist" , async ( ) => {
148+ const error : NodeJS . ErrnoException = new Error ( "File not found" )
149+ error . code = "ENOENT"
150+ const globalTemplate = "Global system prompt: {{workspace}}"
151+
152+ // First call (local) fails, second call (global) succeeds
153+ mockedReadFile . mockRejectedValueOnce ( error ) . mockResolvedValueOnce ( globalTemplate )
154+
155+ const result = await loadSystemPromptFile ( mockCwd , mockMode , mockVariables )
156+
157+ expect ( result ) . toBe ( "Global system prompt: /path/to/workspace" )
158+ expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 2 )
159+ expect ( mockedReadFile ) . toHaveBeenNthCalledWith ( 1 , expectedLocalFilePath , "utf-8" )
160+ expect ( mockedReadFile ) . toHaveBeenNthCalledWith ( 2 , expectedGlobalFilePath , "utf-8" )
161+ } )
162+
163+ it ( "should prefer local system prompt over global when both exist" , async ( ) => {
164+ const localTemplate = "Local system prompt: {{workspace}}"
165+ mockedReadFile . mockResolvedValueOnce ( localTemplate )
166+
167+ const result = await loadSystemPromptFile ( mockCwd , mockMode , mockVariables )
168+
169+ expect ( result ) . toBe ( "Local system prompt: /path/to/workspace" )
170+ // Should only read the local file, not the global one
171+ expect ( mockedReadFile ) . toHaveBeenCalledTimes ( 1 )
172+ expect ( mockedReadFile ) . toHaveBeenCalledWith ( expectedLocalFilePath , "utf-8" )
173+ } )
174+
175+ it ( "should correctly generate global system prompt file path" , ( ) => {
176+ const result = getGlobalSystemPromptFilePath ( mockMode )
177+ expect ( result ) . toBe ( expectedGlobalFilePath )
133178 } )
134179} )
0 commit comments