chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newCallTestCmd builds a fresh call command bound to an out/err buffer and
|
||||
// resets the package-level flag state to defaults so tests don't leak.
|
||||
func newCallTestCmd(t *testing.T) (*cobra.Command, *bytes.Buffer) {
|
||||
t.Helper()
|
||||
callIndex = "."
|
||||
callJSON = ""
|
||||
callJSONFile = ""
|
||||
callArgs = nil
|
||||
callFormat = "json"
|
||||
callDry = false
|
||||
callQuiet = false
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
cmd := &cobra.Command{Use: "call", RunE: runCall}
|
||||
cmd.SetOut(buf)
|
||||
cmd.SetErr(buf)
|
||||
return cmd, buf
|
||||
}
|
||||
|
||||
// TestCoerceScalar exhaustively covers the deterministic --arg value coercion.
|
||||
func TestCoerceScalar(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want any
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{"null", nil},
|
||||
{"42", int64(42)},
|
||||
{"-7", int64(-7)},
|
||||
{"3.14", 3.14},
|
||||
{"", ""},
|
||||
{"hello", "hello"},
|
||||
{"1.0.0", "1.0.0"}, // not a number — stays a string
|
||||
{`[1,2,3]`, []any{float64(1), float64(2), float64(3)}},
|
||||
{`{"k":"v"}`, map[string]any{"k": "v"}},
|
||||
{`[broken`, `[broken`}, // invalid JSON array — falls back to literal string
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := coerceScalar(c.in)
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("coerceScalar(%q) = %#v (%T), want %#v (%T)", c.in, got, got, c.want, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoerceArg covers the key/value split, the := walrus raw-JSON form, and
|
||||
// error cases.
|
||||
func TestCoerceArg(t *testing.T) {
|
||||
t.Run("plain string", func(t *testing.T) {
|
||||
k, v, err := coerceArg("name=foo")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "name", k)
|
||||
require.Equal(t, "foo", v)
|
||||
})
|
||||
t.Run("bool", func(t *testing.T) {
|
||||
k, v, err := coerceArg("flag=true")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "flag", k)
|
||||
require.Equal(t, true, v)
|
||||
})
|
||||
t.Run("int", func(t *testing.T) {
|
||||
_, v, err := coerceArg("limit=50")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(50), v)
|
||||
})
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
k, v, err := coerceArg("note=")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "note", k)
|
||||
require.Equal(t, "", v)
|
||||
})
|
||||
t.Run("walrus forces raw string", func(t *testing.T) {
|
||||
// version:="1.0" must stay the string "1.0", not the number 1.0.
|
||||
k, v, err := coerceArg(`version:="1.0"`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "version", k)
|
||||
require.Equal(t, "1.0", v)
|
||||
})
|
||||
t.Run("walrus parses raw JSON object", func(t *testing.T) {
|
||||
k, v, err := coerceArg(`opts:={"a":1}`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "opts", k)
|
||||
require.Equal(t, map[string]any{"a": float64(1)}, v)
|
||||
})
|
||||
t.Run("walrus invalid JSON errors", func(t *testing.T) {
|
||||
_, _, err := coerceArg(`x:=not-json`)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("missing equals errors", func(t *testing.T) {
|
||||
_, _, err := coerceArg("noequals")
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("empty key errors", func(t *testing.T) {
|
||||
_, _, err := coerceArg("=value")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLowerCallArgs_Precedence asserts the three precedence layers: base JSON,
|
||||
// inline JSON merged over it, and --arg merged on top (last wins per key).
|
||||
func TestLowerCallArgs_Precedence(t *testing.T) {
|
||||
cmd, _ := newCallTestCmd(t)
|
||||
|
||||
t.Run("inline json plus arg override", func(t *testing.T) {
|
||||
obj, err := lowerCallArgs(cmd, `{"a":1,"b":2}`, "", []string{"b=3"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{"a": float64(1), "b": int64(3)}, obj)
|
||||
})
|
||||
|
||||
t.Run("later arg replaces earlier same key", func(t *testing.T) {
|
||||
obj, err := lowerCallArgs(cmd, "", "", []string{"k=first", "k=second"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "second", obj["k"])
|
||||
})
|
||||
|
||||
t.Run("arg over inline json over file base", func(t *testing.T) {
|
||||
// Write a base file, then merge inline JSON and --arg over it.
|
||||
base := writeTempJSON(t, `{"a":"file","b":"file","c":"file"}`)
|
||||
obj, err := lowerCallArgs(cmd, `{"b":"inline","c":"inline"}`, base, []string{"c=arg"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file", obj["a"]) // only in the file base
|
||||
require.Equal(t, "inline", obj["b"]) // inline beats file
|
||||
require.Equal(t, "arg", obj["c"]) // arg beats inline + file
|
||||
})
|
||||
}
|
||||
|
||||
// TestLowerCallArgs_StdinBase asserts --json - reads the base object from stdin.
|
||||
func TestLowerCallArgs_StdinBase(t *testing.T) {
|
||||
cmd, _ := newCallTestCmd(t)
|
||||
cmd.SetIn(strings.NewReader(`{"from":"stdin","n":1}`))
|
||||
obj, err := lowerCallArgs(cmd, "-", "", []string{"n=2"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "stdin", obj["from"])
|
||||
require.Equal(t, int64(2), obj["n"])
|
||||
}
|
||||
|
||||
// TestLowerCallArgs_NonObjectRejected asserts a non-object base JSON is refused.
|
||||
func TestLowerCallArgs_NonObjectRejected(t *testing.T) {
|
||||
cmd, _ := newCallTestCmd(t)
|
||||
_, err := lowerCallArgs(cmd, `[1,2,3]`, "", nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "JSON object")
|
||||
}
|
||||
|
||||
// TestCallDry_NoDaemon asserts --dry prints the lowered object and the target
|
||||
// tool WITHOUT invoking the daemon (the stub records any call).
|
||||
func TestCallDry_NoDaemon(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
|
||||
called := false
|
||||
callDaemonTool = func(string, string, map[string]any) (json.RawMessage, error) {
|
||||
called = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd, buf := newCallTestCmd(t)
|
||||
callDry = true
|
||||
callJSON = `{"id":"pkg/foo.go::Bar"}`
|
||||
callArgs = []string{"depth=2", "verbose=true"}
|
||||
|
||||
require.NoError(t, runCall(cmd, []string{"get_callers"}))
|
||||
require.False(t, called, "--dry must NOT invoke the daemon (not even tool_profile)")
|
||||
|
||||
out := buf.String()
|
||||
require.Contains(t, out, "tool: get_callers")
|
||||
// The lowered object is printed as indented JSON.
|
||||
require.Contains(t, out, `"id": "pkg/foo.go::Bar"`)
|
||||
require.Contains(t, out, `"depth": 2`)
|
||||
require.Contains(t, out, `"verbose": true`)
|
||||
}
|
||||
|
||||
// cannedToolProfileJSON is a minimal tool_profile response with a descriptors
|
||||
// array covering a read tool and a mutating tool.
|
||||
const cannedToolProfileJSON = `{
|
||||
"live": ["search_symbols", "get_callers"],
|
||||
"deferred": ["edit_file", "rename_symbol"],
|
||||
"descriptors": [
|
||||
{"name":"search_symbols","category":"nav","mutating":false,"presets":["core","nav","readonly"],"summary":"BM25 symbol search."},
|
||||
{"name":"get_callers","category":"nav","mutating":false,"presets":["core","nav","readonly"],"summary":"Who calls a function."},
|
||||
{"name":"edit_file","category":"edit","mutating":true,"presets":["core","edit"],"summary":"Edit a file."},
|
||||
{"name":"rename_symbol","category":"edit","mutating":true,"presets":["edit"],"summary":"Rename across files."}
|
||||
]
|
||||
}`
|
||||
|
||||
// TestCall_UnknownToolSuggestsNearest asserts that, when a daemon is reachable
|
||||
// and returns a catalog, an unknown tool name yields an error listing the
|
||||
// nearest matches plus the `gortex tools search` hint.
|
||||
func TestCall_UnknownToolSuggestsNearest(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
|
||||
var calledTools []string
|
||||
callDaemonTool = func(_ string, tool string, _ map[string]any) (json.RawMessage, error) {
|
||||
calledTools = append(calledTools, tool)
|
||||
if tool == "tool_profile" {
|
||||
return json.RawMessage(cannedToolProfileJSON), nil
|
||||
}
|
||||
t.Fatalf("must not call %q after a failed name validation", tool)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd, _ := newCallTestCmd(t)
|
||||
err := runCall(cmd, []string{"get_caller"}) // typo of get_callers
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown tool")
|
||||
require.Contains(t, err.Error(), "get_callers", "should suggest the nearest match")
|
||||
require.Contains(t, err.Error(), "gortex tools search")
|
||||
require.Equal(t, []string{"tool_profile"}, calledTools, "only the catalog fetch should happen")
|
||||
}
|
||||
|
||||
// TestCall_KnownMutatingToolWarns asserts a known mutating tool both forwards
|
||||
// the call and prints the stderr write note (unless --quiet).
|
||||
func TestCall_KnownMutatingToolWarns(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
|
||||
callDaemonTool = func(_ string, tool string, args map[string]any) (json.RawMessage, error) {
|
||||
if tool == "tool_profile" {
|
||||
return json.RawMessage(cannedToolProfileJSON), nil
|
||||
}
|
||||
require.Equal(t, "edit_file", tool)
|
||||
require.Equal(t, "json", args["format"])
|
||||
return json.RawMessage(`{"status":"ok"}`), nil
|
||||
}
|
||||
|
||||
cmd, buf := newCallTestCmd(t)
|
||||
require.NoError(t, runCall(cmd, []string{"edit_file"}))
|
||||
out := buf.String()
|
||||
require.Contains(t, out, "note: edit_file writes")
|
||||
require.Contains(t, out, `"status": "ok"`)
|
||||
}
|
||||
|
||||
// TestCall_QuietSuppressesMutatingNote asserts --quiet hides the write note.
|
||||
func TestCall_QuietSuppressesMutatingNote(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
callDaemonTool = func(_ string, tool string, _ map[string]any) (json.RawMessage, error) {
|
||||
if tool == "tool_profile" {
|
||||
return json.RawMessage(cannedToolProfileJSON), nil
|
||||
}
|
||||
return json.RawMessage(`{}`), nil
|
||||
}
|
||||
|
||||
cmd, buf := newCallTestCmd(t)
|
||||
callQuiet = true
|
||||
require.NoError(t, runCall(cmd, []string{"edit_file"}))
|
||||
require.NotContains(t, buf.String(), "note:")
|
||||
}
|
||||
|
||||
// TestCall_NoDaemonSkipsValidation asserts that when the catalog fetch fails
|
||||
// (no daemon), validation is skipped and the normal call path runs — surfacing
|
||||
// whatever the call returns rather than an "unknown tool" error.
|
||||
func TestCall_NoDaemonSkipsValidation(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
|
||||
callDaemonTool = func(_ string, tool string, _ map[string]any) (json.RawMessage, error) {
|
||||
if tool == "tool_profile" {
|
||||
return nil, daemonRequiredErr(".") // no catalog available
|
||||
}
|
||||
require.Equal(t, "some_tool", tool, "the call must proceed despite the catalog miss")
|
||||
return json.RawMessage(`{"ok":true}`), nil
|
||||
}
|
||||
|
||||
cmd, buf := newCallTestCmd(t)
|
||||
require.NoError(t, runCall(cmd, []string{"some_tool"}))
|
||||
require.Contains(t, buf.String(), `"ok": true`)
|
||||
}
|
||||
|
||||
// TestCall_FormatGCXVerbatim asserts gcx/toon output is printed verbatim, not
|
||||
// re-indented through emitDaemonJSON.
|
||||
func TestCall_FormatGCXVerbatim(t *testing.T) {
|
||||
orig := callDaemonTool
|
||||
t.Cleanup(func() { callDaemonTool = orig })
|
||||
|
||||
callDaemonTool = func(_ string, tool string, args map[string]any) (json.RawMessage, error) {
|
||||
if tool == "tool_profile" {
|
||||
return json.RawMessage(cannedToolProfileJSON), nil
|
||||
}
|
||||
require.Equal(t, "gcx", args["format"], "the chosen format must be forwarded")
|
||||
return json.RawMessage("GCX1|results|...\n"), nil
|
||||
}
|
||||
|
||||
cmd, buf := newCallTestCmd(t)
|
||||
callFormat = "gcx"
|
||||
require.NoError(t, runCall(cmd, []string{"search_symbols"}))
|
||||
require.Equal(t, "GCX1|results|...\n", buf.String())
|
||||
}
|
||||
|
||||
// TestCall_DaemonRequired asserts that, with no daemon running and no stub, the
|
||||
// real call path returns the actionable daemon-required error (mirrors
|
||||
// query_daemon_required_test.go). The catalog fetch fails first (no daemon), so
|
||||
// validation is skipped and the call itself surfaces the error.
|
||||
func TestCall_DaemonRequired(t *testing.T) {
|
||||
// Point at a temp dir no daemon could possibly track so resolveExecutor
|
||||
// returns ErrNoExecutor -> daemonRequiredErr.
|
||||
cmd, _ := newCallTestCmd(t)
|
||||
callIndex = t.TempDir()
|
||||
err := runCall(cmd, []string{"search_symbols"})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "gortex track")
|
||||
}
|
||||
|
||||
// writeTempJSON writes content to a temp file and returns its path.
|
||||
func writeTempJSON(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := dir + "/base.json"
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user