Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions internal/cli/gateway_runtime_bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,53 @@ func (r *runtimeWithoutCreator) CheckpointDiff(ctx context.Context, input agentr
return r.base.CheckpointDiff(ctx, input)
}

type runtimeWithoutCheckpointer struct {
base *runtimeStub
}

func (r *runtimeWithoutCheckpointer) Submit(ctx context.Context, input agentruntime.PrepareInput) error {
return r.base.Submit(ctx, input)
}
func (r *runtimeWithoutCheckpointer) PrepareUserInput(ctx context.Context, input agentruntime.PrepareInput) (agentruntime.UserInput, error) {
return r.base.PrepareUserInput(ctx, input)
}
func (r *runtimeWithoutCheckpointer) Run(ctx context.Context, input agentruntime.UserInput) error {
return r.base.Run(ctx, input)
}
func (r *runtimeWithoutCheckpointer) Compact(ctx context.Context, input agentruntime.CompactInput) (agentruntime.CompactResult, error) {
return r.base.Compact(ctx, input)
}
func (r *runtimeWithoutCheckpointer) ExecuteSystemTool(ctx context.Context, input agentruntime.SystemToolInput) (tools.ToolResult, error) {
return r.base.ExecuteSystemTool(ctx, input)
}
func (r *runtimeWithoutCheckpointer) ResolvePermission(ctx context.Context, input agentruntime.PermissionResolutionInput) error {
return r.base.ResolvePermission(ctx, input)
}
func (r *runtimeWithoutCheckpointer) CancelActiveRun() bool {
return r.base.CancelActiveRun()
}
func (r *runtimeWithoutCheckpointer) Events() <-chan agentruntime.RuntimeEvent {
return r.base.Events()
}
func (r *runtimeWithoutCheckpointer) ListSessions(ctx context.Context) ([]agentsession.Summary, error) {
return r.base.ListSessions(ctx)
}
func (r *runtimeWithoutCheckpointer) LoadSession(ctx context.Context, id string) (agentsession.Session, error) {
return r.base.LoadSession(ctx, id)
}
func (r *runtimeWithoutCheckpointer) ActivateSessionSkill(ctx context.Context, sessionID string, skillID string) error {
return r.base.ActivateSessionSkill(ctx, sessionID, skillID)
}
func (r *runtimeWithoutCheckpointer) DeactivateSessionSkill(ctx context.Context, sessionID string, skillID string) error {
return r.base.DeactivateSessionSkill(ctx, sessionID, skillID)
}
func (r *runtimeWithoutCheckpointer) ListSessionSkills(ctx context.Context, sessionID string) ([]agentruntime.SessionSkillState, error) {
return r.base.ListSessionSkills(ctx, sessionID)
}
func (r *runtimeWithoutCheckpointer) ListAvailableSkills(ctx context.Context, sessionID string) ([]agentruntime.AvailableSkillState, error) {
return r.base.ListAvailableSkills(ctx, sessionID)
}

type bridgeSessionStoreStub struct {
deleteFn func(ctx context.Context, id string) error
updateFn func(ctx context.Context, input agentsession.UpdateSessionStateInput) error
Expand Down Expand Up @@ -366,6 +413,96 @@ func TestGatewayRuntimePortBridgeCheckpointOperations(t *testing.T) {
}
}

func TestGatewayRuntimePortBridgeCheckpointOperations_ReportConflictAndUnsupportedRuntime(t *testing.T) {
t.Run("conflict forwarded", func(t *testing.T) {
stub := &runtimeStub{
restoreCheckpointOut: agentruntime.RestoreResult{
CheckpointID: "cp-1",
SessionID: "session-1",
Conflict: &checkpoint.ConflictResult{HasConflict: true},
},
undoRestoreOut: agentruntime.RestoreResult{
CheckpointID: "guard-1",
SessionID: "session-1",
Conflict: &checkpoint.ConflictResult{HasConflict: true},
},
}
bridge := &gatewayRuntimePortBridge{runtime: stub}

restoreResult, err := bridge.RestoreCheckpoint(context.Background(), gateway.CheckpointRestoreInput{
SessionID: "session-1",
CheckpointID: "cp-1",
})
if err != nil {
t.Fatalf("RestoreCheckpoint() error = %v", err)
}
if !restoreResult.HasConflict {
t.Fatalf("RestoreCheckpoint() conflict flag = false, want true")
}

undoResult, err := bridge.UndoRestore(context.Background(), gateway.UndoRestoreInput{SessionID: "session-1"})
if err != nil {
t.Fatalf("UndoRestore() error = %v", err)
}
if !undoResult.HasConflict {
t.Fatalf("UndoRestore() conflict flag = false, want true")
}
})

t.Run("unsupported runtime", func(t *testing.T) {
bridge := &gatewayRuntimePortBridge{runtime: &runtimeWithoutCheckpointer{base: &runtimeStub{}}}
cases := []struct {
name string
call func() error
}{
{
name: "list",
call: func() error {
_, err := bridge.ListCheckpoints(context.Background(), gateway.ListCheckpointsInput{SessionID: "session-1"})
return err
},
},
{
name: "restore",
call: func() error {
_, err := bridge.RestoreCheckpoint(context.Background(), gateway.CheckpointRestoreInput{
SessionID: "session-1",
CheckpointID: "cp-1",
})
return err
},
},
{
name: "undo",
call: func() error {
_, err := bridge.UndoRestore(context.Background(), gateway.UndoRestoreInput{SessionID: "session-1"})
return err
},
},
{
name: "diff",
call: func() error {
_, err := bridge.CheckpointDiff(context.Background(), gateway.CheckpointDiffInput{
SessionID: "session-1",
CheckpointID: "cp-1",
})
return err
},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := tc.call()
if err == nil || !strings.Contains(err.Error(), "does not support checkpoint operations") {
t.Fatalf("error = %v, want unsupported checkpoint operations", err)
}
})
}
})
}

