158 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|