Files
2026-07-13 13:00:08 +08:00

158 lines
5.3 KiB
Go

package doctor
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"reasonix/internal/config"
)
func TestRedactHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home) // os.UserHomeDir on unix
t.Setenv("USERPROFILE", home) // os.UserHomeDir on windows
sep := string(os.PathSeparator)
if got := redactHome(home); got != "~" {
t.Fatalf("home itself: got %q, want ~", got)
}
under := filepath.Join(home, "projects", "x")
if got, want := redactHome(under), "~"+sep+"projects"+sep+"x"; got != want {
t.Fatalf("under home: got %q, want %q", got, want)
}
outside := filepath.Join(t.TempDir(), "elsewhere") // sibling temp, not under home
if got := redactHome(outside); got != outside {
t.Fatalf("outside home must be unchanged: got %q", got)
}
if got := redactHome(""); got != "" {
t.Fatalf("empty must stay empty: got %q", got)
}
}
func TestCollectReportRedactsSecrets(t *testing.T) {
t.Setenv("REASONIX_TEST_SECRET", "sk-live-secret")
cfg := config.Default()
cfg.DefaultModel = "custom"
cfg.Providers = []config.ProviderEntry{{
Name: "custom",
Kind: "openai",
BaseURL: "https://api.example.com/v1?token=secret-query",
Model: "model-a",
APIKeyEnv: "REASONIX_TEST_SECRET",
}}
cfg.Plugins = []config.PluginEntry{{
Name: "remote",
Type: "http",
URL: "https://mcp.example.com/path?api_key=secret-query",
Headers: map[string]string{"Authorization": "Bearer sk-live-secret"},
}}
cfg.Network = config.NetworkConfig{
ProxyMode: "custom",
Proxy: config.NetworkProxyConfig{
Type: "socks5",
Server: "proxy.example.com",
Port: 1080,
Username: "proxy-user",
Password: "proxy-secret",
},
}
report := Collect(Options{Version: "test-version", Config: cfg})
text := RenderText(report)
raw, err := json.Marshal(report)
if err != nil {
t.Fatal(err)
}
combined := text + "\n" + string(raw)
for _, secret := range []string{"sk-live-secret", "secret-query", "Authorization", "proxy-secret"} {
if strings.Contains(combined, secret) {
t.Fatalf("doctor report leaked %q:\n%s", secret, combined)
}
}
if !strings.Contains(combined, "api.example.com") || !strings.Contains(combined, "mcp.example.com") {
t.Fatalf("doctor report should keep useful host diagnostics:\n%s", combined)
}
}
func TestCollectReportDoesNotRequireAPIKey(t *testing.T) {
t.Setenv("REASONIX_HOME", filepath.Join(t.TempDir(), "reasonix"))
t.Setenv("DEEPSEEK_API_KEY", "")
cfg := config.Default()
report := Collect(Options{Version: "1.2.3", Config: cfg})
text := RenderText(report)
if report.Version != "1.2.3" {
t.Fatalf("version = %q, want 1.2.3", report.Version)
}
if len(report.Providers) == 0 {
t.Fatal("expected built-in providers in report")
}
if report.Providers[0].KeyPresent {
t.Fatal("provider key should be reported missing when env is empty")
}
if !strings.Contains(text, "reasonix 1.2.3 doctor") {
t.Fatalf("text report missing header:\n%s", text)
}
if !strings.Contains(text, "missing") {
t.Fatalf("text report should mention missing key state:\n%s", text)
}
}
func TestRenderTextSurfacesWarningsUpTop(t *testing.T) {
text := RenderText(Report{Warnings: []string{"config reasonix.toml: parse boom"}})
w := strings.Index(text, "parse boom")
if w < 0 {
t.Fatalf("warning missing from report:\n%s", text)
}
if p := strings.Index(text, "\nproviders\n"); p >= 0 && w > p {
t.Fatalf("warning should appear before the providers section, not buried below:\n%s", text)
}
}
func TestRenderTextFlagsUnavailableSandboxAsFailClosed(t *testing.T) {
inactive := RenderText(Report{Sandbox: SandboxReport{Bash: "enforce", Available: false}})
if !strings.Contains(inactive, "bash execution is refused") {
t.Fatalf("enforce without an OS sandbox should report fail-closed bash behavior:\n%s", inactive)
}
if strings.Contains(inactive, "runs unconfined") {
t.Fatalf("enforce without an OS sandbox should not claim bash runs unconfined:\n%s", inactive)
}
active := RenderText(Report{Sandbox: SandboxReport{Bash: "enforce", Available: true}})
if strings.Contains(active, "bash execution is refused") {
t.Fatalf("enforce with an OS sandbox should not be flagged unavailable:\n%s", active)
}
}
// TestCollectFlagsIgnoredEnforceConfig pins the visibility contract for the
// platform force-off: when the config file says enforce but the effective mode
// resolves to off (Windows), doctor must say so in both the warnings list and
// the sandbox bash line instead of silently reporting "off".
func TestCollectFlagsIgnoredEnforceConfig(t *testing.T) {
t.Setenv("REASONIX_HOME", filepath.Join(t.TempDir(), "reasonix"))
cfg := config.Default()
cfg.Sandbox.Bash = "enforce"
report := Collect(Options{Version: "test", Config: cfg})
ignored := cfg.BashMode() == "off"
if report.Sandbox.BashConfigIgnored != ignored {
t.Fatalf("BashConfigIgnored = %v, want %v (BashMode %q)", report.Sandbox.BashConfigIgnored, ignored, cfg.BashMode())
}
text := RenderText(Report{Sandbox: SandboxReport{Bash: "off", BashConfigIgnored: true}})
if !strings.Contains(text, `config requests "enforce", ignored`) {
t.Fatalf("ignored enforce should be flagged on the bash line:\n%s", text)
}
plain := RenderText(Report{Sandbox: SandboxReport{Bash: "off"}})
if strings.Contains(plain, "ignored") {
t.Fatalf("plain off must not claim the config was ignored:\n%s", plain)
}
}