@@ -226,6 +226,264 @@ func TestWrapToolHandler_LongPayload(t *testing.T) {
226226 assert .True (t , strings .HasSuffix (preview , "..." ), "Preview should end with '...'" )
227227}
228228
229+ // TestPayloadStorage_SessionIsolation verifies that payloads are stored in session-specific directories
230+ func TestPayloadStorage_SessionIsolation (t * testing.T ) {
231+ // Create temporary directory for test
232+ baseDir := t .TempDir ()
233+
234+ // Define two different session IDs
235+ session1 := "session-alpha-123"
236+ session2 := "session-beta-456"
237+
238+ // Create a mock handler
239+ mockHandler := func (ctx context.Context , req * sdk.CallToolRequest , args interface {}) (* sdk.CallToolResult , interface {}, error ) {
240+ return & sdk.CallToolResult {IsError : false }, map [string ]interface {}{
241+ "data" : "test-payload" ,
242+ }, nil
243+ }
244+
245+ // Create session ID getters for each session
246+ getSession1 := func (ctx context.Context ) string { return session1 }
247+ getSession2 := func (ctx context.Context ) string { return session2 }
248+
249+ // Call handler for session 1
250+ wrapped1 := WrapToolHandler (mockHandler , "test_tool" , baseDir , getSession1 )
251+ _ , data1 , err := wrapped1 (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
252+ require .NoError (t , err )
253+
254+ // Call handler for session 2
255+ wrapped2 := WrapToolHandler (mockHandler , "test_tool" , baseDir , getSession2 )
256+ _ , data2 , err := wrapped2 (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
257+ require .NoError (t , err )
258+
259+ // Extract payload paths
260+ dataMap1 := data1 .(map [string ]interface {})
261+ dataMap2 := data2 .(map [string ]interface {})
262+
263+ payloadPath1 := dataMap1 ["payloadPath" ].(string )
264+ payloadPath2 := dataMap2 ["payloadPath" ].(string )
265+
266+ // Verify paths are different
267+ assert .NotEqual (t , payloadPath1 , payloadPath2 , "Different sessions should have different payload paths" )
268+
269+ // Verify session directories exist and are isolated
270+ session1Dir := filepath .Join (baseDir , session1 )
271+ session2Dir := filepath .Join (baseDir , session2 )
272+
273+ assert .DirExists (t , session1Dir , "Session 1 directory should exist" )
274+ assert .DirExists (t , session2Dir , "Session 2 directory should exist" )
275+
276+ // Verify payload paths contain respective session IDs
277+ assert .Contains (t , payloadPath1 , session1 , "Payload path 1 should contain session 1 ID" )
278+ assert .Contains (t , payloadPath2 , session2 , "Payload path 2 should contain session 2 ID" )
279+
280+ // Verify payloads are not in each other's directories
281+ assert .NotContains (t , payloadPath1 , session2 , "Payload 1 should not be in session 2 directory" )
282+ assert .NotContains (t , payloadPath2 , session1 , "Payload 2 should not be in session 1 directory" )
283+
284+ // Verify files exist at the correct paths
285+ assert .FileExists (t , payloadPath1 , "Payload file 1 should exist" )
286+ assert .FileExists (t , payloadPath2 , "Payload file 2 should exist" )
287+ }
288+
289+ // TestPayloadStorage_LargePayloadPreserved verifies that the complete large payload is stored to disk
290+ func TestPayloadStorage_LargePayloadPreserved (t * testing.T ) {
291+ baseDir := t .TempDir ()
292+
293+ // Create a large payload (> 500 chars to trigger truncation)
294+ largeContent := strings .Repeat ("This is a large payload content. " , 100 ) // ~3400 chars
295+ largePayload := map [string ]interface {}{
296+ "total_count" : 1000 ,
297+ "items" : []interface {}{
298+ map [string ]interface {}{
299+ "id" : 12345 ,
300+ "name" : "test-item" ,
301+ "description" : largeContent ,
302+ "metadata" : map [string ]interface {}{
303+ "author" : "test-author" ,
304+ "tags" : []interface {}{"tag1" , "tag2" , "tag3" },
305+ },
306+ },
307+ },
308+ }
309+
310+ mockHandler := func (ctx context.Context , req * sdk.CallToolRequest , args interface {}) (* sdk.CallToolResult , interface {}, error ) {
311+ return & sdk.CallToolResult {IsError : false }, largePayload , nil
312+ }
313+
314+ wrapped := WrapToolHandler (mockHandler , "test_tool" , baseDir , testGetSessionID )
315+ _ , data , err := wrapped (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
316+ require .NoError (t , err )
317+
318+ dataMap := data .(map [string ]interface {})
319+
320+ // Verify truncation occurred
321+ assert .True (t , dataMap ["truncated" ].(bool ), "Large payload should be truncated" )
322+
323+ // Get the payload path
324+ payloadPath := dataMap ["payloadPath" ].(string )
325+ assert .FileExists (t , payloadPath , "Payload file should exist" )
326+
327+ // Read the stored payload
328+ storedContent , err := os .ReadFile (payloadPath )
329+ require .NoError (t , err , "Should be able to read stored payload" )
330+
331+ // Verify stored payload is valid JSON
332+ var storedPayload map [string ]interface {}
333+ err = json .Unmarshal (storedContent , & storedPayload )
334+ require .NoError (t , err , "Stored payload should be valid JSON" )
335+
336+ // Verify complete payload structure is preserved
337+ assert .Equal (t , float64 (1000 ), storedPayload ["total_count" ], "total_count should be preserved" )
338+
339+ items := storedPayload ["items" ].([]interface {})
340+ require .Len (t , items , 1 , "Should have 1 item" )
341+
342+ item := items [0 ].(map [string ]interface {})
343+ assert .Equal (t , float64 (12345 ), item ["id" ], "Item ID should be preserved" )
344+ assert .Equal (t , "test-item" , item ["name" ], "Item name should be preserved" )
345+ assert .Equal (t , largeContent , item ["description" ], "Complete large description should be preserved" )
346+
347+ metadata := item ["metadata" ].(map [string ]interface {})
348+ assert .Equal (t , "test-author" , metadata ["author" ], "Metadata author should be preserved" )
349+
350+ // Verify originalSize matches stored content size
351+ originalSize := dataMap ["originalSize" ].(int )
352+ assert .Equal (t , len (storedContent ), originalSize , "originalSize should match stored content size" )
353+ }
354+
355+ // TestPayloadStorage_DirectoryStructure verifies the directory structure {baseDir}/{sessionID}/{queryID}/payload.json
356+ func TestPayloadStorage_DirectoryStructure (t * testing.T ) {
357+ baseDir := t .TempDir ()
358+ sessionID := "test-session-dir-check"
359+
360+ getSessionID := func (ctx context.Context ) string { return sessionID }
361+
362+ mockHandler := func (ctx context.Context , req * sdk.CallToolRequest , args interface {}) (* sdk.CallToolResult , interface {}, error ) {
363+ return & sdk.CallToolResult {IsError : false }, map [string ]interface {}{"test" : "data" }, nil
364+ }
365+
366+ wrapped := WrapToolHandler (mockHandler , "test_tool" , baseDir , getSessionID )
367+ _ , data , err := wrapped (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
368+ require .NoError (t , err )
369+
370+ dataMap := data .(map [string ]interface {})
371+
372+ payloadPath := dataMap ["payloadPath" ].(string )
373+ queryID := dataMap ["queryID" ].(string )
374+
375+ // Verify the expected directory structure
376+ expectedDir := filepath .Join (baseDir , sessionID , queryID )
377+ expectedPath := filepath .Join (expectedDir , "payload.json" )
378+
379+ assert .Equal (t , expectedPath , payloadPath , "Payload path should match expected structure" )
380+ assert .DirExists (t , expectedDir , "Query directory should exist" )
381+
382+ // Verify the file is named payload.json
383+ assert .True (t , strings .HasSuffix (payloadPath , "payload.json" ), "File should be named payload.json" )
384+
385+ // Verify queryID is a valid hex string (32 chars = 16 bytes in hex)
386+ assert .Len (t , queryID , 32 , "Query ID should be 32 hex characters" )
387+ }
388+
389+ // TestPayloadStorage_MultipleRequestsSameSession verifies that multiple requests from the same session
390+ // create separate query directories
391+ func TestPayloadStorage_MultipleRequestsSameSession (t * testing.T ) {
392+ baseDir := t .TempDir ()
393+ sessionID := "same-session"
394+
395+ getSessionID := func (ctx context.Context ) string { return sessionID }
396+
397+ mockHandler := func (ctx context.Context , req * sdk.CallToolRequest , args interface {}) (* sdk.CallToolResult , interface {}, error ) {
398+ return & sdk.CallToolResult {IsError : false }, map [string ]interface {}{"request" : "data" }, nil
399+ }
400+
401+ wrapped := WrapToolHandler (mockHandler , "test_tool" , baseDir , getSessionID )
402+
403+ // Make multiple requests
404+ var payloadPaths []string
405+ var queryIDs []string
406+
407+ for i := 0 ; i < 5 ; i ++ {
408+ _ , data , err := wrapped (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
409+ require .NoError (t , err )
410+
411+ dataMap := data .(map [string ]interface {})
412+ payloadPaths = append (payloadPaths , dataMap ["payloadPath" ].(string ))
413+ queryIDs = append (queryIDs , dataMap ["queryID" ].(string ))
414+ }
415+
416+ // Verify all payload paths are unique
417+ pathSet := make (map [string ]bool )
418+ for _ , path := range payloadPaths {
419+ assert .False (t , pathSet [path ], "Payload paths should be unique" )
420+ pathSet [path ] = true
421+ }
422+
423+ // Verify all query IDs are unique
424+ idSet := make (map [string ]bool )
425+ for _ , id := range queryIDs {
426+ assert .False (t , idSet [id ], "Query IDs should be unique" )
427+ idSet [id ] = true
428+ }
429+
430+ // Verify all files exist
431+ for _ , path := range payloadPaths {
432+ assert .FileExists (t , path , "Each payload file should exist" )
433+ }
434+
435+ // Verify all are in the same session directory but different query directories
436+ sessionDir := filepath .Join (baseDir , sessionID )
437+ for _ , path := range payloadPaths {
438+ assert .True (t , strings .HasPrefix (path , sessionDir ), "All payloads should be in session directory" )
439+ }
440+ }
441+
442+ // TestPayloadStorage_FilePermissions verifies that payload directories and files have secure permissions
443+ func TestPayloadStorage_FilePermissions (t * testing.T ) {
444+ baseDir := t .TempDir ()
445+ sessionID := "test-permissions"
446+ queryID := "test-query-perms"
447+ payload := []byte (`{"secure": "data"}` )
448+
449+ filePath , err := savePayload (baseDir , sessionID , queryID , payload )
450+ require .NoError (t , err )
451+
452+ // Check directory permissions
453+ dirPath := filepath .Dir (filePath )
454+ dirInfo , err := os .Stat (dirPath )
455+ require .NoError (t , err )
456+ assert .Equal (t , os .FileMode (0700 ), dirInfo .Mode ().Perm (), "Directory should have 0700 permissions" )
457+
458+ // Check file permissions
459+ fileInfo , err := os .Stat (filePath )
460+ require .NoError (t , err )
461+ assert .Equal (t , os .FileMode (0600 ), fileInfo .Mode ().Perm (), "File should have 0600 permissions" )
462+ }
463+
464+ // TestPayloadStorage_DefaultSessionID verifies behavior when session ID is empty
465+ func TestPayloadStorage_DefaultSessionID (t * testing.T ) {
466+ baseDir := t .TempDir ()
467+
468+ // Return empty string to trigger default behavior
469+ getEmptySessionID := func (ctx context.Context ) string { return "" }
470+
471+ mockHandler := func (ctx context.Context , req * sdk.CallToolRequest , args interface {}) (* sdk.CallToolResult , interface {}, error ) {
472+ return & sdk.CallToolResult {IsError : false }, map [string ]interface {}{"test" : "data" }, nil
473+ }
474+
475+ wrapped := WrapToolHandler (mockHandler , "test_tool" , baseDir , getEmptySessionID )
476+ _ , data , err := wrapped (context .Background (), & sdk.CallToolRequest {}, map [string ]interface {}{})
477+ require .NoError (t , err )
478+
479+ dataMap := data .(map [string ]interface {})
480+ payloadPath := dataMap ["payloadPath" ].(string )
481+
482+ // Verify the payload is stored in "default" session directory
483+ assert .Contains (t , payloadPath , "default" , "Empty session should use 'default' directory" )
484+ assert .FileExists (t , payloadPath , "Payload file should exist" )
485+ }
486+
229487func TestShouldApplyMiddleware (t * testing.T ) {
230488 tests := []struct {
231489 name string
0 commit comments