Files
goclaw/internal/tools/message_test.go
T
viettranx 2504095dfe fix(agents): complete shell deny groups propagation chain
ShellDenyGroups was defined in SystemPromptConfig but lacked full propagation
through parser, Loop fields, context injection, and system prompt population.
Per-agent overrides from other_config JSONB had zero runtime effect.

Changes:
- agent_store.go: Add ParseShellDenyGroups() to extract overrides from JSONB
- loop_types.go: Add shellDenyGroups field to Loop and LoopConfig, wire in NewLoop
- resolver.go: Wire agent-parsed shell deny groups into LoopConfig
- loop.go: Inject shellDenyGroups into context via store.WithShellDenyGroups
- loop_history.go: Populate ShellDenyGroups in system prompt config
- message_test.go: Fix macOS symlink path normalization in test expectations

Fixes test failures on macOS where /var/folders symlinks to /private/var/folders.
2026-03-18 17:04:26 +07:00

133 lines
4.0 KiB
Go

package tools
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestResolveMediaPath(t *testing.T) {
tmpDir := os.TempDir()
// Create a temp workspace with a test file for workspace-relative tests.
workspace := t.TempDir()
docsDir := filepath.Join(workspace, "docs")
if err := os.MkdirAll(docsDir, 0o755); err != nil {
t.Fatal(err)
}
testFile := filepath.Join(docsDir, "report.pdf")
if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
// Normalize paths to canonical form (resolves macOS /var/folders → /private/var/folders symlink).
// The resolvePath function uses filepath.EvalSymlinks, so test expectations must too.
testFileCanonical, _ := filepath.EvalSymlinks(testFile)
workspaceCanonical, _ := filepath.EvalSymlinks(workspace)
t.Run("restricted", func(t *testing.T) {
tool := NewMessageTool(workspaceCanonical, true)
ctx := context.Background()
tests := []struct {
name string
input string
want string
wantOK bool
}{
// /tmp/ always allowed
{"valid temp file", "MEDIA:" + filepath.Join(tmpDir, "test.png"), filepath.Join(tmpDir, "test.png"), true},
{"valid nested temp", "MEDIA:" + filepath.Join(tmpDir, "sub", "file.txt"), filepath.Join(tmpDir, "sub", "file.txt"), true},
// Workspace files allowed
{"workspace absolute", "MEDIA:" + testFileCanonical, testFileCanonical, true},
{"workspace relative", "MEDIA:docs/report.pdf", testFileCanonical, true},
// Not a MEDIA: message
{"no prefix", filepath.Join(tmpDir, "test.png"), "", false},
{"empty after prefix", "MEDIA:", "", false},
{"dot path", "MEDIA:.", "", false},
{"empty string", "", "", false},
{"just MEDIA", "MEDIA", "", false},
// Outside workspace + outside /tmp/ → blocked
{"outside workspace", "MEDIA:/etc/passwd", "", false},
{"traversal attack", "MEDIA:" + filepath.Join(workspaceCanonical, "..", "etc", "passwd"), "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := tool.resolveMediaPath(ctx, tt.input)
if ok != tt.wantOK {
t.Errorf("resolveMediaPath(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
if ok && got != tt.want {
t.Errorf("resolveMediaPath(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
})
t.Run("unrestricted", func(t *testing.T) {
tool := NewMessageTool(workspace, false)
ctx := context.Background()
tests := []struct {
name string
input string
wantOK bool
}{
{"any absolute path", "MEDIA:/etc/hostname", true},
{"workspace relative", "MEDIA:docs/report.pdf", true},
{"temp file", "MEDIA:" + filepath.Join(tmpDir, "test.png"), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, ok := tool.resolveMediaPath(ctx, tt.input)
if ok != tt.wantOK {
t.Errorf("resolveMediaPath(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
})
}
})
t.Run("context workspace override", func(t *testing.T) {
// Tool has no workspace, but context provides one.
tool := NewMessageTool("", true)
ctx := WithToolWorkspace(context.Background(), workspaceCanonical)
got, ok := tool.resolveMediaPath(ctx, "MEDIA:docs/report.pdf")
if !ok {
t.Fatal("expected ok=true for workspace-relative path with context workspace")
}
if got != testFileCanonical {
t.Errorf("got %q, want %q", got, testFileCanonical)
}
})
}
func TestIsInTempDir(t *testing.T) {
tmpDir := os.TempDir()
tests := []struct {
name string
path string
want bool
}{
{"in tmp", filepath.Join(tmpDir, "test.png"), true},
{"nested in tmp", filepath.Join(tmpDir, "sub", "file.txt"), true},
{"tmp itself", tmpDir, false}, // only files inside, not the dir itself
{"outside tmp", "/etc/passwd", false},
{"relative path", "relative/path.txt", false},
{"traversal", filepath.Join(tmpDir, "..", "etc", "passwd"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isInTempDir(tt.path); got != tt.want {
t.Errorf("isInTempDir(%q) = %v, want %v", tt.path, got, tt.want)
}
})
}
}