Files
multica/server/internal/daemon/profile_path_executable_test.go
leilei3167 f9f5e3b81f MUL-6734 fix(daemon): resolve Windows set-path overrides via LookPath (#7623)
`multica runtime profile set-path` was accepted on Windows but the daemon never
used the override: the gate required a Unix executable bit, which Go never sets
on Windows files (0666/0444 for regular files; 0111 only on directories). Every
override — including a plain .exe — read as not executable, so the daemon fell
back to PATH: either failing registration with a message that blamed the user's
filesystem, or silently registering a different install than the pinned one.

appendProfileRuntimes now resolves the override through resolveAgentExecutablePath
(exec.LookPath) — the same contract agent launches use — and records the resolved
path rather than the raw override, so Windows PATHEXT completion picks up .cmd
shims and extension-less pins. Unix exec-bit checks are unchanged, and a stale or
mistyped override still falls back to PATH with the existing warning.

Regression coverage runs on the real Windows runner via the existing
windows-execenv job.

Fixes #7613
2026-09-17 17:39:48 +08:00

89 lines
2.7 KiB
Go

package daemon
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// TestResolveAgentExecutablePath_ProfileOverride is the set-path gate
// regression for MUL-3284 / #7613. appendProfileRuntimes must reuse
// resolveAgentExecutablePath (exec.LookPath) so Windows PATHEXT completion
// and unix exec-bit checks match what agent launch already does — including
// assigning LookPath's return value, not the raw override.
func TestResolveAgentExecutablePath_ProfileOverride(t *testing.T) {
dir := t.TempDir()
if runtime.GOOS == "windows" {
t.Setenv("PATHEXT", ".COM;.EXE;.BAT;.CMD")
exe := filepath.Join(dir, "tool.exe")
if err := os.WriteFile(exe, []byte("MZ"), 0o644); err != nil {
t.Fatal(err)
}
got, err := resolveAgentExecutablePath(exe)
if err != nil {
t.Fatalf("resolveAgentExecutablePath(%q): %v", exe, err)
}
if got != exe {
t.Fatalf("resolved .exe = %q, want %q", got, exe)
}
cmd := filepath.Join(dir, "tool.cmd")
if err := os.WriteFile(cmd, []byte("@echo off\n"), 0o644); err != nil {
t.Fatal(err)
}
got, err = resolveAgentExecutablePath(cmd)
if err != nil {
t.Fatalf("resolveAgentExecutablePath(%q): %v", cmd, err)
}
if got != cmd {
t.Fatalf("resolved .cmd = %q, want %q", got, cmd)
}
// npm-style shim: operator pins C:\tools\openclaw while only
// openclaw.cmd exists. LookPath must complete the extension.
shimDir := t.TempDir()
shimCmd := filepath.Join(shimDir, "openclaw.cmd")
if err := os.WriteFile(shimCmd, []byte("@echo off\n"), 0o644); err != nil {
t.Fatal(err)
}
extensionless := filepath.Join(shimDir, "openclaw")
got, err = resolveAgentExecutablePath(extensionless)
if err != nil {
t.Fatalf("resolveAgentExecutablePath(%q): %v", extensionless, err)
}
if got != shimCmd {
t.Fatalf("extension-less override resolved to %q, want %q", got, shimCmd)
}
// Non-PATHEXT absolute files are not asserted: on Windows,
// exec.LookPath accepts an absolute path to any existing file.
} else {
tool := filepath.Join(dir, "tool")
if err := os.WriteFile(tool, []byte("#!/bin/sh\n"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := resolveAgentExecutablePath(tool); err == nil {
t.Fatalf("0644 file should not resolve on unix")
}
if err := os.Chmod(tool, 0o755); err != nil {
t.Fatal(err)
}
got, err := resolveAgentExecutablePath(tool)
if err != nil {
t.Fatalf("resolveAgentExecutablePath(%q): %v", tool, err)
}
if got != tool {
t.Fatalf("resolved unix tool = %q, want %q", got, tool)
}
}
if _, err := resolveAgentExecutablePath(dir); err == nil {
t.Fatalf("directory should not resolve")
}
if _, err := resolveAgentExecutablePath(filepath.Join(dir, "missing")); err == nil {
t.Fatalf("missing path should not resolve")
}
}