From fb63ebee6f40f4abb2ec7669413c24a6c32cecab Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 4 May 2026 05:47:25 +0000 Subject: [PATCH] test(checkpoint): cover bridge and filesystem edge cases Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com> --- internal/cli/gateway_runtime_bridge_test.go | 137 ++++++++++++++++++++ internal/tools/filesystem/copy_file_test.go | 43 ++++++ internal/tools/filesystem/helpers_test.go | 92 +++++++++++++ internal/tools/filesystem/move_file_test.go | 43 ++++++ 4 files changed, 315 insertions(+) create mode 100644 internal/tools/filesystem/helpers_test.go diff --git a/internal/cli/gateway_runtime_bridge_test.go b/internal/cli/gateway_runtime_bridge_test.go index 99faa741..f9b48123 100644 --- a/internal/cli/gateway_runtime_bridge_test.go +++ b/internal/cli/gateway_runtime_bridge_test.go @@ -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 @@ -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) { diff --git a/internal/tools/filesystem/copy_file_test.go b/internal/tools/filesystem/copy_file_test.go index e04a40d6..bcada3b7 100644 --- a/internal/tools/filesystem/copy_file_test.go +++ b/internal/tools/filesystem/copy_file_test.go @@ -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) + } +} diff --git a/internal/tools/filesystem/helpers_test.go b/internal/tools/filesystem/helpers_test.go new file mode 100644 index 00000000..3fb75c34 --- /dev/null +++ b/internal/tools/filesystem/helpers_test.go @@ -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) + } +} diff --git a/internal/tools/filesystem/move_file_test.go b/internal/tools/filesystem/move_file_test.go index 9d1d1321..bd95e86d 100644 --- a/internal/tools/filesystem/move_file_test.go +++ b/internal/tools/filesystem/move_file_test.go @@ -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) + } +}