package cli import ( "path/filepath" "strings" "testing" "reasonix/internal/command" "reasonix/internal/hook" "reasonix/internal/memory" "reasonix/internal/outputstyle" "reasonix/internal/plugin" "reasonix/internal/skill" ) func TestRenderSkillListUsesSharedVisualLanguage(t *testing.T) { width := 72 got := renderSkillList(width, []skill.Skill{ {Name: "explore", Description: strings.Repeat("long ", 30), Scope: skill.ScopeProject}, {Name: "deep", Description: "run in isolation", Scope: skill.ScopeGlobal, RunAs: skill.RunSubagent}, }, map[string]bool{"explore": true}) for _, want := range []string{"skills (2)", "/explore", "(project)", "…", "/deep", "subagent", "disabled", "invoke:"} { if !strings.Contains(got, want) { t.Fatalf("skill list missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestRenderSkillShowCapsLongBody(t *testing.T) { width := 80 body := strings.Repeat("line\n", skillShowMaxLines+3) got := renderSkillShow(width, skill.Skill{ Name: "review", Description: "review code", Scope: skill.ScopeBuiltin, Path: "/very/long/path/to/SKILL.md", Body: body, }, true) for _, want := range []string{"skill:", "review", "builtin", "disabled", "review code", "SKILL.md", "+3 more lines"} { if !strings.Contains(got, want) { t.Fatalf("skill show missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestViewProtectLinesCompactsLongBodyLines(t *testing.T) { got := viewProtectLines(strings.Repeat("x", 80)+"\nshort", 20) lines := strings.Split(got, "\n") if len(lines) != 2 || !strings.HasSuffix(lines[0], "…") || visibleWidth(lines[0]) > 20 || lines[1] != "short" { t.Fatalf("protected lines = %q", got) } } func TestRenderMemoryGroupsDocsAndStore(t *testing.T) { width := 72 store := memory.Store{Dir: filepath.Join(t.TempDir(), "memory")} if _, err := store.Save(memory.Memory{Name: "saved-fact", Title: "Saved Fact", Description: "remembered fact"}); err != nil { t.Fatalf("save memory: %v", err) } got := renderMemory(width, &memory.Set{ Docs: []memory.Source{{Path: "/Users/me/project/REASONIX.md", Scope: memory.ScopeProject}}, Store: store, Index: store.Index(), }) for _, want := range []string{"memory", "docs", "(project)", "REASONIX.md", "saved memories", "saved-fact", "Saved Fact", "doc edits apply next session"} { if !strings.Contains(got, want) { t.Fatalf("memory view missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestRenderOutputStylesUsesActiveStatus(t *testing.T) { width := 72 got := renderOutputStyles(width, []outputstyle.OutputStyle{ {Name: "concise", Description: "short answers", Builtin: true}, {Name: "team", Description: strings.Repeat("custom ", 20), Builtin: false}, }, "team") for _, want := range []string{"output styles", "concise", "(builtin)", "team", "(custom)", "active", "…"} { if !strings.Contains(got, want) { t.Fatalf("output style view missing %q:\n%s", want, got) } } if strings.Contains(got, "*") { t.Fatalf("output style view should use active status instead of star:\n%s", got) } assertLinesWithin(t, got, width) } func TestRenderModelsUsesActiveStatus(t *testing.T) { width := 72 got := renderModels(width, []string{"deepseek/v4", "openai/really-long-model-name-" + strings.Repeat("x", 80)}, "deepseek/v4") for _, want := range []string{"models", "deepseek/v4", "active", "…", "switch with /model"} { if !strings.Contains(got, want) { t.Fatalf("model view missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestRenderHooksUsesSharedVisualLanguage(t *testing.T) { width := 72 got := renderHooks(width, []hook.ResolvedHook{{ HookConfig: hook.HookConfig{Command: strings.Repeat("echo ", 30)}, Event: hook.PreToolUse, Scope: hook.ScopeProject, }}, false, true) for _, want := range []string{"hooks (1 active)", "PreToolUse", "project", "…", "not trusted", "/hooks trust"} { if !strings.Contains(got, want) { t.Fatalf("hooks view missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestRenderHooksShowsPermissionRequestMatch(t *testing.T) { width := 72 got := renderHooks(width, []hook.ResolvedHook{{ HookConfig: hook.HookConfig{Command: "notify", Match: "bash"}, Event: hook.PermissionRequest, Scope: hook.ScopeGlobal, }}, true, true) for _, want := range []string{"PermissionRequest", "global", "bash", "notify"} { if !strings.Contains(got, want) { t.Fatalf("hooks view missing %q:\n%s", want, got) } } assertLinesWithin(t, got, width) } func TestRenderHelpGroupsCommands(t *testing.T) { width := 72 got := renderHelp(width, []command.Command{{Name: "review", Description: "review code"}}, []skill.Skill{{Name: "explore", Description: "inspect repo"}}, []plugin.Prompt{{Name: "mcp__docs__summarize", Description: "summarize docs"}}, ) for _, want := range []string{"commands", "built-in", "/tree", "custom", "/review", "skills", "/explore", "MCP prompts", "/mcp__docs__summarize"} { if !strings.Contains(got, want) { t.Fatalf("help view missing %q:\n%s", want, got) } } if strings.Contains(got, " · ") { t.Fatalf("help view should not use one-line command chains:\n%s", got) } assertLinesWithin(t, got, width) } func TestRenderSkillPathsStaysWithinWidth(t *testing.T) { width := 72 got := renderSkillPaths(width, []skill.Root{{ Dir: "/Users/me/projects/really/deep/path/to/.reasonix/skills", Scope: skill.ScopeProject, Priority: 0, Status: skill.StatusMissing, }}) assertLinesWithin(t, got, width) } func TestRenderMCPStatusStaysWithinWidth(t *testing.T) { width := 72 got := renderMCPStatus(width, []plugin.ServerStatus{{Name: "a-very-long-server-name-that-should-not-wrap", Transport: "stdio", Tools: 12}}, []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)}}, []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)}}, []plugin.Failure{{Name: "another-very-long-server-name-that-should-not-wrap", Transport: "stdio", Error: strings.Repeat("failure ", 20)}}, ) assertLinesWithin(t, got, width) } func assertLinesWithin(t *testing.T, s string, width int) { t.Helper() for i, line := range strings.Split(s, "\n") { if got := visibleWidth(line); got > width { t.Fatalf("line %d exceeds width %d with %d cols:\n%s\n\nfull output:\n%s", i+1, width, got, line, s) } if strings.Contains(line, "\n") { t.Fatalf("line %d unexpectedly contains newline: %q", i+1, line) } } }