返回 DeepSeek-Reasonix
tui_diagnostics_test.go
根目录 / internal / cli / tui_diagnostics_test.go
1 package cli
2
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "reasonix/internal/control"
14 "reasonix/internal/event"
15 "reasonix/internal/i18n"
16 )
17
18 func TestTUIDiagnosticsKeepProcessAndPluginLogsOffTerminal(t *testing.T) {
19 var terminal bytes.Buffer
20 beforeTest := slog.Default()
21 terminalLogger := slog.New(slog.NewTextHandler(&terminal, nil))
22 slog.SetDefault(terminalLogger)
23 t.Cleanup(func() { slog.SetDefault(beforeTest) })
24
25 d := startTUIDiagnostics(t.TempDir())
26 t.Cleanup(d.Close)
27 slog.Warn("controller: snapshot conflict", "path", "private-session.jsonl")
28 fmt.Fprintln(d.Writer(), "plugin diagnostic")
29 if got := terminal.String(); got != "" {
30 t.Fatalf("terminal received diagnostics while TUI owned it: %q", got)
31 }
32
33 logPath := d.path
34 d.Close()
35 data, err := os.ReadFile(logPath)
36 if err != nil {
37 t.Fatalf("read TUI diagnostic log: %v", err)
38 }
39 got := string(data)
40 for _, want := range []string{"controller: snapshot conflict", "private-session.jsonl", "plugin diagnostic"} {
41 if !strings.Contains(got, want) {
42 t.Fatalf("diagnostic log = %q, want %q", got, want)
43 }
44 }
45
46 slog.Warn("after TUI")
47 if got := terminal.String(); !strings.Contains(got, "after TUI") {
48 t.Fatalf("previous logger was not restored after TUI close: %q", got)
49 }
50 }
51
52 func TestTUIDiagnosticsFallBackToDiscardWithoutLeakingToTerminal(t *testing.T) {
53 var terminal bytes.Buffer
54 beforeTest := slog.Default()
55 terminalLogger := slog.New(slog.NewTextHandler(&terminal, nil))
56 slog.SetDefault(terminalLogger)
57 t.Cleanup(func() { slog.SetDefault(beforeTest) })
58
59 blockedHome := filepath.Join(t.TempDir(), "not-a-directory")
60 if err := os.WriteFile(blockedHome, []byte("file"), 0o600); err != nil {
61 t.Fatalf("seed blocked home: %v", err)
62 }
63 d := startTUIDiagnostics(blockedHome)
64 defer d.Close()
65
66 slog.Warn("must stay off terminal")
67 fmt.Fprintln(d.Writer(), "plugin must stay off terminal")
68 if got := terminal.String(); got != "" {
69 t.Fatalf("fallback leaked diagnostics to terminal: %q", got)
70 }
71 if d.path != "" {
72 t.Fatalf("fallback diagnostic path = %q, want empty", d.path)
73 }
74 }
75
76 func TestBoundedDiagnosticWriterStopsAtLimit(t *testing.T) {
77 var dst bytes.Buffer
78 w := &boundedDiagnosticWriter{dst: &dst, remaining: 8}
79 payload := strings.Repeat("x", 32)
80 n, err := io.WriteString(w, payload)
81 if err != nil || n != len(payload) {
82 t.Fatalf("Write = (%d, %v), want (%d, nil)", n, err, len(payload))
83 }
84 if !strings.HasPrefix(dst.String(), strings.Repeat("x", 8)) {
85 t.Fatalf("bounded output = %q, want eight payload bytes first", dst.String())
86 }
87 if !strings.Contains(dst.String(), "diagnostic log limit reached") {
88 t.Fatalf("bounded output = %q, want truncation marker", dst.String())
89 }
90 before := dst.Len()
91 if _, err := io.WriteString(w, "more"); err != nil {
92 t.Fatalf("discard after cap: %v", err)
93 }
94 if dst.Len() != before {
95 t.Fatalf("writer grew after cap: before=%d after=%d", before, dst.Len())
96 }
97 }
98
99 func TestCLIProfileBuildOptionsPropagateInteractiveOwners(t *testing.T) {
100 var diagnostic bytes.Buffer
101 recovered := false
102 onRecovered := func(control.SessionRecoveryInfo) error {
103 recovered = true
104 return nil
105 }
106 opts := cliProfileBuildOptions("provider/model", 9, false, event.Discard, "delivery", cliBuildOverrides{
107 WorkspaceRoot: "/workspace",
108 HeadlessApprovalMode: control.ToolApprovalAuto,
109 Stderr: &diagnostic,
110 OnSessionRecovered: onRecovered,
111 })
112
113 if opts.Stderr != &diagnostic {
114 t.Fatalf("Stderr = %T, want caller-owned diagnostic writer", opts.Stderr)
115 }
116 if opts.OnSessionRecovered == nil {
117 t.Fatal("OnSessionRecovered was dropped from CLI build options")
118 }
119 if err := opts.OnSessionRecovered(control.SessionRecoveryInfo{RecoveryPath: "recovery.jsonl"}); err != nil {
120 t.Fatalf("OnSessionRecovered: %v", err)
121 }
122 if !recovered {
123 t.Fatal("propagated recovery callback was not invoked")
124 }
125 if opts.HeadlessApprovalMode != control.ToolApprovalAuto {
126 t.Fatalf("HeadlessApprovalMode = %q, want %q", opts.HeadlessApprovalMode, control.ToolApprovalAuto)
127 }
128 }
129
130 func TestCLIProfileBuildOptionsUseResolvedLocaleForAutoPricing(t *testing.T) {
131 defer i18n.DetectLanguage("en")
132 for _, tt := range []struct {
133 language string
134 want string
135 }{
136 {language: "en", want: "USD"},
137 {language: "zh", want: "CNY"},
138 {language: "zh-TW", want: "CNY"},
139 } {
140 i18n.DetectLanguage(tt.language)
141 opts := cliProfileBuildOptions("provider/model", 0, false, event.Discard, "balanced", cliBuildOverrides{})
142 if opts.AutoPricingCurrency != tt.want {
143 t.Errorf("language %q auto pricing currency = %q, want %q", tt.language, opts.AutoPricingCurrency, tt.want)
144 }
145 }
146 }
147
148 func TestTUIDiagnosticsMilestoneFlushesNonEmptyLog(t *testing.T) {
149 home := t.TempDir()
150 d := startTUIDiagnostics(home)
151 t.Cleanup(d.Close)
152 d.Milestone("config_load_begin")
153 d.Milestone("controller_build_done")
154 if d.Path() == "" {
155 t.Fatal("expected diagnostic log path")
156 }
157 body, err := os.ReadFile(d.Path())
158 if err != nil {
159 t.Fatal(err)
160 }
161 if len(body) == 0 {
162 t.Fatal("diagnostic log must not be empty after milestones")
163 }
164 for _, want := range []string{"diagnostics_started", "config_load_begin", "controller_build_done"} {
165 if !strings.Contains(string(body), want) {
166 t.Fatalf("log missing %q:\n%s", want, body)
167 }
168 }
169 }
170
170 lines GO