diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31558e1..5f04838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,10 @@ on: jobs: test: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/codeagent-wrapper/internal/app/os_paths_test.go b/codeagent-wrapper/internal/app/os_paths_test.go new file mode 100644 index 0000000..8643fca --- /dev/null +++ b/codeagent-wrapper/internal/app/os_paths_test.go @@ -0,0 +1,46 @@ +package wrapper + +import ( + "os" + "testing" +) + +func TestParseArgs_Workdir_OSPaths(t *testing.T) { + oldArgv := os.Args + t.Cleanup(func() { os.Args = oldArgv }) + + workdirs := []struct { + name string + path string + }{ + {name: "windows drive forward slashes", path: "D:/repo/path"}, + {name: "windows drive backslashes", path: `C:\repo\path`}, + {name: "windows UNC", path: `\\server\share\repo`}, + {name: "unix absolute", path: "/home/user/repo"}, + {name: "relative", path: "./relative/repo"}, + } + + for _, wd := range workdirs { + t.Run("new mode: "+wd.name, func(t *testing.T) { + os.Args = []string{"codeagent-wrapper", "task", wd.path} + cfg, err := parseArgs() + if err != nil { + t.Fatalf("parseArgs() error: %v", err) + } + if cfg.Mode != "new" || cfg.Task != "task" || cfg.WorkDir != wd.path { + t.Fatalf("cfg mismatch: got mode=%q task=%q workdir=%q, want mode=%q task=%q workdir=%q", cfg.Mode, cfg.Task, cfg.WorkDir, "new", "task", wd.path) + } + }) + + t.Run("resume mode: "+wd.name, func(t *testing.T) { + os.Args = []string{"codeagent-wrapper", "resume", "sid-1", "task", wd.path} + cfg, err := parseArgs() + if err != nil { + t.Fatalf("parseArgs() error: %v", err) + } + if cfg.Mode != "resume" || cfg.SessionID != "sid-1" || cfg.Task != "task" || cfg.WorkDir != wd.path { + t.Fatalf("cfg mismatch: got mode=%q sid=%q task=%q workdir=%q, want mode=%q sid=%q task=%q workdir=%q", cfg.Mode, cfg.SessionID, cfg.Task, cfg.WorkDir, "resume", "sid-1", "task", wd.path) + } + }) + } +} diff --git a/codeagent-wrapper/internal/app/stdin_mode_test.go b/codeagent-wrapper/internal/app/stdin_mode_test.go new file mode 100644 index 0000000..8b659a9 --- /dev/null +++ b/codeagent-wrapper/internal/app/stdin_mode_test.go @@ -0,0 +1,119 @@ +package wrapper + +import ( + "strings" + "testing" +) + +func TestRunSingleMode_UseStdin_TargetArgAndTaskText(t *testing.T) { + defer resetTestHooks() + + t.Setenv("TMPDIR", t.TempDir()) + logger, err := NewLogger() + if err != nil { + t.Fatalf("NewLogger(): %v", err) + } + setLogger(logger) + t.Cleanup(func() { _ = closeLogger() }) + + type testCase struct { + name string + cfgTask string + explicit bool + stdinData string + isTerminal bool + + wantUseStdin bool + wantTarget string + wantTaskText string + } + + longTask := strings.Repeat("a", 801) + + tests := []testCase{ + { + name: "piped input forces stdin mode", + cfgTask: "cli-task", + stdinData: "piped task text", + isTerminal: false, + wantUseStdin: true, + wantTarget: "-", + wantTaskText: "piped task text", + }, + { + name: "explicit dash forces stdin mode", + cfgTask: "-", + explicit: true, + stdinData: "explicit task text", + isTerminal: true, + wantUseStdin: true, + wantTarget: "-", + wantTaskText: "explicit task text", + }, + { + name: "special char backslash forces stdin mode", + cfgTask: `C:\repo\file.go`, + isTerminal: true, + wantUseStdin: true, + wantTarget: "-", + wantTaskText: `C:\repo\file.go`, + }, + { + name: "length>800 forces stdin mode", + cfgTask: longTask, + isTerminal: true, + wantUseStdin: true, + wantTarget: "-", + wantTaskText: longTask, + }, + { + name: "simple task uses argv target", + cfgTask: "analyze code", + isTerminal: true, + wantUseStdin: false, + wantTarget: "analyze code", + wantTaskText: "analyze code", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotTarget string + buildCodexArgsFn = func(cfg *Config, targetArg string) []string { + gotTarget = targetArg + return []string{targetArg} + } + + var gotTask TaskSpec + runTaskFn = func(task TaskSpec, silent bool, timeout int) TaskResult { + gotTask = task + return TaskResult{ExitCode: 0, Message: "ok"} + } + + stdinReader = strings.NewReader(tt.stdinData) + isTerminalFn = func() bool { return tt.isTerminal } + + cfg := &Config{ + Mode: "new", + Task: tt.cfgTask, + WorkDir: defaultWorkdir, + Backend: defaultBackendName, + ExplicitStdin: tt.explicit, + } + + if code := runSingleMode(cfg, "codeagent-wrapper"); code != 0 { + t.Fatalf("runSingleMode() = %d, want 0", code) + } + + if gotTarget != tt.wantTarget { + t.Fatalf("targetArg = %q, want %q", gotTarget, tt.wantTarget) + } + if gotTask.UseStdin != tt.wantUseStdin { + t.Fatalf("taskSpec.UseStdin = %v, want %v", gotTask.UseStdin, tt.wantUseStdin) + } + if gotTask.Task != tt.wantTaskText { + t.Fatalf("taskSpec.Task = %q, want %q", gotTask.Task, tt.wantTaskText) + } + }) + } +} diff --git a/codeagent-wrapper/internal/backend/codex_paths_test.go b/codeagent-wrapper/internal/backend/codex_paths_test.go new file mode 100644 index 0000000..072c433 --- /dev/null +++ b/codeagent-wrapper/internal/backend/codex_paths_test.go @@ -0,0 +1,54 @@ +package backend + +import ( + "reflect" + "testing" + + config "codeagent-wrapper/internal/config" +) + +func TestBuildCodexArgs_Workdir_OSPaths(t *testing.T) { + t.Setenv("CODEX_BYPASS_SANDBOX", "false") + + tests := []struct { + name string + workdir string + }{ + {name: "windows drive forward slashes", workdir: "D:/repo/path"}, + {name: "windows drive backslashes", workdir: `C:\repo\path`}, + {name: "windows UNC", workdir: `\\server\share\repo`}, + {name: "unix absolute", workdir: "/home/user/repo"}, + {name: "relative", workdir: "./relative/repo"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.Config{Mode: "new", WorkDir: tt.workdir} + got := BuildCodexArgs(cfg, "task") + want := []string{"e", "--skip-git-repo-check", "-C", tt.workdir, "--json", "task"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("BuildCodexArgs() = %v, want %v", got, want) + } + }) + } + + t.Run("new mode stdin target uses dash", func(t *testing.T) { + cfg := &config.Config{Mode: "new", WorkDir: `C:\repo\path`} + got := BuildCodexArgs(cfg, "-") + want := []string{"e", "--skip-git-repo-check", "-C", `C:\repo\path`, "--json", "-"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("BuildCodexArgs() = %v, want %v", got, want) + } + }) +} + +func TestBuildCodexArgs_ResumeMode_OmitsWorkdir(t *testing.T) { + t.Setenv("CODEX_BYPASS_SANDBOX", "false") + + cfg := &config.Config{Mode: "resume", SessionID: "sid-123", WorkDir: `C:\repo\path`} + got := BuildCodexArgs(cfg, "-") + want := []string{"e", "--skip-git-repo-check", "--json", "resume", "sid-123", "-"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("BuildCodexArgs() = %v, want %v", got, want) + } +}