返回 DeepSeek-Reasonix
view_helpers_test.go
根目录 / internal / cli / view_helpers_test.go
1 package cli
2
3 import (
4 "path/filepath"
5 "strings"
6 "testing"
7
8 "reasonix/internal/command"
9 "reasonix/internal/hook"
10 "reasonix/internal/memory"
11 "reasonix/internal/outputstyle"
12 "reasonix/internal/plugin"
13 "reasonix/internal/skill"
14 )
15
16 func TestRenderSkillListUsesSharedVisualLanguage(t *testing.T) {
17 width := 72
18 got := renderSkillList(width, []skill.Skill{
19 {Name: "explore", Description: strings.Repeat("long ", 30), Scope: skill.ScopeProject},
20 {Name: "deep", Description: "run in isolation", Scope: skill.ScopeGlobal, RunAs: skill.RunSubagent},
21 }, map[string]bool{"explore": true})
22 for _, want := range []string{"skills (2)", "/explore", "(project)", "…", "/deep", "subagent", "disabled", "invoke:"} {
23 if !strings.Contains(got, want) {
24 t.Fatalf("skill list missing %q:\n%s", want, got)
25 }
26 }
27 assertLinesWithin(t, got, width)
28 }
29
30 func TestRenderSkillShowCapsLongBody(t *testing.T) {
31 width := 80
32 body := strings.Repeat("line\n", skillShowMaxLines+3)
33 got := renderSkillShow(width, skill.Skill{
34 Name: "review",
35 Description: "review code",
36 Scope: skill.ScopeBuiltin,
37 Path: "/very/long/path/to/SKILL.md",
38 Body: body,
39 }, true)
40 for _, want := range []string{"skill:", "review", "builtin", "disabled", "review code", "SKILL.md", "+3 more lines"} {
41 if !strings.Contains(got, want) {
42 t.Fatalf("skill show missing %q:\n%s", want, got)
43 }
44 }
45 assertLinesWithin(t, got, width)
46 }
47
48 func TestViewProtectLinesCompactsLongBodyLines(t *testing.T) {
49 got := viewProtectLines(strings.Repeat("x", 80)+"\nshort", 20)
50 lines := strings.Split(got, "\n")
51 if len(lines) != 2 || !strings.HasSuffix(lines[0], "…") || visibleWidth(lines[0]) > 20 || lines[1] != "short" {
52 t.Fatalf("protected lines = %q", got)
53 }
54 }
55
56 func TestRenderMemoryGroupsDocsAndStore(t *testing.T) {
57 width := 72
58 store := memory.Store{Dir: filepath.Join(t.TempDir(), "memory")}
59 if _, err := store.Save(memory.Memory{Name: "saved-fact", Title: "Saved Fact", Description: "remembered fact"}); err != nil {
60 t.Fatalf("save memory: %v", err)
61 }
62 got := renderMemory(width, &memory.Set{
63 Docs: []memory.Source{{Path: "/Users/me/project/REASONIX.md", Scope: memory.ScopeProject}},
64 Store: store,
65 Index: store.Index(),
66 })
67 for _, want := range []string{
68 "memory", "instructions", "precedence=1", "scope=project", "REASONIX.md",
69 "saved memories", "saved-fact", "Saved Fact", "revision=1", "freshness=fresh",
70 "doc edits apply next session",
71 } {
72 if !strings.Contains(got, want) {
73 t.Fatalf("memory view missing %q:\n%s", want, got)
74 }
75 }
76 assertLinesWithin(t, got, width)
77 }
78
79 func TestRenderOutputStylesUsesActiveStatus(t *testing.T) {
80 width := 72
81 got := renderOutputStyles(width, []outputstyle.OutputStyle{
82 {Name: "concise", Description: "short answers", Builtin: true},
83 {Name: "team", Description: strings.Repeat("custom ", 20), Builtin: false},
84 }, "team")
85 for _, want := range []string{"output styles", "concise", "(builtin)", "team", "(custom)", "active", "…"} {
86 if !strings.Contains(got, want) {
87 t.Fatalf("output style view missing %q:\n%s", want, got)
88 }
89 }
90 if strings.Contains(got, "*") {
91 t.Fatalf("output style view should use active status instead of star:\n%s", got)
92 }
93 assertLinesWithin(t, got, width)
94 }
95
96 func TestRenderModelsUsesActiveStatus(t *testing.T) {
97 width := 72
98 got := renderModels(width, []string{"deepseek/v4", "openai/really-long-model-name-" + strings.Repeat("x", 80)}, "deepseek/v4")
99 for _, want := range []string{"models", "deepseek/v4", "active", "…", "switch with /model"} {
100 if !strings.Contains(got, want) {
101 t.Fatalf("model view missing %q:\n%s", want, got)
102 }
103 }
104 assertLinesWithin(t, got, width)
105 }
106
107 func TestRenderHooksUsesSharedVisualLanguage(t *testing.T) {
108 width := 72
109 got := renderHooks(width, []hook.ResolvedHook{{
110 HookConfig: hook.HookConfig{Command: strings.Repeat("echo ", 30)},
111 Event: hook.PreToolUse,
112 Scope: hook.ScopeProject,
113 }})
114 for _, want := range []string{"hooks (1 active)", "PreToolUse", "project", "…", ".reasonix/settings.json"} {
115 if !strings.Contains(got, want) {
116 t.Fatalf("hooks view missing %q:\n%s", want, got)
117 }
118 }
119 assertLinesWithin(t, got, width)
120 }
121
122 func TestRenderHooksShowsPermissionRequestMatch(t *testing.T) {
123 width := 72
124 got := renderHooks(width, []hook.ResolvedHook{{
125 HookConfig: hook.HookConfig{Command: "notify", Match: "bash"},
126 Event: hook.PermissionRequest,
127 Scope: hook.ScopeGlobal,
128 }})
129 for _, want := range []string{"PermissionRequest", "global", "bash", "notify"} {
130 if !strings.Contains(got, want) {
131 t.Fatalf("hooks view missing %q:\n%s", want, got)
132 }
133 }
134 assertLinesWithin(t, got, width)
135 }
136
137 func TestRenderHelpGroupsCommands(t *testing.T) {
138 width := 72
139 got := renderHelp(width,
140 []command.Command{{Name: "review", Description: "review code"}},
141 []skill.Skill{{Name: "explore", Description: "inspect repo"}},
142 []plugin.Prompt{{Name: "mcp__docs__summarize", Description: "summarize docs"}},
143 )
144 for _, want := range []string{"commands", "built-in", "/tree", "custom", "/review", "skills", "/explore", "MCP prompts", "/mcp__docs__summarize"} {
145 if !strings.Contains(got, want) {
146 t.Fatalf("help view missing %q:\n%s", want, got)
147 }
148 }
149 if strings.Contains(got, " · ") {
150 t.Fatalf("help view should not use one-line command chains:\n%s", got)
151 }
152 assertLinesWithin(t, got, width)
153 }
154
155 func TestRenderHelpDocsShowsOnlyRuntimeWinner(t *testing.T) {
156 got := renderHelp(72,
157 []command.Command{{Name: "docs", Description: "custom docs"}},
158 []skill.Skill{{Name: "docs", Description: "docs skill"}},
159 nil,
160 )
161 if count := strings.Count(got, "/docs"); count != 1 {
162 t.Fatalf("help contains %d /docs entries, want one:\n%s", count, got)
163 }
164 if !strings.Contains(got, "custom docs") || strings.Contains(got, "docs skill") {
165 t.Fatalf("help did not preserve the runtime-winning custom command:\n%s", got)
166 }
167 if !strings.Contains(got, "/reasonix:docs") {
168 t.Fatalf("help did not preserve the qualified built-in docs fallback:\n%s", got)
169 }
170 }
171
172 func TestRenderHelpDocsUsesQualifiedFallbackForHiddenAlias(t *testing.T) {
173 got := renderHelp(72,
174 []command.Command{
175 {Name: "docs", Plugin: "manuals", Hidden: true},
176 {Name: "manuals:docs", Plugin: "manuals", Description: "plugin docs"},
177 },
178 nil,
179 nil,
180 )
181 if strings.Contains(got, "\n /docs ") {
182 t.Fatalf("help exposed a misleading hidden /docs alias:\n%s", got)
183 }
184 for _, want := range []string{"/reasonix:docs", "/manuals:docs"} {
185 if !strings.Contains(got, want) {
186 t.Fatalf("help missing %q:\n%s", want, got)
187 }
188 }
189 }
190
191 func TestRenderSkillPathsStaysWithinWidth(t *testing.T) {
192 width := 72
193 got := renderSkillPaths(width, []skill.Root{{
194 Dir: "/Users/me/projects/really/deep/path/to/.reasonix/skills",
195 Scope: skill.ScopeProject,
196 Priority: 0,
197 Status: skill.StatusMissing,
198 }})
199 assertLinesWithin(t, got, width)
200 }
201
202 func TestRenderMCPStatusStaysWithinWidth(t *testing.T) {
203 width := 72
204 got := renderMCPStatus(width,
205 []plugin.ServerStatus{{Name: "a-very-long-server-name-that-should-not-wrap", Transport: "stdio", Tools: 12}},
206 []plugin.Prompt{{Server: "a-very-long-server-name-that-should-not-wrap", Name: "mcp__server__prompt_with_a_really_long_name", Description: strings.Repeat("describe ", 20)}},
207 []plugin.Resource{{Server: "a-very-long-server-name-that-should-not-wrap", URI: "file:///Users/me/project/docs/really/deep/resource.md", Name: strings.Repeat("resource ", 20)}},
208 []plugin.Failure{{Name: "another-very-long-server-name-that-should-not-wrap", Transport: "stdio", Error: strings.Repeat("failure ", 20)}},
209 )
210 assertLinesWithin(t, got, width)
211 }
212
213 func assertLinesWithin(t *testing.T, s string, width int) {
214 t.Helper()
215 for i, line := range strings.Split(s, "\n") {
216 if got := visibleWidth(line); got > width {
217 t.Fatalf("line %d exceeds width %d with %d cols:\n%s\n\nfull output:\n%s", i+1, width, got, line, s)
218 }
219 if strings.Contains(line, "\n") {
220 t.Fatalf("line %d unexpectedly contains newline: %q", i+1, line)
221 }
222 }
223 }
224
224 lines GO