26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
149 lines
4.3 KiB
Go
149 lines
4.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type fallbackTestAgent struct {
|
|
name string
|
|
run func() (*Result, error)
|
|
calls int
|
|
resumable bool
|
|
}
|
|
|
|
func (a *fallbackTestAgent) Name() string { return a.name }
|
|
|
|
func (a *fallbackTestAgent) Run(context.Context, RunOpts) (*Result, error) {
|
|
a.calls++
|
|
return a.run()
|
|
}
|
|
|
|
func (a *fallbackTestAgent) Close() error { return nil }
|
|
|
|
func (a *fallbackTestAgent) SupportsSessionResume() bool { return a.resumable }
|
|
|
|
func TestFallbackAgentFallsBackOnLaunchFailure(t *testing.T) {
|
|
first := &fallbackTestAgent{
|
|
name: "codex",
|
|
run: func() (*Result, error) {
|
|
return nil, errors.New(`codex start: exec: "codex": executable file not found`)
|
|
},
|
|
}
|
|
second := &fallbackTestAgent{
|
|
name: "claude",
|
|
run: func() (*Result, error) {
|
|
return &Result{Text: "ok"}, nil
|
|
},
|
|
}
|
|
var chunks []string
|
|
|
|
result, err := NewFallback([]Agent{first, second}).Run(context.Background(), RunOpts{
|
|
OnChunk: func(text string) { chunks = append(chunks, text) },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
if result == nil || result.Text != "ok" {
|
|
t.Fatalf("Run() result = %+v, want text ok", result)
|
|
}
|
|
if first.calls != 1 || second.calls != 1 {
|
|
t.Fatalf("calls = first %d second %d, want 1/1", first.calls, second.calls)
|
|
}
|
|
joined := strings.Join(chunks, "\n")
|
|
if !strings.Contains(joined, "agent codex failed") || !strings.Contains(joined, "falling back to claude") {
|
|
t.Fatalf("fallback log missing, got %q", joined)
|
|
}
|
|
}
|
|
|
|
func TestFallbackAgentDoesNotFallBackOnFindingsResult(t *testing.T) {
|
|
first := &fallbackTestAgent{
|
|
name: "codex",
|
|
run: func() (*Result, error) {
|
|
return &Result{Output: []byte(`{"findings":[{"severity":"warning","description":"issue"}],"summary":"1 issue"}`)}, nil
|
|
},
|
|
}
|
|
second := &fallbackTestAgent{
|
|
name: "claude",
|
|
run: func() (*Result, error) {
|
|
return &Result{Text: "should not run"}, nil
|
|
},
|
|
}
|
|
|
|
result, err := NewFallback([]Agent{first, second}).Run(context.Background(), RunOpts{})
|
|
if err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
if string(result.Output) == "" {
|
|
t.Fatalf("Run() result = %+v, want findings output", result)
|
|
}
|
|
if first.calls != 1 || second.calls != 0 {
|
|
t.Fatalf("calls = first %d second %d, want 1/0", first.calls, second.calls)
|
|
}
|
|
}
|
|
|
|
func TestFallbackAgentDoesNotFallBackOnStructuredOutputError(t *testing.T) {
|
|
parseErr := errors.New(`codex output parse: invalid JSON (output snippet: "not json")`)
|
|
first := &fallbackTestAgent{
|
|
name: "codex",
|
|
run: func() (*Result, error) {
|
|
return nil, parseErr
|
|
},
|
|
}
|
|
second := &fallbackTestAgent{
|
|
name: "claude",
|
|
run: func() (*Result, error) {
|
|
return &Result{Text: "should not run"}, nil
|
|
},
|
|
}
|
|
|
|
_, err := NewFallback([]Agent{first, second}).Run(context.Background(), RunOpts{})
|
|
if !errors.Is(err, parseErr) {
|
|
t.Fatalf("Run() error = %v, want %v", err, parseErr)
|
|
}
|
|
if first.calls != 1 || second.calls != 0 {
|
|
t.Fatalf("calls = first %d second %d, want 1/0", first.calls, second.calls)
|
|
}
|
|
}
|
|
|
|
func TestFallbackAgent_ForwardsSessionCapability(t *testing.T) {
|
|
first := &fallbackTestAgent{name: "codex", resumable: true, run: func() (*Result, error) { return &Result{}, nil }}
|
|
second := &fallbackTestAgent{name: "claude", resumable: true, run: func() (*Result, error) { return &Result{}, nil }}
|
|
if !SupportsSessionResume(NewFallback([]Agent{WithSteering(first), WithSteering(second)})) {
|
|
t.Fatal("fallback's primary resumable agent must retain session support")
|
|
}
|
|
}
|
|
|
|
func TestFallbackAgent_ReportsEveryAttempt(t *testing.T) {
|
|
first := &fallbackTestAgent{
|
|
name: "codex",
|
|
run: func() (*Result, error) {
|
|
return nil, errors.New(`codex start: executable not found`)
|
|
},
|
|
}
|
|
second := &fallbackTestAgent{
|
|
name: "claude",
|
|
run: func() (*Result, error) {
|
|
return &Result{Text: "ok"}, nil
|
|
},
|
|
}
|
|
var attempts []Attempt
|
|
_, err := NewFallback([]Agent{first, second}).Run(context.Background(), RunOpts{
|
|
OnAttempt: func(attempt Attempt) { attempts = append(attempts, attempt) },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if len(attempts) != 2 {
|
|
t.Fatalf("attempts = %d, want 2", len(attempts))
|
|
}
|
|
if attempts[0].Agent != "codex" || attempts[0].Err == nil {
|
|
t.Fatalf("first attempt = %+v", attempts[0])
|
|
}
|
|
if attempts[1].Agent != "claude" || attempts[1].Result == nil || attempts[1].Result.Text != "ok" {
|
|
t.Fatalf("second attempt = %+v", attempts[1])
|
|
}
|
|
}
|