返回 DeepSeek-Reasonix
report_test.go
根目录 / internal / doctor / report_test.go
1 package doctor
2
3 import (
4 "encoding/json"
5 "os"
6 "os/user"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "reasonix/internal/config"
12 )
13
14 func TestRedactHome(t *testing.T) {
15 home := t.TempDir()
16 t.Setenv("HOME", home) // os.UserHomeDir on unix
17 t.Setenv("USERPROFILE", home) // os.UserHomeDir on windows
18 sep := string(os.PathSeparator)
19
20 if got := redactHome(home); got != "~" {
21 t.Fatalf("home itself: got %q, want ~", got)
22 }
23 under := filepath.Join(home, "projects", "x")
24 if got, want := redactHome(under), "~"+sep+"projects"+sep+"x"; got != want {
25 t.Fatalf("under home: got %q, want %q", got, want)
26 }
27 outside := filepath.Join(t.TempDir(), "elsewhere") // sibling temp, not under home
28 if got := redactHome(outside); got != outside {
29 t.Fatalf("outside home must be unchanged: got %q", got)
30 }
31 if got := redactHome(""); got != "" {
32 t.Fatalf("empty must stay empty: got %q", got)
33 }
34 }
35
36 func TestCollectReportRedactsSecrets(t *testing.T) {
37 t.Setenv("REASONIX_TEST_SECRET", "sk-live-secret")
38
39 cfg := config.Default()
40 cfg.DefaultModel = "custom"
41 cfg.Providers = []config.ProviderEntry{{
42 Name: "custom",
43 Kind: "openai",
44 BaseURL: "https://api.example.com/v1?token=secret-query",
45 Model: "model-a",
46 APIKeyEnv: "REASONIX_TEST_SECRET",
47 }}
48 cfg.Plugins = []config.PluginEntry{{
49 Name: "remote",
50 Type: "http",
51 URL: "https://mcp.example.com/path?api_key=secret-query",
52 Headers: map[string]string{"Authorization": "Bearer sk-live-secret"},
53 }}
54 cfg.Network = config.NetworkConfig{
55 ProxyMode: "custom",
56 Proxy: config.NetworkProxyConfig{
57 Type: "socks5",
58 Server: "proxy.example.com",
59 Port: 1080,
60 Username: "proxy-user",
61 Password: "proxy-secret",
62 },
63 }
64
65 report := Collect(Options{Version: "test-version", Config: cfg})
66 text := RenderText(report)
67 raw, err := json.Marshal(report)
68 if err != nil {
69 t.Fatal(err)
70 }
71 combined := text + "\n" + string(raw)
72
73 for _, secret := range []string{"sk-live-secret", "secret-query", "Authorization", "proxy-secret"} {
74 if strings.Contains(combined, secret) {
75 t.Fatalf("doctor report leaked %q:\n%s", secret, combined)
76 }
77 }
78 if !strings.Contains(combined, "api.example.com") || !strings.Contains(combined, "mcp.example.com") {
79 t.Fatalf("doctor report should keep useful host diagnostics:\n%s", combined)
80 }
81 }
82
83 func TestCollectReportDoesNotRequireAPIKey(t *testing.T) {
84 t.Setenv("REASONIX_HOME", filepath.Join(t.TempDir(), "reasonix"))
85 t.Setenv("DEEPSEEK_API_KEY", "")
86
87 cfg := config.Default()
88 report := Collect(Options{Version: "1.2.3", Config: cfg})
89 text := RenderText(report)
90
91 if report.Version != "1.2.3" {
92 t.Fatalf("version = %q, want 1.2.3", report.Version)
93 }
94 if len(report.Providers) == 0 {
95 t.Fatal("expected built-in providers in report")
96 }
97 if report.Providers[0].KeyPresent {
98 t.Fatal("provider key should be reported missing when env is empty")
99 }
100 if !strings.Contains(text, "reasonix 1.2.3 doctor") {
101 t.Fatalf("text report missing header:\n%s", text)
102 }
103 if !strings.Contains(text, "missing") {
104 t.Fatalf("text report should mention missing key state:\n%s", text)
105 }
106 }
107
108 func TestRenderTextSurfacesWarningsUpTop(t *testing.T) {
109 text := RenderText(Report{Warnings: []string{"config reasonix.toml: parse boom"}})
110 w := strings.Index(text, "parse boom")
111 if w < 0 {
112 t.Fatalf("warning missing from report:\n%s", text)
113 }
114 if p := strings.Index(text, "\nproviders\n"); p >= 0 && w > p {
115 t.Fatalf("warning should appear before the providers section, not buried below:\n%s", text)
116 }
117 }
118
119 func TestRenderTextFlagsUnavailableSandboxAsFailClosed(t *testing.T) {
120 inactive := RenderText(Report{Sandbox: SandboxReport{Bash: "enforce", Available: false}})
121 if !strings.Contains(inactive, "bash execution is refused") {
122 t.Fatalf("enforce without an OS sandbox should report fail-closed bash behavior:\n%s", inactive)
123 }
124 if strings.Contains(inactive, "runs unconfined") {
125 t.Fatalf("enforce without an OS sandbox should not claim bash runs unconfined:\n%s", inactive)
126 }
127
128 active := RenderText(Report{Sandbox: SandboxReport{Bash: "enforce", Available: true}})
129 if strings.Contains(active, "bash execution is refused") {
130 t.Fatalf("enforce with an OS sandbox should not be flagged unavailable:\n%s", active)
131 }
132 }
133
134 // TestCollectFlagsIgnoredEnforceConfig pins the visibility contract for the
135 // platform force-off: when the config file says enforce but the effective mode
136 // resolves to off (Windows), doctor must say so in both the warnings list and
137 // the sandbox bash line instead of silently reporting "off".
138 func TestCollectFlagsIgnoredEnforceConfig(t *testing.T) {
139 t.Setenv("REASONIX_HOME", filepath.Join(t.TempDir(), "reasonix"))
140
141 cfg := config.Default()
142 cfg.Sandbox.Bash = "enforce"
143 report := Collect(Options{Version: "test", Config: cfg})
144
145 ignored := cfg.BashMode() == "off"
146 if report.Sandbox.BashConfigIgnored != ignored {
147 t.Fatalf("BashConfigIgnored = %v, want %v (BashMode %q)", report.Sandbox.BashConfigIgnored, ignored, cfg.BashMode())
148 }
149
150 text := RenderText(Report{Sandbox: SandboxReport{Bash: "off", BashConfigIgnored: true}})
151 if !strings.Contains(text, `config requests "enforce", ignored`) {
152 t.Fatalf("ignored enforce should be flagged on the bash line:\n%s", text)
153 }
154 plain := RenderText(Report{Sandbox: SandboxReport{Bash: "off"}})
155 if strings.Contains(plain, "ignored") {
156 t.Fatalf("plain off must not claim the config was ignored:\n%s", plain)
157 }
158 }
159
160 func TestHomeIsolationWarningDetectsMismatch(t *testing.T) {
161 // Prefer a synthetic mismatch without requiring root privileges.
162 acct, err := user.Current()
163 if err != nil || acct == nil || strings.TrimSpace(acct.HomeDir) == "" {
164 t.Skip("account home unavailable")
165 }
166 t.Setenv("REASONIX_HOME", "")
167 serviceHome := filepath.Join(t.TempDir(), "service-home")
168 t.Setenv("HOME", serviceHome)
169 t.Setenv("USERPROFILE", serviceHome)
170 got := homeIsolationWarning()
171 if got == "" {
172 t.Fatal("expected HOME mismatch warning")
173 }
174 if !strings.Contains(got, "REASONIX_HOME") {
175 t.Fatalf("warning = %q, want REASONIX_HOME guidance", got)
176 }
177 // Shareable output must not embed either absolute home path.
178 if strings.Contains(got, serviceHome) || strings.Contains(got, acct.HomeDir) {
179 t.Fatalf("warning leaked a home path: %q", got)
180 }
181 t.Setenv("REASONIX_HOME", t.TempDir())
182 if got := homeIsolationWarning(); got != "" {
183 t.Fatalf("REASONIX_HOME set should silence warning, got %q", got)
184 }
185 }
186
186 lines GO