test: add unit tests for edit path, cd, guard, ls helpers

- resolveEditPath: absolute blocked, ~ maps to workspace, traversal
  blocked, symlink escape blocked, valid relative works
- shortenHomePath: home/subpath/other cases
- isLsCommand, hasLongFlag, isPermString: table-driven tests
- guardCommand: verify ./executable is not blocked
- handleExtensionCommand: verify :) :D 🤔 pass through

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seagochen 2026-02-26 16:15:42 +09:00
parent 027a1b3ee7
commit 9fcce86fbd
4 changed files with 226 additions and 0 deletions

View file

@ -0,0 +1,104 @@
package agent
import (
"os"
"path/filepath"
"testing"
)
func TestResolveEditPath_AbsoluteBlocked(t *testing.T) {
workspace := t.TempDir()
_, err := resolveEditPath("/etc/passwd", workspace, workspace)
if err == nil {
t.Fatal("Expected absolute path outside workspace to be blocked")
}
}
func TestResolveEditPath_TildeIsWorkspace(t *testing.T) {
workspace := t.TempDir()
// Create a file so the path resolves
os.WriteFile(filepath.Join(workspace, "test.txt"), []byte("hi"), 0o644)
path, err := resolveEditPath("~/test.txt", workspace, workspace)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expected := filepath.Join(workspace, "test.txt")
if path != expected {
t.Errorf("Expected %s, got %s", expected, path)
}
}
func TestResolveEditPath_BareTildeIsWorkspace(t *testing.T) {
workspace := t.TempDir()
path, err := resolveEditPath("~", workspace, workspace)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if path != workspace {
t.Errorf("Expected %s, got %s", workspace, path)
}
}
func TestResolveEditPath_TraversalBlocked(t *testing.T) {
workspace := t.TempDir()
_, err := resolveEditPath("../../etc/passwd", workspace, workspace)
if err == nil {
t.Fatal("Expected path traversal to be blocked")
}
}
func TestResolveEditPath_SymlinkBlocked(t *testing.T) {
root := t.TempDir()
workspace := filepath.Join(root, "workspace")
os.MkdirAll(workspace, 0o755)
secret := filepath.Join(root, "secret.txt")
os.WriteFile(secret, []byte("secret"), 0o644)
link := filepath.Join(workspace, "link.txt")
if err := os.Symlink(secret, link); err != nil {
t.Skip("symlinks not supported")
}
_, err := resolveEditPath("link.txt", workspace, workspace)
if err == nil {
t.Fatal("Expected symlink escape to be blocked")
}
}
func TestResolveEditPath_ValidRelative(t *testing.T) {
workspace := t.TempDir()
subdir := filepath.Join(workspace, "subdir")
os.MkdirAll(subdir, 0o755)
testFile := filepath.Join(subdir, "test.txt")
os.WriteFile(testFile, []byte("content"), 0o644)
path, err := resolveEditPath("subdir/test.txt", workspace, workspace)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if path != testFile {
t.Errorf("Expected %s, got %s", testFile, path)
}
}
func TestShortenHomePath(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Skip("cannot get home dir")
}
tests := []struct {
input string
expected string
}{
{home, "~"},
{filepath.Join(home, "projects"), "~/projects"},
{"/tmp/other", "/tmp/other"},
}
for _, tt := range tests {
result := shortenHomePath(tt.input)
if result != tt.expected {
t.Errorf("shortenHomePath(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}

View file

@ -631,3 +631,37 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
t.Errorf("Expected history to be compressed (len < 8), got %d", len(finalHistory))
}
}
// TestHandleExtensionCommand_EmojiPassthrough verifies that emoji-like
// messages starting with : are not intercepted as commands.
func TestHandleExtensionCommand_EmojiPassthrough(t *testing.T) {
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: t.TempDir(),
Model: "test-model",
MaxTokens: 4096,
},
},
}
msgBus := bus.NewMessageBus()
provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider)
emojiInputs := []string{":)", ":D", ":heart:", ":thinking:", ":-)", ":100:"}
for _, input := range emojiInputs {
_, handled := al.handleExtensionCommand(input)
if handled {
t.Errorf("Expected %q to pass through (not handled), but it was handled", input)
}
}
// Known commands should still be handled
knownCommands := []string{":help", ":usage"}
for _, cmd := range knownCommands {
_, handled := al.handleExtensionCommand(cmd)
if !handled {
t.Errorf("Expected %q to be handled, but it was not", cmd)
}
}
}

View file

@ -0,0 +1,64 @@
package agent
import "testing"
func TestIsLsCommand(t *testing.T) {
tests := []struct {
cmd string
want bool
}{
{"ls", true},
{"ls -la", true},
{"ls /tmp", true},
{"lsof", false},
{"echo ls", false},
{"", false},
}
for _, tt := range tests {
if got := isLsCommand(tt.cmd); got != tt.want {
t.Errorf("isLsCommand(%q) = %v, want %v", tt.cmd, got, tt.want)
}
}
}
func TestHasLongFlag(t *testing.T) {
tests := []struct {
cmd string
want bool
}{
{"ls", false},
{"ls -l", true},
{"ls -la", true},
{"ls -al", true},
{"ls --color", false},
{"ls -a /tmp", false},
{"ls -l --color /tmp", true},
}
for _, tt := range tests {
if got := hasLongFlag(tt.cmd); got != tt.want {
t.Errorf("hasLongFlag(%q) = %v, want %v", tt.cmd, got, tt.want)
}
}
}
func TestIsPermString(t *testing.T) {
tests := []struct {
s string
want bool
}{
{"drwxr-xr-x", true},
{"-rw-r--r--", true},
{"lrwxrwxrwx", true},
{"-rwsr-xr-x", true}, // setuid
{"drwxrwxrwt", true}, // sticky
{"hello world", false}, // wrong length/chars
{"----------", true},
{"xrwxrwxrwx", false}, // invalid first char
{"", false},
}
for _, tt := range tests {
if got := isPermString(tt.s); got != tt.want {
t.Errorf("isPermString(%q) = %v, want %v", tt.s, got, tt.want)
}
}
}

View file

@ -272,3 +272,27 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
)
}
}
// TestGuardCommand_DotSlashExecutable verifies that ./executable style
// commands are NOT blocked by the path extraction regex in guardCommand.
func TestGuardCommand_DotSlashExecutable(t *testing.T) {
tmpDir := t.TempDir()
tool := NewExecTool(tmpDir, true)
// Create a test script in the workspace
scriptPath := filepath.Join(tmpDir, "test.sh")
os.WriteFile(scriptPath, []byte("#!/bin/sh\necho ok"), 0o755)
ctx := context.Background()
result := tool.Execute(ctx, map[string]any{
"command": "./test.sh",
"working_dir": tmpDir,
})
if result.IsError {
t.Errorf("Expected ./test.sh to be allowed, got error: %s", result.ForLLM)
}
if !strings.Contains(result.ForLLM, "ok") {
t.Errorf("Expected output 'ok', got: %s", result.ForLLM)
}
}