diff --git a/cmd/internal/agentworkspace/delete.go b/cmd/internal/agentworkspace/delete.go index 328c22571..bd35e68ec 100644 --- a/cmd/internal/agentworkspace/delete.go +++ b/cmd/internal/agentworkspace/delete.go @@ -91,11 +91,31 @@ func (cmd *DeleteCmd) Run(ctx context.Context) error { } } - // delete workspace folder + removeWorkspaceFolders(workspaceInfo) + + return nil +} + +// removeWorkspaceFolders deletes the workspace's config folder and cached +// content folder, logging rather than failing on either error since deletion +// should proceed best-effort once the container/daemon are already gone. +func removeWorkspaceFolders(workspaceInfo *provider2.AgentWorkspaceInfo) { if err := forceRemoveAll(workspaceInfo.Origin); err != nil { log.Errorf("remove workspace folder: %v", err) } - return nil + if err := removeContentFolder(workspaceInfo); err != nil { + log.Errorf("remove workspace content folder: %v", err) + } +} + +// removeContentFolder deletes the workspace's cached content folder, unless +// it's the user's own local folder mounted directly rather than a devsy copy. +func removeContentFolder(workspaceInfo *provider2.AgentWorkspaceInfo) error { + if workspaceInfo.ContentFolder == "" || + workspaceInfo.ContentFolder == workspaceInfo.Workspace.Source.LocalFolder { + return nil + } + return forceRemoveAll(workspaceInfo.ContentFolder) } func removeContainer( diff --git a/cmd/internal/agentworkspace/delete_test.go b/cmd/internal/agentworkspace/delete_test.go index 6a7d63a57..2adf84563 100644 --- a/cmd/internal/agentworkspace/delete_test.go +++ b/cmd/internal/agentworkspace/delete_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "runtime" "testing" + + provider2 "github.com/devsy-org/devsy/pkg/provider" ) func skipIfPermissionsNotEnforced(t *testing.T) { @@ -120,3 +122,48 @@ func TestForceRemoveAll_EmptyString(t *testing.T) { t.Fatalf("forceRemoveAll with empty string should not error: %v", err) } } + +func TestRemoveContentFolder_RemovesDevsyManagedContent(t *testing.T) { + dir := t.TempDir() + content := filepath.Join(dir, "contents", "my-workspace") + if err := os.MkdirAll(content, 0o750); err != nil { + t.Fatal(err) + } + + workspaceInfo := &provider2.AgentWorkspaceInfo{ + ContentFolder: content, + Workspace: &provider2.Workspace{}, + } + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder failed: %v", err) + } + assertRemoved(t, content) +} + +func TestRemoveContentFolder_SkipsUsersOwnLocalFolder(t *testing.T) { + dir := t.TempDir() + local := filepath.Join(dir, "my-project") + if err := os.MkdirAll(local, 0o750); err != nil { + t.Fatal(err) + } + + workspaceInfo := &provider2.AgentWorkspaceInfo{ + ContentFolder: local, + Workspace: &provider2.Workspace{ + Source: provider2.WorkspaceSource{LocalFolder: local}, + }, + } + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder failed: %v", err) + } + if _, err := os.Stat(local); err != nil { + t.Fatalf("expected local folder to survive, got stat err: %v", err) + } +} + +func TestRemoveContentFolder_EmptyContentFolder(t *testing.T) { + workspaceInfo := &provider2.AgentWorkspaceInfo{Workspace: &provider2.Workspace{}} + if err := removeContentFolder(workspaceInfo); err != nil { + t.Fatalf("removeContentFolder with empty ContentFolder should not error: %v", err) + } +}