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

328 lines
11 KiB
Go

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
}