Files
WeKnora/cli/cmd/root_test.go
T
nullkey cc8254f862 refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):

1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
   (gh, aws, stripe, lark): none expose --dry-run. The mainstream that
   does (kubectl/git/helm/ansible) operates on declarative manifests
   or local state where the preview is materially different from the
   executed action. WeKnora's CLI just echoed the same parameters
   that would have gone on the wire — the preview added no real
   signal over `--help` + reading the call site. Removes:
   - root --dry-run persistent flag + cmdutil/dryrun.go
   - DryRun fields + EmitDryRun calls in 12 write commands
   - format.Envelope.DryRun field
   - 8 corresponding *_test.go cases
   - --dry-run mention from README.md and CHANGELOG.md
   - "dry_run":false from 16 golden envelopes

2. Migrate every --json output to bare data:
   - New format.WriteJSON / WriteJSONFiltered helpers
     (cli/internal/format/bare.go) share filterArrayItems /
     filterObjectKeys / writeJQ with the (still-live for now) envelope
     filter helpers.
   - Read commands (kb/doc/session list+view, search chunks/docs/
     sessions/kb, auth list/status, agent list/view, context list,
     doctor) emit bare arrays / objects on stdout.
   - Write commands (kb create/edit/delete/pin/empty, doc upload/
     upload_recursive/delete, session delete, auth login/logout/
     refresh/token, link/unlink, context add/use/remove, agent
     invoke, chat, api, version) emit bare result objects. Risk
     classification dropped — the resource + exit code already
     convey the action.

Per-command shape changes:
   list / search       → []T   (was {ok, data:{items:[…]}})
   view                → T     (was {ok, data:T, _meta:…})
   create / edit       → T
   delete / pin / etc. → {id, …action result…}
   doctor              → {summary, checks}
   api                 → {status, headers, body}

_meta dropped on the read path:
   pagination (page/page_size/total/has_more) — agents iterate with
   --all-pages or accept --limit (gh CLI parity);
   kb_id / context echo — caller already knows what it asked for.

Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
2026-05-15 12:03:56 +08:00

176 lines
6.0 KiB
Go

