| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- package runner
- import (
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "syscall"
- "testing"
- "time"
- "github.com/kotoyuuko/cc-switch-cli/internal/config"
- )
- // makeExec writes an executable script at dir/name and returns its path.
- func makeExec(t *testing.T, dir, name, body string) string {
- t.Helper()
- p := filepath.Join(dir, name)
- if err := os.WriteFile(p, []byte(body), 0o755); err != nil {
- t.Fatal(err)
- }
- return p
- }
- func TestResolveClaudePath_FromConfig(t *testing.T) {
- dir := t.TempDir()
- p := makeExec(t, dir, "claude", "#!/bin/sh\nexit 0\n")
- got, err := ResolveClaudePath(config.Config{ClaudePath: p})
- if err != nil {
- t.Fatalf("unexpected: %v", err)
- }
- if got != p {
- t.Errorf("got %q want %q", got, p)
- }
- }
- func TestResolveClaudePath_FromPATH(t *testing.T) {
- dir := t.TempDir()
- _ = makeExec(t, dir, "claude", "#!/bin/sh\nexit 0\n")
- t.Setenv("PATH", dir)
- got, err := ResolveClaudePath(config.Config{})
- if err != nil {
- t.Fatalf("unexpected: %v", err)
- }
- // LookPath may return a symlink-resolved form; just check it matches.
- if !strings.HasSuffix(got, "/claude") {
- t.Errorf("unexpected path: %q", got)
- }
- }
- func TestResolveClaudePath_NotFound(t *testing.T) {
- t.Setenv("PATH", t.TempDir()) // empty dir on PATH
- _, err := ResolveClaudePath(config.Config{})
- if err == nil {
- t.Fatal("expected error")
- }
- if !strings.Contains(err.Error(), "config set claude-path") {
- t.Errorf("missing hint: %v", err)
- }
- }
- func TestResolveClaudePath_NonExecutable(t *testing.T) {
- dir := t.TempDir()
- p := filepath.Join(dir, "claude")
- if err := os.WriteFile(p, []byte(""), 0o644); err != nil {
- t.Fatal(err)
- }
- _, err := ResolveClaudePath(config.Config{ClaudePath: p})
- if err == nil {
- t.Fatal("expected non-executable error")
- }
- }
- func TestRun_EnvInjected(t *testing.T) {
- dir := t.TempDir()
- out := filepath.Join(dir, "out")
- // Capture FOO's value to a file and exit 0.
- script := makeExec(t, dir, "test", "#!/bin/sh\nprintf '%s' \"$FOO\" > "+out+"\nexit 0\n")
- code, err := Run(script, []string{"FOO=bar", "PATH=/usr/bin"}, nil)
- if err != nil {
- t.Fatalf("Run: %v", err)
- }
- if code != 0 {
- t.Errorf("exit code = %d", code)
- }
- b, err := os.ReadFile(out)
- if err != nil {
- t.Fatal(err)
- }
- if string(b) != "bar" {
- t.Errorf("FOO not injected: %q", string(b))
- }
- }
- func TestRun_ExitCodePassthrough(t *testing.T) {
- dir := t.TempDir()
- script := makeExec(t, dir, "test", "#!/bin/sh\nexit 42\n")
- code, err := Run(script, nil, nil)
- if err != nil {
- t.Fatalf("Run: %v", err)
- }
- if code != 42 {
- t.Errorf("exit code = %d, want 42", code)
- }
- }
- func TestRun_SignalForwarding(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("signal forwarding is Unix-only")
- }
- dir := t.TempDir()
- // Child traps INT, exits with 130 (128+2).
- script := makeExec(t, dir, "trapper", "#!/bin/sh\ntrap 'exit 130' INT\nsleep 5\n")
- // Use a goroutine to launch Run, then send SIGINT to our own pid (Run
- // installs the handler which forwards it to the child).
- type result struct {
- code int
- err error
- }
- done := make(chan result, 1)
- go func() {
- c, e := Run(script, nil, nil)
- done <- result{c, e}
- }()
- // Give the child a moment to install its trap.
- time.Sleep(200 * time.Millisecond)
- if err := syscall.Kill(os.Getpid(), syscall.SIGINT); err != nil {
- t.Fatal(err)
- }
- select {
- case r := <-done:
- if r.err != nil {
- t.Fatalf("Run: %v", r.err)
- }
- if r.code != 130 {
- t.Errorf("exit code = %d, want 130", r.code)
- }
- case <-time.After(3 * time.Second):
- t.Fatal("timed out waiting for child to exit")
- }
- }
- func TestRun_SignalKill_128Plus(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("Unix-only")
- }
- // Child that ignores nothing and just sleeps; we kill it with SIGKILL
- // directly to avoid the signal-forwarding path (which would route via the
- // current process first).
- dir := t.TempDir()
- script := makeExec(t, dir, "sleeper", "#!/bin/sh\nsleep 3\n")
- cmd := exec.Command(script)
- if err := cmd.Start(); err != nil {
- t.Fatal(err)
- }
- go func() {
- time.Sleep(100 * time.Millisecond)
- _ = cmd.Process.Signal(syscall.SIGKILL)
- }()
- code, _ := asExitError(cmd.Wait())
- if code != 128+int(syscall.SIGKILL) {
- t.Errorf("exit code = %d, want %d", code, 128+int(syscall.SIGKILL))
- }
- }
|