diff --git a/internal/app/skill_setup.go b/internal/app/skill_setup.go index 6c0becf4..2d2abf24 100644 --- a/internal/app/skill_setup.go +++ b/internal/app/skill_setup.go @@ -127,7 +127,7 @@ var ( skillSetupExecutable = os.Executable skillSetupGetwd = os.Getwd skillSetupUserHomeDir = os.UserHomeDir - skillSetupSystemHomeDir = os.UserHomeDir + skillSetupSystemHomeDir = upgrade.ResolveSystemHomeDir skillSetupRemoveAll = os.RemoveAll skillSetupBackupAndRemove = upgrade.BackupAndRemoveSkillDir skillSetupRestoreBackup = upgrade.RestoreSkillPath diff --git a/internal/upgrade/paths.go b/internal/upgrade/paths.go index f0424ae6..d0edb495 100644 --- a/internal/upgrade/paths.go +++ b/internal/upgrade/paths.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "os/user" "path/filepath" "runtime" "sort" @@ -139,9 +140,26 @@ var universalSkillDirs = map[string]bool{ ".amp/skills": true, } +// upgradeCurrentUser is the test seam backing ResolveSystemHomeDir. +var upgradeCurrentUser = user.Current + +// ResolveSystemHomeDir returns the operating-system user's real home directory +// independent of the $HOME environment variable. $HOME may be overridden in +// isolated or verifier contexts, so the app-bundle detection gate +// (allowSystemApps = homeDir == systemHome) must compare against a HOME- +// independent source to actually mean "operating on the real user profile". +// It prefers the OS user database (getpwuid on Unix) and falls back to $HOME +// only when the user record cannot be resolved. +func ResolveSystemHomeDir() (string, error) { + if u, err := upgradeCurrentUser(); err == nil && u.HomeDir != "" { + return u.HomeDir, nil + } + return os.UserHomeDir() +} + var ( upgradeUserHomeDir = os.UserHomeDir - upgradeSystemHomeDir = os.UserHomeDir + upgradeSystemHomeDir = ResolveSystemHomeDir upgradeExecutable = os.Executable upgradeEvalSymlinks = filepath.EvalSymlinks upgradeCopyDir = copyDir diff --git a/internal/upgrade/system_home_test.go b/internal/upgrade/system_home_test.go new file mode 100644 index 00000000..7870742f --- /dev/null +++ b/internal/upgrade/system_home_test.go @@ -0,0 +1,49 @@ +package upgrade + +import ( + "errors" + "os/user" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" +) + +// TestCrossPlatformCoverageSystemHomeIndependentOfHOME pins that the app-bundle +// detection gate compares against a HOME-independent system home: ResolveSystemHomeDir +// prefers the OS user record (getpwuid on Unix) and only falls back to $HOME when that +// record cannot be resolved. Without this, allowSystemApps = homeDir == systemHome is a +// no-op whenever $HOME is overridden, because both sides would honor the same override. +func TestCrossPlatformCoverageSystemHomeIndependentOfHOME(t *testing.T) { + // Success path: the OS user database home is returned as-is. + testseam.Swap(t, &upgradeCurrentUser, func() (*user.User, error) { + return &user.User{HomeDir: "/tmp/dws-real-system-home"}, nil + }) + got, err := ResolveSystemHomeDir() + if err != nil || got != "/tmp/dws-real-system-home" { + t.Fatalf("ResolveSystemHomeDir success = (%q, %v), want /tmp/dws-real-system-home", got, err) + } + + // Fallback path: when the user record is unavailable, fall back to $HOME. + testseam.Swap(t, &upgradeCurrentUser, func() (*user.User, error) { + return nil, errors.New("user record unavailable") + }) + got, err = ResolveSystemHomeDir() + if err != nil { + t.Fatalf("ResolveSystemHomeDir fallback returned error: %v", err) + } + if got == "" { + t.Fatalf("ResolveSystemHomeDir fallback returned empty home") + } + + // Empty HomeDir is also treated as "unresolved" and falls back. + testseam.Swap(t, &upgradeCurrentUser, func() (*user.User, error) { + return &user.User{HomeDir: ""}, nil + }) + got, err = ResolveSystemHomeDir() + if err != nil { + t.Fatalf("ResolveSystemHomeDir empty-home fallback returned error: %v", err) + } + if got == "" { + t.Fatalf("ResolveSystemHomeDir empty-home fallback returned empty home") + } +}