package environment import ( "context" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "reasonix/internal/secrets" ) func TestFormatSectionSortsAndRedacts(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Fatalf("home: %v", err) } section := FormatSection([]ProbeResult{ {Binary: "python3", Found: true, Output: "Python 3.12.0"}, {Binary: "go", Found: true, Output: "go version go1.24 darwin/arm64"}, {Binary: "docker", Error: "not found"}, }, "darwin/arm64", filepath.Join(home, "bin", "bash"), map[string]string{ "python3": filepath.Join(home, ".pyenv", "shims", "python3"), "go": "/opt/homebrew/bin/go", }) for _, want := range []string{ "## Environment", "- OS: darwin/arm64", "- Shell: ~/bin/bash", "Configured tools:\n- go: /opt/homebrew/bin/go\n- python3: ~/.pyenv/shims/python3", "Detected tools:\n- go: go version go1.24 darwin/arm64\n- python3: Python 3.12.0", "Not found or unavailable:\n- docker: not found", } { if !strings.Contains(section, want) { t.Fatalf("section missing %q:\n%s", want, section) } } } func TestRunProbesReportsMissingCommand(t *testing.T) { results := RunProbes(context.Background(), []string{"__reasonix_missing_probe__ --version"}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if results[0].Found { t.Fatalf("missing command marked found: %+v", results[0]) } if results[0].Error != "not found" { t.Fatalf("Error = %q, want not found", results[0].Error) } } func TestRunProbesUsesOverridePathAndFirstLine(t *testing.T) { dir := t.TempDir() toolPath := filepath.Join(dir, "mytool") toolPath = writeProbeTool(t, toolPath, "custom version\nignored") results := RunProbesWithOverrides(context.Background(), []string{"mytool --version"}, map[string]string{"mytool": toolPath}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if !results[0].Found { t.Fatalf("override command not found: %+v", results[0]) } if results[0].Output != "custom version" { t.Fatalf("Output = %q, want first line", results[0].Output) } } func TestRunProbesParsesQuotedStaticArgs(t *testing.T) { resetProbeCacheForTest(t, time.Unix(25, 0)) dir := t.TempDir() toolPath := filepath.Join(dir, "quotedtool") toolPath = writeProbeTool(t, toolPath, "quoted version") results := RunProbesWithOverrides(context.Background(), []string{`quotedtool "--version with spaces"`}, map[string]string{"quotedtool": toolPath}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if !results[0].Found || results[0].Output != "quoted version" { t.Fatalf("quoted probe result = %+v", results[0]) } } func TestRunProbesAllowsStaticEnvAssignment(t *testing.T) { resetProbeCacheForTest(t, time.Unix(35, 0)) dir := t.TempDir() toolPath := filepath.Join(dir, "envtool") toolPath = writeEnvProbeTool(t, toolPath) results := RunProbesWithOverrides(context.Background(), []string{`REASONIX_PROBE_ENV=ok envtool --version`}, map[string]string{"envtool": toolPath}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if !results[0].Found || results[0].Output != "ok" { t.Fatalf("env probe result = %+v", results[0]) } } func TestRunProbesAllowsStaticStderrMerge(t *testing.T) { resetProbeCacheForTest(t, time.Unix(40, 0)) dir := t.TempDir() toolPath := filepath.Join(dir, "stderrtool") toolPath = writeStderrProbeTool(t, toolPath, "stderr version") results := RunProbesWithOverrides(context.Background(), []string{`stderrtool --version 2>&1`}, map[string]string{"stderrtool": toolPath}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if !results[0].Found || results[0].Output != "stderr version" { t.Fatalf("stderr merge probe result = %+v", results[0]) } } func TestRunProbesRejectsDeniedOverridePath(t *testing.T) { resetProbeCacheForTest(t, time.Unix(50, 0)) dir := t.TempDir() toolPath := filepath.Join(dir, "deniedtool") toolPath = writeProbeTool(t, toolPath, "should not run") results := RunProbesWithOptions(context.Background(), []string{"deniedtool --version"}, ProbeOptions{ Overrides: map[string]string{"deniedtool": toolPath}, DenyRoots: []string{dir}, }) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if results[0].Found || results[0].Error != "not trusted" { t.Fatalf("denied override result = %+v, want not trusted", results[0]) } } func TestRunProbesRejectsDeniedPathHit(t *testing.T) { resetProbeCacheForTest(t, time.Unix(75, 0)) dir := t.TempDir() writeProbeTool(t, filepath.Join(dir, "pathtool"), "should not run") t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) results := RunProbesWithOptions(context.Background(), []string{"pathtool --version"}, ProbeOptions{ DenyRoots: []string{dir}, }) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if results[0].Found || results[0].Error != "not trusted" { t.Fatalf("denied PATH result = %+v, want not trusted", results[0]) } } func TestRunProbesReportsTimeout(t *testing.T) { setProbeTimeoutForTest(t, 200*time.Millisecond) dir := t.TempDir() toolPath := filepath.Join(dir, "slowtool") body := "#!/bin/sh\nsleep 3\n" if runtime.GOOS == "windows" { toolPath += ".bat" body = "@ping 127.0.0.1 -n 4 > nul\r\n" } if err := os.WriteFile(toolPath, []byte(body), 0o755); err != nil { t.Fatalf("write tool: %v", err) } results := RunProbesWithOverrides(context.Background(), []string{"slowtool --version"}, map[string]string{"slowtool": toolPath}) if len(results) != 1 { t.Fatalf("results len = %d, want 1", len(results)) } if results[0].Found { t.Fatalf("timeout command marked found: %+v", results[0]) } if results[0].Error != "timeout" { t.Fatalf("Error = %q, want timeout", results[0].Error) } } func TestPrepareProbeCommandSetsCancellationBudget(t *testing.T) { cmd := exec.Command("reasonix-test-probe") prepareProbeCommand(cmd) if cmd.Cancel == nil { t.Fatal("probe command must install a cancellation hook") } if cmd.WaitDelay != probeWaitDelay { t.Fatalf("WaitDelay = %v, want %v", cmd.WaitDelay, probeWaitDelay) } if runtime.GOOS == "windows" && cmd.SysProcAttr == nil { t.Fatal("probe command must hide console windows on Windows") } } func TestRunProbesCachesByFingerprint(t *testing.T) { resetProbeCacheForTest(t, time.Unix(100, 0)) dir := t.TempDir() toolPath := filepath.Join(dir, "cachedtool") toolPath = writeProbeTool(t, toolPath, "version one") results := RunProbesWithOverrides(context.Background(), []string{"cachedtool --version"}, map[string]string{"cachedtool": toolPath}) if got := results[0].Output; got != "version one" { t.Fatalf("first Output = %q, want version one", got) } results[0].Output = "mutated" toolPath = writeProbeTool(t, toolPath, "version two") results = RunProbesWithOverrides(context.Background(), []string{"cachedtool --version"}, map[string]string{"cachedtool": toolPath}) if got := results[0].Output; got != "version one" { t.Fatalf("cached Output = %q, want version one", got) } } func TestRunProbesCacheExpires(t *testing.T) { now := time.Unix(200, 0) resetProbeCacheForTest(t, now) dir := t.TempDir() toolPath := filepath.Join(dir, "expiringtool") toolPath = writeProbeTool(t, toolPath, "version one") results := RunProbesWithOverrides(context.Background(), []string{"expiringtool --version"}, map[string]string{"expiringtool": toolPath}) if got := results[0].Output; got != "version one" { t.Fatalf("first Output = %q, want version one", got) } toolPath = writeProbeTool(t, toolPath, "version two") setProbeNowForTest(now.Add(probeCacheTTL + time.Second)) results = RunProbesWithOverrides(context.Background(), []string{"expiringtool --version"}, map[string]string{"expiringtool": toolPath}) if got := results[0].Output; got != "version two" { t.Fatalf("expired Output = %q, want version two", got) } } func TestRunProbesCacheSeparatesOverrides(t *testing.T) { resetProbeCacheForTest(t, time.Unix(300, 0)) dir := t.TempDir() toolOne := filepath.Join(dir, "override-one") toolTwo := filepath.Join(dir, "override-two") toolOne = writeProbeTool(t, toolOne, "version one") toolTwo = writeProbeTool(t, toolTwo, "version two") results := RunProbesWithOverrides(context.Background(), []string{"overridetool --version"}, map[string]string{"overridetool": toolOne}) if got := results[0].Output; got != "version one" { t.Fatalf("first override Output = %q, want version one", got) } results = RunProbesWithOverrides(context.Background(), []string{"overridetool --version"}, map[string]string{"overridetool": toolTwo}) if got := results[0].Output; got != "version two" { t.Fatalf("second override Output = %q, want version two", got) } } func TestFormatSectionLimitsToolOutput(t *testing.T) { overrides := map[string]string{} var results []ProbeResult for i := 0; i < maxRenderedTools+2; i++ { name := fmt.Sprintf("tool%02d", i) overrides[name] = "/bin/" + name results = append(results, ProbeResult{Binary: name, Found: true, Output: "ok"}) results = append(results, ProbeResult{Binary: "missing" + name, Error: "not found"}) } section := FormatSection(results, "test/os", "", overrides) for _, want := range []string{ "- ... 2 more configured tools omitted", "- ... 2 more detected tools omitted", "- ... 2 more unavailable tools omitted", } { if !strings.Contains(section, want) { t.Fatalf("section missing %q:\n%s", want, section) } } } func writeProbeTool(t *testing.T, path, output string) string { t.Helper() setProbeTimeoutForTest(t, 10*time.Second) body := "#!/bin/sh\nprintf '%s\\n'\n" body = fmt.Sprintf(body, strings.ReplaceAll(output, "'", "'\\''")) if runtime.GOOS == "windows" { if !strings.HasSuffix(path, ".bat") { path += ".bat" } body = "@echo " + strings.ReplaceAll(output, "\n", "\r\n@echo ") + "\r\n" } if err := os.WriteFile(path, []byte(body), 0o755); err != nil { t.Fatalf("write tool: %v", err) } return path } func writeEnvProbeTool(t *testing.T, path string) string { t.Helper() setProbeTimeoutForTest(t, 10*time.Second) body := "#!/bin/sh\nprintf '%s\\n' \"$REASONIX_PROBE_ENV\"\n" if runtime.GOOS == "windows" { if !strings.HasSuffix(path, ".bat") { path += ".bat" } body = "@echo %REASONIX_PROBE_ENV%\r\n" } if err := os.WriteFile(path, []byte(body), 0o755); err != nil { t.Fatalf("write env tool: %v", err) } return path } func writeStderrProbeTool(t *testing.T, path, output string) string { t.Helper() setProbeTimeoutForTest(t, 10*time.Second) body := "#!/bin/sh\nprintf '%s\\n' >&2\n" body = fmt.Sprintf(body, strings.ReplaceAll(output, "'", "'\\''")) if runtime.GOOS == "windows" { if !strings.HasSuffix(path, ".bat") { path += ".bat" } body = "@echo " + strings.ReplaceAll(output, "\n", "\r\n@echo ") + " 1>&2\r\n" } if err := os.WriteFile(path, []byte(body), 0o755); err != nil { t.Fatalf("write stderr tool: %v", err) } return path } func resetProbeCacheForTest(t *testing.T, now time.Time) { t.Helper() setProbeNowForTest(now) probeCacheMu.Lock() probeCache = map[string]probeCacheEntry{} probeInflightCalls = map[string]*probeInflight{} probeCacheMu.Unlock() t.Cleanup(func() { probeCacheMu.Lock() probeCache = map[string]probeCacheEntry{} probeInflightCalls = map[string]*probeInflight{} probeNow = time.Now probeTimeout = ProbeTimeout probeCacheMu.Unlock() }) } func setProbeNowForTest(now time.Time) { probeCacheMu.Lock() probeNow = func() time.Time { return now } probeCacheMu.Unlock() } func setProbeTimeoutForTest(t *testing.T, timeout time.Duration) { t.Helper() probeCacheMu.Lock() probeTimeout = timeout probeCacheMu.Unlock() t.Cleanup(func() { probeCacheMu.Lock() probeTimeout = ProbeTimeout probeCacheMu.Unlock() }) } func TestRunProbesFilterSubprocessEnv(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("POSIX shell probe tool") } resetProbeCacheForTest(t, time.Unix(90, 0)) setProbeTimeoutForTest(t, 10*time.Second) dir := t.TempDir() toolPath := filepath.Join(dir, "envtool") body := "#!/bin/sh\nprintf 'tok=%s' \"${REASONIX_TEST_SECRET_TOKEN:-none}\"\n" if err := os.WriteFile(toolPath, []byte(body), 0o755); err != nil { t.Fatal(err) } t.Setenv("REASONIX_TEST_SECRET_TOKEN", "ghp_abcdefghijklmnopqrstuvwxyz") secrets.SetFilterSubprocessEnv(true) t.Cleanup(func() { secrets.SetFilterSubprocessEnv(false) }) results := RunProbesWithOverrides(context.Background(), []string{"envtool --version"}, map[string]string{"envtool": toolPath}) if len(results) != 1 || !results[0].Found { t.Fatalf("probe result = %+v", results) } // Probes declaring no extra env of their own must still get the filtered // environment, not inherit the full one. if results[0].Output != "tok=none" { t.Fatalf("probe leaked filtered env: output = %q", results[0].Output) } }