Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cli/internal/cli/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,63 @@ func TestNamespaceDispatchCallsTool(t *testing.T) {
t.Errorf("expected tool output in stdout, got: %q", out)
}
}

// TestNamespaceDispatchGlobalFlagBeforeTool covers a global flag positioned
// before the tool subcommand (e.g. "subtext --format=text doc list"). Because
// the namespace command uses DisableFlagParsing, cobra hands the leading
// --format through as args[0]; without extraction the dispatcher glues it onto
// the tool name ("doc---format=text") and the lookup 404s. The dispatcher must
// instead consume it as a global and resolve the real tool.
func TestNamespaceDispatchGlobalFlagBeforeTool(t *testing.T) {
var sawCallTool bool

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req struct {
Method string `json:"method"`
}
_ = json.Unmarshal(body, &req)

w.Header().Set("Content-Type", "application/json")
switch req.Method {
case "tools/list":
io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"tools":[`+
`{"name":"doc-list","description":"List docs","inputSchema":{"type":"object","properties":{},"required":[]}}`+
`]}}`)
case "tools/call":
sawCallTool = true
io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"dispatched-ok"}],"isError":false}}`)
default:
http.Error(w, "unexpected method "+req.Method, http.StatusBadRequest)
}
}))
t.Cleanup(srv.Close)

t.Setenv("SUBTEXT_ALLOW_INSECURE_ENDPOINT", "1")
saveGlobalFlags(t)
globalFlags.endpoint = srv.URL
globalFlags.apiKey = "test-key"
globalFlags.format = "json" // a leading --format=text below must override this
globalConfig = config.File{}

docCmd.SetContext(context.Background())

var err error
out := captureStdout(t, func() {
err = namespaceRunE(docCmd, []string{"--format=text", "list"})
})

if err != nil {
t.Fatalf("namespace dispatch with leading global flag returned error: %v", err)
}
if !sawCallTool {
t.Error("server never received tools/call; leading --format was likely glued onto the tool name")
}
if globalFlags.format != "text" {
t.Errorf("leading --format=text was not consumed as a global flag; got format=%q", globalFlags.format)
}
// text format renders the content directly (no JSON wrapping).
if !strings.Contains(out, "dispatched-ok") {
t.Errorf("expected text-rendered tool output, got: %q", out)
}
}
6 changes: 6 additions & 0 deletions cli/internal/cli/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
// to the "live-act-click" tool. DisableFlagParsing must be set on the parent
// command, so --help is intercepted here before delegating.
func namespaceRunE(cmd *cobra.Command, args []string) error {
// With DisableFlagParsing, cobra hands through any global flags that appear
// before the tool subcommand (e.g. "subtext --format=text live view-list").
// Pull them out before deriving the tool name, otherwise a leading global
// gets glued onto it ("live---format=text") and the lookup 404s. runCall
// re-runs extractGlobalFlags on the remainder, a no-op once globals are gone.
args = extractGlobalFlags(args)
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
return cmd.Help()
}
Expand Down