@@ -42,14 +42,13 @@ function createTmpFile(dir: string, name: string, content: string): string {
4242describe ( 'MCP note attachment tools' , ( ) => {
4343 let ctx : McpTestContext ;
4444 let noteId : string ;
45- let tmpDir : string ;
45+ let projectDir : string ;
4646 let filePath : string ;
4747
4848 beforeAll ( async ( ) => {
49- tmpDir = makeTmpDir ( ) ;
50- filePath = createTmpFile ( tmpDir , 'doc.txt' , 'Hello attachment' ) ;
49+ projectDir = makeTmpDir ( ) ;
50+ filePath = createTmpFile ( projectDir , 'doc.txt' , 'Hello attachment' ) ;
5151
52- const projectDir = makeTmpDir ( ) ;
5352 const knowledgeGraph = createKnowledgeGraph ( ) ;
5453 noteId = createNote ( knowledgeGraph , 'Test Note' , 'content' , [ ] , unitVec ( 0 ) ) ;
5554
@@ -62,7 +61,7 @@ describe('MCP note attachment tools', () => {
6261
6362 afterAll ( async ( ) => {
6463 await ctx . close ( ) ;
65- fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
64+ fs . rmSync ( projectDir , { recursive : true , force : true } ) ;
6665 } ) ;
6766
6867 it ( 'add_note_attachment: attaches file successfully' , async ( ) => {
@@ -74,17 +73,28 @@ describe('MCP note attachment tools', () => {
7473 } ) ;
7574
7675 it ( 'add_note_attachment: file not found returns error' , async ( ) => {
77- const result = await ctx . call ( 'add_note_attachment' , { noteId, filePath : '/nonexistent/file.txt' } ) ;
76+ const missing = path . join ( projectDir , 'nonexistent.txt' ) ;
77+ const result = await ctx . call ( 'add_note_attachment' , { noteId, filePath : missing } ) ;
7878 expect ( result . isError ) . toBe ( true ) ;
7979 expect ( text ( result ) ) . toContain ( 'File not found' ) ;
8080 } ) ;
8181
8282 it ( 'add_note_attachment: directory returns error' , async ( ) => {
83- const result = await ctx . call ( 'add_note_attachment' , { noteId, filePath : tmpDir } ) ;
83+ const subDir = path . join ( projectDir , 'subdir' ) ;
84+ fs . mkdirSync ( subDir , { recursive : true } ) ;
85+ const result = await ctx . call ( 'add_note_attachment' , { noteId, filePath : subDir } ) ;
8486 expect ( result . isError ) . toBe ( true ) ;
8587 expect ( text ( result ) ) . toContain ( 'not a regular file' ) ;
8688 } ) ;
8789
90+ it ( 'add_note_attachment: path traversal rejected' , async ( ) => {
91+ const outside = createTmpFile ( os . tmpdir ( ) , 'evil.txt' , 'secrets' ) ;
92+ const result = await ctx . call ( 'add_note_attachment' , { noteId, filePath : outside } ) ;
93+ expect ( result . isError ) . toBe ( true ) ;
94+ expect ( text ( result ) ) . toContain ( 'within the project directory' ) ;
95+ fs . unlinkSync ( outside ) ;
96+ } ) ;
97+
8898 it ( 'add_note_attachment: note not found returns error' , async ( ) => {
8999 const result = await ctx . call ( 'add_note_attachment' , { noteId : 'ghost-note' , filePath } ) ;
90100 expect ( result . isError ) . toBe ( true ) ;
@@ -113,14 +123,13 @@ describe('MCP note attachment tools', () => {
113123describe ( 'MCP task attachment tools' , ( ) => {
114124 let ctx : McpTestContext ;
115125 let taskId : string ;
116- let tmpDir : string ;
126+ let projectDir : string ;
117127 let filePath : string ;
118128
119129 beforeAll ( async ( ) => {
120- tmpDir = makeTmpDir ( ) ;
121- filePath = createTmpFile ( tmpDir , 'report.csv' , 'id,name\n1,alice' ) ;
130+ projectDir = makeTmpDir ( ) ;
131+ filePath = createTmpFile ( projectDir , 'report.csv' , 'id,name\n1,alice' ) ;
122132
123- const projectDir = makeTmpDir ( ) ;
124133 const taskGraph = createTaskGraph ( ) ;
125134 taskId = createTask ( taskGraph , 'Test Task' , 'desc' , 'todo' , 'medium' , [ ] , unitVec ( 0 ) ) ;
126135
@@ -133,7 +142,7 @@ describe('MCP task attachment tools', () => {
133142
134143 afterAll ( async ( ) => {
135144 await ctx . close ( ) ;
136- fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
145+ fs . rmSync ( projectDir , { recursive : true , force : true } ) ;
137146 } ) ;
138147
139148 it ( 'add_task_attachment: attaches file' , async ( ) => {
@@ -144,13 +153,24 @@ describe('MCP task attachment tools', () => {
144153 } ) ;
145154
146155 it ( 'add_task_attachment: file not found' , async ( ) => {
147- const result = await ctx . call ( 'add_task_attachment' , { taskId, filePath : '/no/file.txt' } ) ;
156+ const missing = path . join ( projectDir , 'nope.txt' ) ;
157+ const result = await ctx . call ( 'add_task_attachment' , { taskId, filePath : missing } ) ;
148158 expect ( result . isError ) . toBe ( true ) ;
149159 } ) ;
150160
151161 it ( 'add_task_attachment: directory returns error' , async ( ) => {
152- const result = await ctx . call ( 'add_task_attachment' , { taskId, filePath : tmpDir } ) ;
162+ const subDir = path . join ( projectDir , 'subdir' ) ;
163+ fs . mkdirSync ( subDir , { recursive : true } ) ;
164+ const result = await ctx . call ( 'add_task_attachment' , { taskId, filePath : subDir } ) ;
165+ expect ( result . isError ) . toBe ( true ) ;
166+ } ) ;
167+
168+ it ( 'add_task_attachment: path traversal rejected' , async ( ) => {
169+ const outside = createTmpFile ( os . tmpdir ( ) , 'evil.csv' , 'stolen' ) ;
170+ const result = await ctx . call ( 'add_task_attachment' , { taskId, filePath : outside } ) ;
153171 expect ( result . isError ) . toBe ( true ) ;
172+ expect ( text ( result ) ) . toContain ( 'within the project directory' ) ;
173+ fs . unlinkSync ( outside ) ;
154174 } ) ;
155175
156176 it ( 'remove_task_attachment: removes' , async ( ) => {
@@ -173,14 +193,13 @@ describe('MCP task attachment tools', () => {
173193describe ( 'MCP skill attachment tools' , ( ) => {
174194 let ctx : McpTestContext ;
175195 let skillId : string ;
176- let tmpDir : string ;
196+ let projectDir : string ;
177197 let filePath : string ;
178198
179199 beforeAll ( async ( ) => {
180- tmpDir = makeTmpDir ( ) ;
181- filePath = createTmpFile ( tmpDir , 'template.yaml' , 'key: value' ) ;
200+ projectDir = makeTmpDir ( ) ;
201+ filePath = createTmpFile ( projectDir , 'template.yaml' , 'key: value' ) ;
182202
183- const projectDir = makeTmpDir ( ) ;
184203 const skillGraph = createSkillGraph ( ) ;
185204 skillId = createSkill ( skillGraph , 'Test Skill' , 'desc' , [ ] , [ ] , [ ] , [ ] , [ ] , 'user' , 1 , unitVec ( 0 ) ) ;
186205
@@ -193,7 +212,7 @@ describe('MCP skill attachment tools', () => {
193212
194213 afterAll ( async ( ) => {
195214 await ctx . close ( ) ;
196- fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
215+ fs . rmSync ( projectDir , { recursive : true , force : true } ) ;
197216 } ) ;
198217
199218 it ( 'add_skill_attachment: attaches file' , async ( ) => {
@@ -204,13 +223,24 @@ describe('MCP skill attachment tools', () => {
204223 } ) ;
205224
206225 it ( 'add_skill_attachment: file not found' , async ( ) => {
207- const result = await ctx . call ( 'add_skill_attachment' , { skillId, filePath : '/missing/file' } ) ;
226+ const missing = path . join ( projectDir , 'missing.file' ) ;
227+ const result = await ctx . call ( 'add_skill_attachment' , { skillId, filePath : missing } ) ;
208228 expect ( result . isError ) . toBe ( true ) ;
209229 } ) ;
210230
211231 it ( 'add_skill_attachment: directory returns error' , async ( ) => {
212- const result = await ctx . call ( 'add_skill_attachment' , { skillId, filePath : tmpDir } ) ;
232+ const subDir = path . join ( projectDir , 'subdir' ) ;
233+ fs . mkdirSync ( subDir , { recursive : true } ) ;
234+ const result = await ctx . call ( 'add_skill_attachment' , { skillId, filePath : subDir } ) ;
235+ expect ( result . isError ) . toBe ( true ) ;
236+ } ) ;
237+
238+ it ( 'add_skill_attachment: path traversal rejected' , async ( ) => {
239+ const outside = createTmpFile ( os . tmpdir ( ) , 'evil.yaml' , 'stolen' ) ;
240+ const result = await ctx . call ( 'add_skill_attachment' , { skillId, filePath : outside } ) ;
213241 expect ( result . isError ) . toBe ( true ) ;
242+ expect ( text ( result ) ) . toContain ( 'within the project directory' ) ;
243+ fs . unlinkSync ( outside ) ;
214244 } ) ;
215245
216246 it ( 'remove_skill_attachment: removes' , async ( ) => {
0 commit comments