package cmd
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
)
func TestRoot_Help(t *testing.T) {
var out bytes.Buffer
root := NewRootCmd(cmdutil.New())
root.SetArgs([]string{"--help"})
root.SetOut(&out)
require.NoError(t, root.Execute())
got := out.String()
assert.Contains(t, got, "weknora")
assert.Contains(t, got, "version")
}
func TestVersion_JSON(t *testing.T) {
var out bytes.Buffer
root := NewRootCmd(cmdutil.New())
root.SetArgs([]string{"version", "--json"})
root.SetOut(&out)
require.NoError(t, root.Execute())
got := out.String()
assert.True(t, strings.HasPrefix(strings.TrimSpace(got), `{`), "expected bare JSON object, got: %q", got)
assert.Contains(t, got, `"version":"`)
assert.NotContains(t, got, `"ok":`)
assert.NotContains(t, got, `"data":`)
}
// Smoke test for cmdutil.ExitCode wiring; full coverage lives in
// cli/internal/cmdutil/exit_test.go.
func TestExecute_ExitCodeSurface(t *testing.T) {
assert.Equal(t, 0, cmdutil.ExitCode(nil))
assert.Equal(t, 1, cmdutil.ExitCode(assert.AnError))
}
// TestMapCobraError_PinnedPrefixes guards against silent breakage if cobra
// changes the message format of unknown-command / required-flag / arg-count
// errors. Cobra v1.10 emits these via fmt.Errorf in args.go and command.go;
// if a future bump alters the wording, this test fails loudly so we update
// cobraFlagErrorPrefixes (or migrate to typed sentinels if cobra ever
// provides them).
func TestMapCobraError_PinnedPrefixes(t *testing.T) {
t.Run("unknown command", func(t *testing.T) {
root := NewRootCmd(cmdutil.New())
root.SetArgs([]string{"bogus"})
root.SetErr(&bytes.Buffer{})
root.SetOut(&bytes.Buffer{})
err := root.Execute()
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "unknown command "),
"cobra unknown-command prefix changed; update cobraFlagErrorPrefixes. got: %q", err.Error())
})
t.Run("required flag(s)", func(t *testing.T) {
// Self-contained probe — the pin must hold even before resource commands
// register their own required flags. RunE is required: without it cobra
// treats the command as a parent and skips ValidateRequiredFlags.
probe := &cobra.Command{Use: "probe", RunE: func(*cobra.Command, []string) error { return nil }}
probe.Flags().String("host", "", "")
require.NoError(t, probe.MarkFlagRequired("host"))
probe.SetErr(&bytes.Buffer{})
probe.SetOut(&bytes.Buffer{})
err := probe.Execute()
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "required flag(s)"),
"cobra required-flag prefix changed; update cobraFlagErrorPrefixes. got: %q", err.Error())
})
t.Run("accepts N arg(s) — ExactArgs", func(t *testing.T) {
probe := &cobra.Command{
Use: "probe",
Args: cobra.ExactArgs(1),
RunE: func(*cobra.Command, []string) error { return nil },
}
probe.SetArgs([]string{}) // no args, but ExactArgs(1) wants 1
probe.SetErr(&bytes.Buffer{})
probe.SetOut(&bytes.Buffer{})
err := probe.Execute()
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), "accepts "),
"cobra ExactArgs prefix changed; update cobraFlagErrorPrefixes. got: %q", err.Error())
})
}
func TestMapCobraError(t *testing.T) {
t.Run("nil passes through", func(t *testing.T) {
assert.Nil(t, MapCobraError(nil))
})
t.Run("non-matching error passes through", func(t *testing.T) {
err := MapCobraError(assert.AnError)
assert.Equal(t, assert.AnError, err)
})
t.Run("unknown command wraps as FlagError", func(t *testing.T) {
err := MapCobraError(errors.New(`unknown command "bogus" for "weknora"`))
var fe *cmdutil.FlagError
assert.True(t, errors.As(err, &fe))
})
t.Run("required flag wraps as FlagError", func(t *testing.T) {
err := MapCobraError(errors.New(`required flag(s) "host" not set`))
var fe *cmdutil.FlagError
assert.True(t, errors.As(err, &fe))
})
}
// TestRoot_ContextFlagPropagation guards the cobra → Factory wiring of the
// global --context flag. Without this, a future refactor that disconnects
// PersistentPreRun from f.ContextOverride would only fail e2e — the
// per-package TestFactory_ContextOverride only proves the Factory side.
func TestRoot_ContextFlagPropagation(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"no flag", []string{"version"}, ""},
{"global before subcmd", []string{"--context", "staging", "version"}, "staging"},
{"--context=value form", []string{"--context=prod", "version"}, "prod"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := cmdutil.New()
root := NewRootCmd(f)
root.SetArgs(tc.args)
root.SetOut(&bytes.Buffer{})
root.SetErr(&bytes.Buffer{})
require.NoError(t, root.Execute())
assert.Equal(t, tc.want, f.ContextOverride)
})
}
}
func TestArgsRequestJSON(t *testing.T) {
// v0.4 R-1: --json is now StringSlice (gh-style field filter). Any
// `--json` token in args means the user wants JSON output, regardless
// of value (boolean string parsing dropped).
cases := []struct {
name string
args []string
want bool
}{
{"empty", nil, false},
{"--json bare", []string{"version", "--json"}, true},
{"--json=id,name", []string{"kb", "list", "--json=id,name"}, true},
{"--json=anything", []string{"version", "--json=foo"}, true},
{"unrelated", []string{"bogus", "--kb", "x"}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, argsRequestJSON(tc.args))
})
}
}
func TestWantsJSONOutput(t *testing.T) {
// Build a minimal *cobra.Command with the json flag directly so we test
// the helper without going through cobra's parse pipeline. WantsJSONOutput
// reads cmd.Flags() which on a fresh command equals LocalFlags().
c := &cobra.Command{Use: "x"}
c.Flags().Bool("json", false, "")
assert.False(t, WantsJSONOutput(c), "default: --json unset")
require.NoError(t, c.Flags().Set("json", "true"))
assert.True(t, WantsJSONOutput(c), "--json=true honored")
}