mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-13 01:04:38 +00:00
d4463cde6f
* fix(exec): surface chdir error instead of misleading fork/exec Linux Go forkAndExecInChild conflates chdir + execve failures into one PathError naming only argv0. When agent workspace points to a stale directory, exec reports "fork/exec /usr/bin/gh: no such file" even though the binary exists — the real failure is chdir on cmd.Dir. Two fixes: - loop_context.go: fall back to l.workspace when per-user MkdirAll fails (3 sites: user workspace, dispatched team, auto-resolved team) - shell.go + credentialed_exec.go: preflight cwd via validateExecCwd before exec.Command, so the error names the directory, not the binary Regression test TestValidateExecCwd covers empty / existing / missing / file-instead-of-dir. * feat(upgrade): auto-normalize stale agent workspace paths Detects non-portable workspace values left by deployment migrations (Docker → bare-metal, host path drift) and rewrites them to the current configured base. Registered as data hook 072_normalize_agent_workspaces; runs automatically after `migrate up`. Stale patterns: - /app/workspace/* (Docker container path persisted on bare-metal) - ~ prefix (Go doesn't expand tildes; MkdirAll creates literal ~ dir) Base resolved with gateway precedence: GOCLAW_WORKSPACE env > config.Agents.Defaults.Workspace > default ~/.goclaw/workspace. Idempotent (skips rows already at proposed value). Conservative (leaves absolute non-stale paths untouched as intentional custom config).
67 lines
2.1 KiB
Go
67 lines
2.1 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsStaleWorkspace(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want bool
|
|
}{
|
|
{"docker era app workspace", "/app/workspace/clax", true},
|
|
{"docker era root", "/app/workspace", true},
|
|
{"docker era trailing slash", "/app/workspace/", true},
|
|
{"tilde literal", "~/.goclaw/x-workspace", true},
|
|
{"tilde only", "~", true},
|
|
{"absolute host path", "/var/lib/goclaw/workspace/clax", false},
|
|
{"current dot", ".", false},
|
|
{"empty", "", false},
|
|
{"whitespace", " ", false},
|
|
{"custom absolute (preserve)", "/srv/agents/clax", false},
|
|
{"relative non-tilde", "data/workspace", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := isStaleWorkspace(tc.in); got != tc.want {
|
|
t.Fatalf("isStaleWorkspace(%q) = %v, want %v", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceBase_EnvWins(t *testing.T) {
|
|
t.Setenv("GOCLAW_WORKSPACE", "/var/lib/goclaw/workspace")
|
|
if got := resolveWorkspaceBase(); got != "/var/lib/goclaw/workspace" {
|
|
t.Fatalf("resolveWorkspaceBase() = %q, want /var/lib/goclaw/workspace", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceBase_EnvTilde(t *testing.T) {
|
|
t.Setenv("GOCLAW_WORKSPACE", "~/custom/ws")
|
|
home, _ := os.UserHomeDir()
|
|
want := home + "/custom/ws"
|
|
if got := resolveWorkspaceBase(); got != want {
|
|
t.Fatalf("resolveWorkspaceBase() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceBase_StripsTrailingSlash(t *testing.T) {
|
|
t.Setenv("GOCLAW_WORKSPACE", "/var/lib/goclaw/workspace/")
|
|
if got := resolveWorkspaceBase(); got != "/var/lib/goclaw/workspace" {
|
|
t.Fatalf("resolveWorkspaceBase() = %q, want trimmed", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveWorkspaceBase_FallbackOnMissingConfig(t *testing.T) {
|
|
t.Setenv("GOCLAW_WORKSPACE", "")
|
|
t.Setenv("GOCLAW_CONFIG", "/nonexistent/path/config.json")
|
|
// Should not return empty even if config file missing — falls back through
|
|
// config.Load's IsNotExist branch (which returns defaults) or to default.
|
|
if got := resolveWorkspaceBase(); got == "" {
|
|
t.Fatal("resolveWorkspaceBase() returned empty for missing config; expected fallback")
|
|
}
|
|
}
|