Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

251 lines
7.7 KiB
Go

package agentcli
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/zzet/gortex/internal/llm"
)
type fakeOpts struct {
stdout string
stderr string
exitCode int
sleep time.Duration
}
// fakeBin writes a shell script standing in for a coding-agent CLI. It
// logs its argv (one per line) and stdin to sidecar files, echoes a
// canned stdout, and exits with a chosen code — enough to exercise
// every engine path (arg construction, stdin piping, extraction,
// stderr/exit handling, timeout) without a real CLI install.
func fakeBin(t *testing.T, opts fakeOpts) (bin, argsLog, stdinLog string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("fake-binary harness uses /bin/sh")
}
dir := t.TempDir()
bin = filepath.Join(dir, "fake-cli.sh")
argsLog = filepath.Join(dir, "args.txt")
stdinLog = filepath.Join(dir, "stdin.txt")
q := func(s string) string { return strings.ReplaceAll(s, "'", "'\\''") }
body := "#!/bin/sh\n" +
"printf '%s\\n' \"$@\" > '" + argsLog + "'\n" +
"cat > '" + stdinLog + "'\n"
if opts.sleep > 0 {
body += fmt.Sprintf("sleep %d\n", int(opts.sleep.Seconds())+1)
}
if opts.stderr != "" {
body += "printf '%s' '" + q(opts.stderr) + "' >&2\n"
}
if opts.stdout != "" {
body += "printf '%s' '" + q(opts.stdout) + "'\n"
}
if opts.exitCode != 0 {
body += fmt.Sprintf("exit %d\n", opts.exitCode)
}
if err := os.WriteFile(bin, []byte(body), 0o755); err != nil {
t.Fatal(err)
}
return bin, argsLog, stdinLog
}
func readLines(t *testing.T, path string) []string {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
s := strings.TrimRight(string(raw), "\n")
if s == "" {
return nil
}
return strings.Split(s, "\n")
}
func TestComplete_FlagDeliveryBuildsArgv(t *testing.T) {
bin, argsLog, stdinLog := fakeBin(t, fakeOpts{stdout: "the answer"})
p, err := New(Spec{
ProviderID: "cursor",
Model: "claude-x",
ModelFlag: "--model",
BaseArgs: []string{"--output-format", "text"},
Delivery: DeliveryFlag,
PromptFlag: "-p",
ExtraArgs: []string{"--trust"},
}, bin, "cursor-agent")
if err != nil {
t.Fatal(err)
}
resp, err := p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hello"}},
})
if err != nil {
t.Fatal(err)
}
if resp.Text != "the answer" {
t.Errorf("text=%q", resp.Text)
}
args := readLines(t, argsLog)
want := []string{"--output-format", "text", "--model", "claude-x", "--trust", "-p", "User: hello"}
if strings.Join(args, "|") != strings.Join(want, "|") {
t.Errorf("argv=%v\nwant %v", args, want)
}
// Flag delivery must not feed the prompt on stdin.
if got := readLines(t, stdinLog); len(got) != 0 {
t.Errorf("stdin should be empty for flag delivery, got %v", got)
}
}
func TestComplete_ArgDeliveryAppendsPositional(t *testing.T) {
bin, argsLog, _ := fakeBin(t, fakeOpts{stdout: "ok"})
p, err := New(Spec{
ProviderID: "opencode",
Model: "anthropic/claude-sonnet-4-6",
ModelFlag: "--model",
BaseArgs: []string{"run"},
Delivery: DeliveryArg,
}, bin, "opencode")
if err != nil {
t.Fatal(err)
}
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
}); err != nil {
t.Fatal(err)
}
args := readLines(t, argsLog)
want := []string{"run", "--model", "anthropic/claude-sonnet-4-6", "User: hi"}
if strings.Join(args, "|") != strings.Join(want, "|") {
t.Errorf("argv=%v\nwant %v", args, want)
}
}
func TestComplete_StdinDeliveryPipesPrompt(t *testing.T) {
bin, argsLog, stdinLog := fakeBin(t, fakeOpts{stdout: "ok"})
p, err := New(Spec{
ProviderID: "copilot",
Delivery: DeliveryStdin,
}, bin, "copilot")
if err != nil {
t.Fatal(err)
}
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "piped"}},
}); err != nil {
t.Fatal(err)
}
if got := readLines(t, argsLog); len(got) != 0 {
t.Errorf("stdin delivery should pass no prompt arg, got %v", got)
}
if got := strings.TrimSpace(strings.Join(readLines(t, stdinLog), "\n")); got != "User: piped" {
t.Errorf("stdin=%q want 'User: piped'", got)
}
}
func TestComplete_StructuredExtractsJSONFromChattyReply(t *testing.T) {
bin, argsLog, _ := fakeBin(t, fakeOpts{stdout: "Here you go:\n{\"terms\":[\"jwt\"]}\nhope that helps"})
p, err := New(Spec{ProviderID: "copilot", Delivery: DeliveryFlag, PromptFlag: "-p"}, bin, "copilot")
if err != nil {
t.Fatal(err)
}
resp, err := p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "auth"}},
Shape: llm.ShapeExpandTerms,
})
if err != nil {
t.Fatal(err)
}
if resp.Text != `{"terms":["jwt"]}` {
t.Errorf("text=%q want the extracted JSON", resp.Text)
}
// The structured prompt must carry the schema rider (the prompt arg
// spans multiple lines, so check the whole logged argv).
args := strings.Join(readLines(t, argsLog), "\n")
if !strings.Contains(args, "JSON Schema") {
t.Errorf("expected a schema rider in the prompt arg, got %q", args)
}
}
func TestComplete_NonZeroExitErrorsWithStderr(t *testing.T) {
bin, _, _ := fakeBin(t, fakeOpts{stderr: "not signed in", exitCode: 1})
p, err := New(Spec{ProviderID: "copilot", Delivery: DeliveryFlag, PromptFlag: "-p"}, bin, "copilot")
if err != nil {
t.Fatal(err)
}
_, err = p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
})
if err == nil {
t.Fatal("expected an error for a non-zero exit")
}
if !strings.Contains(err.Error(), "not signed in") {
t.Errorf("error should surface stderr, got %v", err)
}
}
func TestComplete_EmptyOutputErrors(t *testing.T) {
bin, _, _ := fakeBin(t, fakeOpts{stdout: ""})
p, err := New(Spec{ProviderID: "opencode", Delivery: DeliveryArg, BaseArgs: []string{"run"}}, bin, "opencode")
if err != nil {
t.Fatal(err)
}
if _, err := p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
}); err == nil {
t.Fatal("expected an error for empty CLI output")
}
}
func TestComplete_Timeout(t *testing.T) {
bin, _, _ := fakeBin(t, fakeOpts{stdout: "late", sleep: 2 * time.Second})
p, err := New(Spec{ProviderID: "cursor", Delivery: DeliveryFlag, PromptFlag: "-p", Timeout: 500 * time.Millisecond}, bin, "cursor-agent")
if err != nil {
t.Fatal(err)
}
_, err = p.Complete(context.Background(), llm.CompletionRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: "hi"}},
})
if err == nil || !strings.Contains(err.Error(), "timed out") {
t.Fatalf("expected a timeout error, got %v", err)
}
}
func TestNew_BinaryNotFound(t *testing.T) {
if _, err := New(Spec{ProviderID: "copilot"}, "definitely-not-a-real-binary-xyz", "copilot"); err == nil {
t.Fatal("expected an error when the binary is not on PATH")
}
}
func TestName(t *testing.T) {
bin, _, _ := fakeBin(t, fakeOpts{stdout: "x"})
p, err := New(Spec{ProviderID: "opencode", Delivery: DeliveryArg}, bin, "opencode")
if err != nil {
t.Fatal(err)
}
if p.Name() != "opencode" {
t.Errorf("Name()=%q", p.Name())
}
}
func TestFlattenPrompt(t *testing.T) {
got := FlattenPrompt([]llm.Message{
{Role: llm.RoleSystem, Content: "be terse"},
{Role: llm.RoleUser, Content: "find auth"},
{Role: llm.RoleAssistant, Content: "looking"},
{Role: llm.RoleTool, ToolName: "search", Content: "found x"},
})
for _, want := range []string{"System instructions:\nbe terse", "User: find auth", "Assistant: looking", "Tool result (search):\nfound x"} {
if !strings.Contains(got, want) {
t.Errorf("flattened prompt missing %q\n--- got ---\n%s", want, got)
}
}
}