Skip to content

Commit db47835

Browse files
committed
v0.53.1 — Unit tests for tool-recovery changes
13 new tests confirming: - Shell stderr visible on error (3 tests: empty stdout+stderr, mixed stdout+stderr+error, stderr-only success) - Directory errors suggest alternatives (3 tests: read_file, count_lines, batch_read) - WriteFile empty path rejection - Patch old_string not found - Glob empty pattern rejection - Tree directory exploration - ReadFile binary detection
1 parent cf940f7 commit db47835

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

cmd/odek/tool_recovery_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// ── Shell Stderr Tests ───────────────────────────────────────────────
12+
13+
func TestShellTool_StderrWithEmptyStdout(t *testing.T) {
14+
st := &shellTool{}
15+
// Command that writes only to stderr and fails
16+
result, err := st.Call(`{"command": "echo error msg >&2 && exit 1"}`)
17+
if err != nil {
18+
t.Fatalf("Call() should return stderr on error, not error: %v", err)
19+
}
20+
if !strings.Contains(result, "error msg") {
21+
t.Errorf("result = %q, should contain stderr 'error msg'", result)
22+
}
23+
}
24+
25+
func TestShellTool_StderrWithStdoutAndError(t *testing.T) {
26+
st := &shellTool{}
27+
result, err := st.Call(`{"command": "echo stdout_line && echo stderr_line >&2 && exit 1"}`)
28+
if err != nil {
29+
t.Fatalf("Call() should return combined output, not error: %v", err)
30+
}
31+
if !strings.Contains(result, "stdout_line") {
32+
t.Errorf("result should contain stdout content, got: %s", result)
33+
}
34+
if !strings.Contains(result, "stderr_line") {
35+
t.Errorf("result should contain stderr content, got: %s", result)
36+
}
37+
}
38+
39+
func TestShellTool_StderrOnlySuccess(t *testing.T) {
40+
st := &shellTool{}
41+
result, err := st.Call(`{"command": "echo warning >&2"}`)
42+
if err != nil {
43+
t.Fatalf("Call() error: %v", err)
44+
}
45+
if !strings.Contains(result, "warning") {
46+
t.Errorf("result = %q, should contain stderr 'warning'", result)
47+
}
48+
}
49+
50+
// ── Directory Error Message Tests ────────────────────────────────────
51+
52+
func TestReadFile_DirectorySuggestsAlternatives(t *testing.T) {
53+
dir := t.TempDir()
54+
tool := &readFileTool{}
55+
result := callJSON(t, tool, fmt.Sprintf(`{"path":"%s"}`, dir))
56+
var r struct {
57+
Error string `json:"error"`
58+
}
59+
mustUnmarshal(t, result, &r)
60+
if r.Error == "" {
61+
t.Fatal("expected error for directory path")
62+
}
63+
if !strings.Contains(r.Error, "tree") && !strings.Contains(r.Error, "search_files") && !strings.Contains(r.Error, "glob") {
64+
t.Errorf("error should suggest alternatives (tree/search_files/glob), got: %s", r.Error)
65+
}
66+
}
67+
68+
func TestCountLines_DirectorySuggestsAlternatives(t *testing.T) {
69+
dir := t.TempDir()
70+
tool := &countLinesTool{}
71+
result := callJSON(t, tool, fmt.Sprintf(`{"files":[{"path":"%s"}]}`, dir))
72+
var r struct {
73+
Results []struct {
74+
Error string `json:"error"`
75+
} `json:"results"`
76+
}
77+
mustUnmarshal(t, result, &r)
78+
if len(r.Results) == 0 {
79+
t.Fatal("expected a result")
80+
}
81+
if r.Results[0].Error == "" {
82+
t.Fatal("expected error for directory path")
83+
}
84+
if !strings.Contains(r.Results[0].Error, "tree") && !strings.Contains(r.Results[0].Error, "glob") {
85+
t.Errorf("error should suggest tree or glob, got: %s", r.Results[0].Error)
86+
}
87+
}
88+
89+
func TestBatchRead_DirectorySuggestsAlternatives(t *testing.T) {
90+
dir := t.TempDir()
91+
tool := &batchReadTool{}
92+
result := callJSON(t, tool, fmt.Sprintf(`{"files":[{"path":"%s"}]}`, dir))
93+
var r struct {
94+
Results []struct {
95+
Error string `json:"error"`
96+
} `json:"results"`
97+
}
98+
mustUnmarshal(t, result, &r)
99+
if len(r.Results) == 0 {
100+
t.Fatal("expected a result")
101+
}
102+
if r.Results[0].Error == "" {
103+
t.Fatal("expected error for directory path")
104+
}
105+
if !strings.Contains(r.Results[0].Error, "tree") && !strings.Contains(r.Results[0].Error, "search_files") && !strings.Contains(r.Results[0].Error, "glob") {
106+
t.Errorf("error should suggest alternatives (tree/search_files/glob), got: %s", r.Results[0].Error)
107+
}
108+
}
109+
110+
// ── WriteFile Edge Cases (non-conflicting) ───────────────────────────
111+
112+
func TestWriteFile_EmptyPath_Recovery(t *testing.T) {
113+
tool := &writeFileTool{}
114+
result := callJSON(t, tool, `{"path":"","content":"test"}`)
115+
var r struct {
116+
Error string `json:"error"`
117+
}
118+
mustUnmarshal(t, result, &r)
119+
if r.Error == "" {
120+
t.Fatal("expected error for empty path")
121+
}
122+
}
123+
124+
// ── Patch Edge Cases (non-conflicting) ───────────────────────────────
125+
126+
func TestPatch_OldStringNotFound_Recovery(t *testing.T) {
127+
dir := t.TempDir()
128+
path := filepath.Join(dir, "test.txt")
129+
os.WriteFile(path, []byte("hello world"), 0644)
130+
tool := &patchTool{}
131+
result := callJSON(t, tool, fmt.Sprintf(`{"path":"%s","old_string":"zzz","new_string":"yyy"}`, path))
132+
var r struct {
133+
Success bool `json:"success"`
134+
Error string `json:"error"`
135+
}
136+
mustUnmarshal(t, result, &r)
137+
if r.Success {
138+
t.Fatal("expected patch to fail when old_string not found")
139+
}
140+
if !strings.Contains(r.Error, "not found") {
141+
t.Errorf("error should say 'not found', got: %s", r.Error)
142+
}
143+
}
144+
145+
// ── Glob Edge Cases (non-conflicting) ────────────────────────────────
146+
147+
func TestGlob_EmptyPattern_Recovery(t *testing.T) {
148+
tool := &globTool{}
149+
result := callJSON(t, tool, `{"pattern":""}`)
150+
var r struct {
151+
Error string `json:"error"`
152+
}
153+
mustUnmarshal(t, result, &r)
154+
if r.Error == "" {
155+
t.Fatal("expected error for empty pattern")
156+
}
157+
}
158+
159+
// ── Tree Edge Cases (non-conflicting) ────────────────────────────────
160+
161+
func TestTree_DirectoryPath_Recovery(t *testing.T) {
162+
dir := t.TempDir()
163+
os.WriteFile(filepath.Join(dir, "test.txt"), []byte("hello"), 0644)
164+
tool := &treeTool{}
165+
result := callJSON(t, tool, fmt.Sprintf(`{"path":"%s"}`, dir))
166+
var r struct {
167+
Tree struct {
168+
Path string `json:"path"`
169+
IsDir bool `json:"is_dir"`
170+
Children []interface{} `json:"children"`
171+
} `json:"tree"`
172+
Error string `json:"error"`
173+
}
174+
mustUnmarshal(t, result, &r)
175+
if r.Error != "" {
176+
t.Fatalf("error: %s", r.Error)
177+
}
178+
if !r.Tree.IsDir {
179+
t.Errorf("expected root to be a directory")
180+
}
181+
}
182+
183+
// ── ReadFile Binary Detection (non-conflicting) ────────────────────
184+
185+
func TestReadFile_BinaryContent_Recovery(t *testing.T) {
186+
dir := t.TempDir()
187+
path := filepath.Join(dir, "data.bin")
188+
os.WriteFile(path, []byte{0x00, 0xFF, 0x00, 0xFF}, 0644)
189+
tool := &readFileTool{}
190+
result := callJSON(t, tool, fmt.Sprintf(`{"path":"%s"}`, path))
191+
var r struct {
192+
Error string `json:"error"`
193+
}
194+
mustUnmarshal(t, result, &r)
195+
if r.Error == "" {
196+
t.Fatal("expected error for binary file")
197+
}
198+
if !strings.Contains(r.Error, "binary") {
199+
t.Errorf("error should mention 'binary', got: %s", r.Error)
200+
}
201+
}

0 commit comments

Comments
 (0)