返回 DeepSeek-Reasonix
cli_flags_test.go
根目录 / internal / cli / cli_flags_test.go
1 package cli
2
3 import (
4 "os"
5 "path/filepath"
6 "reflect"
7 "strings"
8 "testing"
9
10 "github.com/spf13/pflag"
11 "reasonix/internal/agent"
12 "reasonix/internal/provider"
13 )
14
15 func TestSplitAllowedToolRules(t *testing.T) {
16 got, err := splitAllowedToolRules([]string{
17 "Bash(git *) Edit,read_file",
18 "Bash(go test ./...) Edit(docs/**)",
19 "Edit",
20 })
21 if err != nil {
22 t.Fatalf("splitAllowedToolRules: %v", err)
23 }
24 want := []string{"Bash(git *)", "Edit", "read_file", "Bash(go test ./...)", "Edit(docs/**)"}
25 if !reflect.DeepEqual(got, want) {
26 t.Fatalf("rules = %#v, want %#v", got, want)
27 }
28 }
29
30 func TestSplitAllowedToolRulesRejectsUnbalancedParentheses(t *testing.T) {
31 for _, input := range []string{"Bash(git *", "Bash(git *))"} {
32 if _, err := splitAllowedToolRules([]string{input}); err == nil {
33 t.Fatalf("splitAllowedToolRules(%q) unexpectedly succeeded", input)
34 }
35 }
36 }
37
38 func TestRegisterContinueFlagShorthandParses(t *testing.T) {
39 cases := []struct {
40 args []string
41 want bool
42 }{
43 {[]string{"-c"}, true},
44 {[]string{"-c=true"}, true},
45 {[]string{"--continue"}, true},
46 {[]string{"--continue=true"}, true},
47 {[]string{}, false},
48 }
49 for _, tc := range cases {
50 fs := pflag.NewFlagSet("reasonix", pflag.ContinueOnError)
51 cont := registerContinueFlag(fs)
52 if err := fs.Parse(tc.args); err != nil {
53 t.Fatalf("Parse(%#v): %v", tc.args, err)
54 }
55 if *cont != tc.want {
56 t.Fatalf("Parse(%#v) continue = %v, want %v", tc.args, *cont, tc.want)
57 }
58 }
59 }
60
61 // Regression guard: registering the shorthand with BoolVar instead of BoolP
62 // leaves "-c" unparseable ("unknown shorthand flag") while accidentally
63 // accepting "--c" as a long flag name (the pre-fix bug, #7156/#7171).
64 func TestRegisterContinueFlagRejectsAccidentalLongC(t *testing.T) {
65 fs := pflag.NewFlagSet("reasonix", pflag.ContinueOnError)
66 cont := registerContinueFlag(fs)
67 if err := fs.Parse([]string{"--c"}); err == nil {
68 t.Fatalf("Parse(--c) should fail: --c must not exist as a long flag name")
69 }
70 if *cont {
71 t.Fatalf("--c unexpectedly set the continue flag")
72 }
73 }
74
75 func TestNormalizeOptionalResumeArg(t *testing.T) {
76 got := normalizeOptionalResumeArg([]string{"--model", "x", "--resume", "session-id", "--copy"})
77 want := []string{"--model", "x", "--resume=session-id", "--copy"}
78 if !reflect.DeepEqual(got, want) {
79 t.Fatalf("normalized args = %#v, want %#v", got, want)
80 }
81 got = normalizeOptionalResumeArg([]string{"-r", "--copy"})
82 if !reflect.DeepEqual(got, []string{"-r", "--copy"}) {
83 t.Fatalf("bare resume args = %#v", got)
84 }
85 }
86
87 func TestHasLeadingPrintFlag(t *testing.T) {
88 cases := []struct {
89 args []string
90 want bool
91 }{
92 {[]string{"-p", "task"}, true},
93 {[]string{"--print", "task"}, true},
94 {[]string{"--model", "x", "-p", "task"}, true},
95 {[]string{"--effort", "max", "--print"}, true},
96 {[]string{"--model", "x", "task"}, false},
97 {[]string{"--", "-p"}, false}, // after -- it is a literal prompt token
98 {[]string{"--model", "x", "--", "-p"}, false},
99 }
100 for _, tc := range cases {
101 if got := hasLeadingPrintFlag(tc.args); got != tc.want {
102 t.Fatalf("hasLeadingPrintFlag(%#v) = %v, want %v", tc.args, got, tc.want)
103 }
104 }
105 }
106
107 func TestStripLeadingPrintFlag(t *testing.T) {
108 cases := []struct {
109 args []string
110 want []string
111 }{
112 {[]string{"-p", "task"}, []string{"task"}},
113 {[]string{"--model", "x", "-p", "task"}, []string{"--model", "x", "task"}},
114 {[]string{"--print", "--model", "x"}, []string{"--model", "x"}},
115 // Only the first print token is dropped; a later "--print" after "--" is prompt text.
116 {[]string{"-p", "--", "--print"}, []string{"--", "--print"}},
117 {[]string{"--model", "x", "task"}, []string{"--model", "x", "task"}},
118 }
119 for _, tc := range cases {
120 if got := stripLeadingPrintFlag(tc.args); !reflect.DeepEqual(got, tc.want) {
121 t.Fatalf("stripLeadingPrintFlag(%#v) = %#v, want %#v", tc.args, got, tc.want)
122 }
123 }
124 }
125
126 func TestResolveSessionQueryByMachineSessionID(t *testing.T) {
127 identityKey := installMachineTestIdentity(t)
128 dir := t.TempDir()
129 path := saveQueryTestSession(t, dir, "opaque-branch.jsonl", "resume by machine id")
130 machineID := machineSessionIDWithKey(agent.BranchID(path), identityKey)
131 if machineID == "" || !looksLikeMachineSessionID(machineID) {
132 t.Fatalf("machine session id = %q", machineID)
133 }
134
135 got, err := resolveSessionQuery(dir, machineID)
136 if err != nil || got.path != path || got.canonical() {
137 t.Fatalf("resolve by machine id = (%+v, %v), want %q", got, err, path)
138 }
139 missing := "session_" + strings.Repeat("0", 32)
140 if _, err := resolveSessionQuery(dir, missing); err == nil || !strings.Contains(err.Error(), "no session") {
141 t.Fatalf("missing machine id error = %v", err)
142 }
143 }
144
145 func TestResolveSessionQueryByIDAndPreview(t *testing.T) {
146 dir := t.TempDir()
147 first := saveQueryTestSession(t, dir, "alpha-session.jsonl", "fix provider configuration")
148 _ = saveQueryTestSession(t, dir, "beta-session.jsonl", "improve terminal picker")
149
150 got, err := resolveSessionQuery(dir, "alpha-session")
151 if err != nil || got.path != first || got.canonical() {
152 t.Fatalf("resolve by ID = (%+v, %v), want %q", got, err, first)
153 }
154 got, err = resolveSessionQuery(dir, "provider configuration")
155 if err != nil || got.path != first || got.canonical() {
156 t.Fatalf("resolve by preview = (%+v, %v), want %q", got, err, first)
157 }
158 if _, err := resolveSessionQuery(dir, "session"); err == nil || !strings.Contains(err.Error(), "ambiguous") {
159 t.Fatalf("ambiguous query error = %v", err)
160 }
161 if _, err := resolveSessionQuery(dir, "missing"); err == nil || !strings.Contains(err.Error(), "no session") {
162 t.Fatalf("missing query error = %v", err)
163 }
164 }
165
166 func saveQueryTestSession(t *testing.T, dir, name, prompt string) string {
167 t.Helper()
168 path := filepath.Join(dir, name)
169 session := agent.NewSession("")
170 session.Add(provider.Message{Role: provider.RoleUser, Content: prompt})
171 session.Add(provider.Message{Role: provider.RoleAssistant, Content: "done"})
172 if err := session.Save(path); err != nil {
173 t.Fatal(err)
174 }
175 return path
176 }
177
178 // TestResolveSessionQueryMatchesCanonicalSessions proves --resume QUERY covers
179 // the final-format store the picker shows: exact ids and identity routes,
180 // title/preview substrings, ambiguity across both stores, and a migrated
181 // transcript that must not compete with the identity it became.
182 func TestResolveSessionQueryMatchesCanonicalSessions(t *testing.T) {
183 dir := t.TempDir()
184 sessionDir := filepath.Join(dir, "sessions")
185 v4root := filepath.Join(dir, "sessions-v4")
186 if err := os.MkdirAll(sessionDir, 0o700); err != nil {
187 t.Fatal(err)
188 }
189 legacy := saveQueryTestSession(t, sessionDir, "alpha-session.jsonl", "fix provider configuration")
190 migrated := saveQueryTestSession(t, sessionDir, "migrated-source.jsonl", "rewrite the desktop tree")
191 createCanonicalTestSession(t, v4root, "canon0001", "rewrite the desktop tree")
192 writeTestMigrationMap(t, v4root, migrated, "canon0001")
193 waitForCatalogMetadata(t, sessionDir, "canon0001")
194
195 for _, query := range []string{"canon0001", cliCanonicalRoute("canon0001"), "desktop tree"} {
196 got, err := resolveSessionQuery(sessionDir, query)
197 if err != nil || !got.canonical() || got.ref.SessionID != "canon0001" {
198 t.Fatalf("resolve %q = (%+v, %v), want the canonical session", query, got, err)
199 }
200 }
201 got, err := resolveSessionQuery(sessionDir, "provider configuration")
202 if err != nil || got.canonical() || got.path != legacy {
203 t.Fatalf("resolve legacy preview = (%+v, %v), want %q", got, err, legacy)
204 }
205 if _, err := resolveSessionQuery(sessionDir, "t"); err == nil || !strings.Contains(err.Error(), "ambiguous") {
206 t.Fatalf("query matching both stores = %v, want ambiguous", err)
207 }
208 if _, err := resolveSessionQuery(sessionDir, "nothing here"); err == nil || !strings.Contains(err.Error(), "no session") {
209 t.Fatalf("missing query error = %v", err)
210 }
211 }
212
212 lines GO