var testSessionStore bridgeSessionStore = &bridgeSessionStoreStub{}

func TestNewGatewayRuntimePortBridgeRuntimeUnavailable(t *testing.T) {
Expand Down
43 changes: 43 additions & 0 deletions internal/tools/filesystem/copy_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,46 @@ func TestCopyFileTool_InvalidJSON(t *testing.T) {
t.Fatalf("expected error result")
}
}

func TestCopyFileTool_RejectsDirectorySource(t *testing.T) {
t.Parallel()
workspace := t.TempDir()
sourceDir := filepath.Join(workspace, "srcdir")
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
t.Fatalf("seed dir: %v", err)
}
tool := NewCopy(workspace)
args, _ := json.Marshal(map[string]any{
"source_path": "srcdir",
"destination_path": "copy.txt",
})
_, err := tool.Execute(context.Background(), tools.ToolCallInput{
Name: tool.Name(),
Arguments: args,
Workdir: workspace,
})
if err == nil || !strings.Contains(err.Error(), "must be a file") {
t.Fatalf("expected directory source error, got %v", err)
}
}

func TestCopyFileTool_RejectsCanceledContext(t *testing.T) {
t.Parallel()
workspace := t.TempDir()
tool := NewCopy(workspace)
args, _ := json.Marshal(map[string]any{
"source_path": "src.txt",
"destination_path": "dst.txt",
})
ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err := tool.Execute(ctx, tools.ToolCallInput{
Name: tool.Name(),
Arguments: args,
Workdir: workspace,
})
if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) {
t.Fatalf("expected canceled error, got %v", err)
}
}
92 changes: 92 additions & 0 deletions internal/tools/filesystem/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package filesystem

import (
"errors"
"os"
"path/filepath"
"testing"
)

func TestToRelativePath(t *testing.T) {
t.Parallel()
root := t.TempDir()
inside := filepath.Join(root, "nested", "file.txt")
outside := filepath.Join(filepath.Dir(root), "outside.txt")

if got := toRelativePath(root, inside); got != filepath.Join("nested", "file.txt") {
t.Fatalf("inside path = %q, want nested/file.txt", got)
}
if got := toRelativePath(root, outside); got != filepath.Join("..", "outside.txt") {
t.Fatalf("outside path = %q, want ../outside.txt", got)
}
}

func TestSkipDirEntry(t *testing.T) {
t.Parallel()
root := t.TempDir()
mustCreateDir(t, filepath.Join(root, ".git"))
mustCreateDir(t, filepath.Join(root, "node_modules"))
mustCreateDir(t, filepath.Join(root, "keep"))
mustWriteTestFile(t, filepath.Join(root, ".vscode"), "not-a-dir")

entries, err := os.ReadDir(root)
if err != nil {
t.Fatalf("ReadDir() error = %v", err)
}

got := map[string]bool{}
for _, entry := range entries {
got[entry.Name()] = skipDirEntry(filepath.Join(root, entry.Name()), entry)
}

if !got[".git"] {
t.Fatalf(".git skip = false, want true")
}
if !got["node_modules"] {
t.Fatalf("node_modules skip = false, want true")
}
if got["keep"] {
t.Fatalf("keep skip = true, want false")
}
if got[".vscode"] {
t.Fatalf(".vscode file skip = true, want false for non-directory")
}
}

func TestIsCrossDeviceLinkError(t *testing.T) {
t.Parallel()

cases := []struct {
name string
err error
want bool
}{
{name: "nil", err: nil, want: false},
{name: "other", err: errors.New("permission denied"), want: false},
{name: "cross-device", err: errors.New("invalid cross-device link"), want: true},
{name: "exdev", err: errors.New("rename failed: EXDEV"), want: true},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if got := isCrossDeviceLinkError(tc.err); got != tc.want {
t.Fatalf("isCrossDeviceLinkError(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}

func mustCreateDir(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", path, err)
}
}

func mustWriteTestFile(t *testing.T, path string, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", path, err)
}
}
43 changes: 43 additions & 0 deletions internal/tools/filesystem/move_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,46 @@ func TestMoveFileTool_InvalidJSON(t *testing.T) {
t.Fatalf("expected error result")
}
}

func TestMoveFileTool_RejectsDirectorySource(t *testing.T) {
t.Parallel()
workspace := t.TempDir()
sourceDir := filepath.Join(workspace, "srcdir")
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
t.Fatalf("seed dir: %v", err)
}
tool := NewMove(workspace)
args, _ := json.Marshal(map[string]any{
"source_path": "srcdir",
"destination_path": "moved.txt",
})
_, err := tool.Execute(context.Background(), tools.ToolCallInput{
Name: tool.Name(),
Arguments: args,
Workdir: workspace,
})
if err == nil || !strings.Contains(err.Error(), "must be a file") {
t.Fatalf("expected directory source error, got %v", err)
}
}

func TestMoveFileTool_RejectsCanceledContext(t *testing.T) {
t.Parallel()
workspace := t.TempDir()
tool := NewMove(workspace)
args, _ := json.Marshal(map[string]any{
"source_path": "src.txt",
"destination_path": "dst.txt",
})
ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err := tool.Execute(ctx, tools.ToolCallInput{
Name: tool.Name(),
Arguments: args,
Workdir: workspace,
})
if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) {
t.Fatalf("expected canceled error, got %v", err)
}
}
Loading