package builtin import ( "context" "encoding/json" "os/exec" "runtime" "strings" "testing" "reasonix/internal/sandbox" ) func powershellPath(t *testing.T) string { t.Helper() for _, n := range []string{"pwsh", "powershell"} { if p, err := exec.LookPath(n); err == nil { return p } } t.Skip("no PowerShell on PATH") return "" } func runPS(t *testing.T, command string) (string, error) { t.Helper() b := bash{shell: sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: powershellPath(t)}} args, _ := json.Marshal(map[string]string{"command": command}) return b.Execute(context.Background(), args) } func TestBashPowerShellRunsNativeCommand(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("powershell e2e is windows-only") } out, err := runPS(t, "Write-Output reasonix-ok") if err != nil { t.Fatalf("powershell command failed: %v (out=%q)", err, out) } if !strings.Contains(out, "reasonix-ok") { t.Fatalf("output = %q, want it to contain reasonix-ok", out) } } func TestBashPowerShellSurfacesNonZeroExit(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("powershell e2e is windows-only") } if _, err := runPS(t, "exit 3"); err == nil { t.Fatal("non-zero exit should surface as an error") } } func TestBashPowerShellRejectsChaining(t *testing.T) { b := bash{shell: sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: "powershell"}} for _, cmd := range []string{"echo a && echo b", "echo a || echo b"} { args, _ := json.Marshal(map[string]string{"command": cmd}) out, err := b.Execute(context.Background(), args) if err == nil { t.Errorf("%q should be rejected on powershell, got out=%q", cmd, out) } else if !strings.Contains(err.Error(), "PowerShell") { t.Errorf("%q error should explain PowerShell, got %v", cmd, err) } } } func TestBashPowerShellAllowsQuotedOperator(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("runs a real powershell command") } // "&&" inside a string literal is data, not chaining — must not be rejected. out, err := runPS(t, `Write-Output "a && b"`) if err != nil { t.Fatalf("quoted && should run: %v (out=%q)", err, out) } if !strings.Contains(out, "a && b") { t.Fatalf("output = %q", out) } } func TestBashPwshAllowsChaining(t *testing.T) { // pwsh (PowerShell 7+) parses && — the guard must not block it. b := bash{shell: sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: "pwsh"}} args, _ := json.Marshal(map[string]string{"command": "echo a && echo b"}) _, err := b.Execute(context.Background(), args) if err != nil && strings.Contains(err.Error(), "does not parse") { t.Errorf("pwsh should not be blocked by the chaining guard: %v", err) } } func TestBashPowerShellOutputIsUTF8(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("powershell e2e is windows-only") } out, err := runPS(t, "Write-Output 'AB-中文-CD'") if err != nil { t.Fatalf("command failed: %v (out=%q)", err, out) } if !strings.Contains(out, "中文") { t.Fatalf("non-ASCII output mojibake — got %q (want it to contain 中文)", out) } } func TestBashDescriptionReflectsShell(t *testing.T) { ps := bash{shell: sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: "powershell"}} psDesc := ps.Description() if !strings.Contains(psDesc, "Windows PowerShell") { t.Errorf("powershell description should name Windows PowerShell: %q", psDesc) } if !strings.Contains(psDesc, "'&&' and '||' are NOT parsed") { t.Errorf("powershell description should warn about unsupported chaining: %q", psDesc) } pwsh := bash{shell: sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: "pwsh"}} pwshDesc := pwsh.Description() if !strings.Contains(pwshDesc, "PowerShell 7 (pwsh)") { t.Errorf("pwsh description should name PowerShell 7: %q", pwshDesc) } if !strings.Contains(pwshDesc, "'&&' and '||' are parsed") { t.Errorf("pwsh description should allow conditional chaining: %q", pwshDesc) } if strings.Contains(pwshDesc, "NOT parsed") { t.Errorf("pwsh description should not reuse the Windows PowerShell chaining warning: %q", pwshDesc) } sh := bash{shell: sandbox.Shell{Kind: sandbox.ShellBash, Path: "bash"}} if strings.Contains(sh.Description(), "PowerShell") { t.Errorf("bash description should not mention PowerShell: %q", sh.Description()) } }