wip: P1 feature batch — retry, search, code symbols, diff/status/test commands, tool output head+tail

- retry logic for transient LLM errors (openai.go)
- file_search context_lines + max_results (file.go)
- FormatResult now preserves head+tail with omitted bytes (tools.go)
- /status /diff /test slash commands wired (workflow.go, app.go)
- CodeSymbolsTool for Go package/type/func listing (code.go)
- README updated with v0.0.4 features
This commit is contained in:
loveuer
2026-06-24 10:13:28 +08:00
parent e4c75c7c0e
commit 86f69f6dd3
11 changed files with 724 additions and 57 deletions
+66 -3
View File
@@ -43,6 +43,41 @@ func TestFileTools(t *testing.T) {
}
}
func TestFileSearchSupportsContextAndMaxResults(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "notes.txt")
content := strings.Join([]string{"before", "needle one", "middle", "needle two", "after"}, "\n")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
search := NewFileSearchTool(dir)
out, err := search.walkSearch(context.Background(), fileSearchArgs{Query: "needle", ContextLines: 1, MaxResults: 1}, dir)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"-- ", "before", "needle one", "middle", "stopped after 1 matches"} {
if !strings.Contains(out, want) {
t.Fatalf("search output missing %q:\n%s", want, out)
}
}
if strings.Contains(out, "needle two") {
t.Fatalf("search should stop after max_results:\n%s", out)
}
}
func TestFormatResultKeepsHeadAndTail(t *testing.T) {
input := strings.Join([]string{"head-1", "head-2", strings.Repeat("x", 220), "tail-1", "tail-2"}, "\n")
out := FormatResultLimit(input, 220)
for _, want := range []string{"head-1", "truncated: showing head and tail", "tail-2"} {
if !strings.Contains(out, want) {
t.Fatalf("formatted output missing %q:\n%s", want, out)
}
}
if len(out) > 220 {
t.Fatalf("formatted output len = %d, want <= 220", len(out))
}
}
func TestShellRunTool(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "x.txt"), []byte("x"), 0o644); err != nil {
@@ -69,12 +104,12 @@ func TestReadOnlyBuiltinsExposeOpenAICompatibleNamesAndAliases(t *testing.T) {
}
}
for _, want := range []string{"file_list", "file_read", "file_search"} {
for _, want := range []string{"file_list", "file_read", "file_search", "code_symbols"} {
if !slices.Contains(names, want) {
t.Fatalf("definitions missing %s: %#v", want, names)
}
}
for _, alias := range []string{"file.list", "file.read", "file.search"} {
for _, alias := range []string{"file.list", "file.read", "file.search", "code.symbols"} {
if _, ok := registry.Get(alias); !ok {
t.Fatalf("alias missing: %s", alias)
}
@@ -84,6 +119,34 @@ func TestReadOnlyBuiltinsExposeOpenAICompatibleNamesAndAliases(t *testing.T) {
}
}
func TestCodeSymbolsToolListsGoSymbols(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "sample.go")
if err := os.WriteFile(path, []byte(`package sample
import "context"
type Worker struct{}
type Runner interface{ Run(context.Context) error }
func NewWorker() *Worker { return &Worker{} }
func (w *Worker) Run(ctx context.Context) error { return nil }
`), 0o644); err != nil {
t.Fatal(err)
}
tool := NewCodeSymbolsTool(dir)
out, err := tool.Execute(context.Background(), json.RawMessage(`{"path":"sample.go"}`))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"package sample", "imports: context", "type Worker struct", "type Runner interface", "func NewWorker", "method *Worker.Run"} {
if !strings.Contains(out, want) {
t.Fatalf("symbols output missing %q:\n%s", want, out)
}
}
}
func TestRegistryCanExposeWebSearch(t *testing.T) {
registry := ReadOnlyBuiltins(t.TempDir())
registry.Register(NewBuiltinSearchTool(nil))
@@ -132,7 +195,7 @@ func TestAgentInstructionsExplainCommandModes(t *testing.T) {
func TestAgentInstructionsExplainWebSearch(t *testing.T) {
withSearch := AgentInstructions("/tmp/project", false)
for _, want := range []string{"use web_search", "Use web_fetch", "latest information", "source URLs", "Available tools: file_list, file_read, file_search, web_search, web_fetch"} {
for _, want := range []string{"use web_search", "Use web_fetch", "latest information", "source URLs", "Available tools: file_list, file_read, file_search, code_symbols, web_search, web_fetch"} {
if !strings.Contains(withSearch, want) {
t.Fatalf("web search instructions missing %q:\n%s", want, withSearch)
}