mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
The session_start env block previously named the OS but not the shell. When the model reads 'Operating System: Windows' it defaults to POSIX syntax by habit and emits '&&', 'grep', 'head', '2>/dev/null', 'ls -la', '$(pwd)' — none of which work under Windows PowerShell 5.1 or cmd.exe. The shell tool description already carries a shell-specific hint (shellSyntaxHint), but it sits far from the user turn and is drowned out by the agent's own system prompt; the env block sits at the top of every turn and is cached, so that is where the disambiguation belongs. Resolve the shell via the shared shellpath.DetectShell helper and print its base name plus the resolved path. Long-form dialect rules stay in the shell tool description (shellSyntaxHint) so this block stays terse and doesn't diverge from them. The base-name derivation lives in a new shellpath.ShellBaseName helper so the same logic isn't copied into hooks/builtins. No behaviour change on darwin/linux where the shell defaults to $SHELL or /bin/sh; the extra 'Shell:' line lands cleanly in the existing tab-indented env block.
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package shellpath
|
|
|
|
import (
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestShellBaseName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
path string
|
|
expected string
|
|
}{
|
|
{path: "/bin/zsh", expected: "zsh"},
|
|
{path: "/usr/local/bin/fish", expected: "fish"},
|
|
{path: "/bin/sh", expected: "sh"},
|
|
{path: `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, expected: "powershell"},
|
|
{path: `C:\Program Files\PowerShell\7\pwsh.exe`, expected: "pwsh"},
|
|
{path: `C:\Windows\System32\cmd.exe`, expected: "cmd"},
|
|
{path: "", expected: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := ShellBaseName(tt.path); got != tt.expected {
|
|
t.Errorf("ShellBaseName(%q) = %q, want %q", tt.path, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWindowsCmdExe_ComSpec(t *testing.T) {
|
|
t.Setenv("ComSpec", `C:\Custom\cmd.exe`)
|
|
got := WindowsCmdExe()
|
|
if got != `C:\Custom\cmd.exe` {
|
|
t.Errorf("WindowsCmdExe() = %q, want %q", got, `C:\Custom\cmd.exe`)
|
|
}
|
|
}
|
|
|
|
func TestWindowsCmdExe_SystemRoot(t *testing.T) {
|
|
t.Setenv("ComSpec", "")
|
|
t.Setenv("SystemRoot", `C:\Windows`)
|
|
got := WindowsCmdExe()
|
|
want := `C:\Windows` + string(filepath.Separator) + filepath.Join("System32", "cmd.exe")
|
|
if got != want {
|
|
t.Errorf("WindowsCmdExe() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestWindowsCmdExe_Fallback(t *testing.T) {
|
|
t.Setenv("ComSpec", "")
|
|
t.Setenv("SystemRoot", "")
|
|
got := WindowsCmdExe()
|
|
if got != "cmd.exe" {
|
|
t.Errorf("WindowsCmdExe() = %q, want %q", got, "cmd.exe")
|
|
}
|
|
}
|
|
|
|
func TestDetectShell_Unix(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Unix-only test")
|
|
}
|
|
|
|
t.Setenv("SHELL", "/bin/zsh")
|
|
shell, args := DetectShell()
|
|
if shell != "/bin/zsh" {
|
|
t.Errorf("DetectShell() shell = %q, want /bin/zsh", shell)
|
|
}
|
|
if len(args) != 1 || args[0] != "-c" {
|
|
t.Errorf("DetectShell() args = %v, want [-c]", args)
|
|
}
|
|
}
|
|
|
|
func TestDetectShell_Unix_Fallback(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Unix-only test")
|
|
}
|
|
|
|
t.Setenv("SHELL", "")
|
|
shell, _ := DetectShell()
|
|
if shell != "/bin/sh" {
|
|
t.Errorf("DetectShell() shell = %q, want /bin/sh", shell)
|
|
}
|
|
}
|
|
|
|
func TestDefaultUnixShell(t *testing.T) {
|
|
t.Setenv("SHELL", "/usr/local/bin/fish")
|
|
got := defaultUnixShell()
|
|
if got != "/usr/local/bin/fish" {
|
|
t.Errorf("defaultUnixShell() = %q, want /usr/local/bin/fish", got)
|
|
}
|
|
|
|
t.Setenv("SHELL", "")
|
|
got = defaultUnixShell()
|
|
if got != "/bin/sh" {
|
|
t.Errorf("defaultUnixShell() = %q, want /bin/sh", got)
|
|
}
|
|
}
|
|
|
|
func TestWindowsCmdExe_PrefersComSpecOverSystemRoot(t *testing.T) {
|
|
// When both are set, ComSpec should take priority
|
|
t.Setenv("ComSpec", `D:\Tools\cmd.exe`)
|
|
t.Setenv("SystemRoot", `C:\Windows`)
|
|
got := WindowsCmdExe()
|
|
if got != `D:\Tools\cmd.exe` {
|
|
t.Errorf("WindowsCmdExe() = %q, want %q (ComSpec should take priority)", got, `D:\Tools\cmd.exe`)
|
|
}
|
|
}
|
|
|
|
func TestDetectWindowsShell_FallbackUsesAbsolutePath(t *testing.T) {
|
|
// Simulate an environment where no PowerShell is found:
|
|
// set PATH to empty so LookPath won't find pwsh.exe or powershell.exe
|
|
t.Setenv("PATH", "")
|
|
|
|
t.Setenv("ComSpec", "")
|
|
t.Setenv("SystemRoot", `C:\Windows`)
|
|
|
|
shell, args := DetectWindowsShell()
|
|
want := `C:\Windows` + string(filepath.Separator) + filepath.Join("System32", "cmd.exe")
|
|
if shell != want {
|
|
t.Errorf("DetectWindowsShell() shell = %q, want %q", shell, want)
|
|
}
|
|
if len(args) != 1 || args[0] != "/C" {
|
|
t.Errorf("DetectWindowsShell() args = %v, want [/C]", args)
|
|
}
|
|
}
|
|
|
|
func TestInteractiveShellCmd_Unix(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Unix-only test")
|
|
}
|
|
|
|
t.Setenv("SHELL", "/bin/zsh")
|
|
// A hostile message must stay an env value, never argv or shell syntax.
|
|
msg := `pwned"; rm -rf $(HOME) &`
|
|
cmd := InteractiveShellCmd(msg)
|
|
|
|
want := []string{"/bin/zsh", "-i", "-c", `printf '\n%s\n' "$DOCKER_AGENT_SHELL_EXIT_MSG"; exec /bin/zsh`}
|
|
if !slices.Equal(cmd.Args, want) {
|
|
t.Errorf("InteractiveShellCmd() args = %q, want %q", cmd.Args, want)
|
|
}
|
|
if !slices.Contains(cmd.Env, "DOCKER_AGENT_SHELL_EXIT_MSG="+msg) {
|
|
t.Errorf("InteractiveShellCmd() env is missing DOCKER_AGENT_SHELL_EXIT_MSG=%q", msg)
|
|
}
|
|
}
|