Files
WeKnora/cli/acceptance/contract/helpers_test.go
T
nullkey bb592a59a6 feat(cli): contract test suite + dependabot (PR-8)
cli/acceptance/contract/:
  envelope_test.go    — 16 envelope golden cases (9 commands × {success/error
                        variants}; 3 cases dropped with rationale: doctor.success
                        non-offline has unstable timing detail; auth_login.* needs
                        stdin/keyring scaffold deferred to v0.2; context_use.error
                        needs leaf-local --json deferred to follow-up)
  errorcodes_test.go  — single-direction AST scan of cli/cmd/ extracting first
                        arg of cmdutil.NewError / cmdutil.Wrapf calls;
                        ClassifyHTTPError dynamic-classify bridged via
                        cmdutil.ClassifyHTTPErrorOutputs() per spec §4.3.
  testdata/envelopes/ — 16 JSON golden files

helpers_test.go (PR-6 scaffold) extended:
  runCmd now wires cobra Out/Err sinks (version uses c.OutOrStdout) AND
  replicates cmd.Execute()'s error-envelope path so error-case goldens are
  populated. Without this, every error scenario's golden was 0 bytes.

cli/cmd/root.go: mapCobraError → MapCobraError, wantsJSONOutput → WantsJSONOutput
                 (exported so the contract test helper can replicate Execute()'s
                 envelope-printing path without calling Execute() itself).
                 root_test.go updated to use new exported names.

.github/dependabot.yml (新增):gomod /cli + github-actions weekly,gh-style
                              ignore semver-major to avoid noise. Open-source
                              dependency safety,independent of release cadence.

v0.1 不发布到任何分发平台 (release infra 推迟到发布窗口 milestone)。
2026-05-09 12:18:01 +08:00

121 lines
4.3 KiB
Go

// cli/acceptance/contract/helpers_test.go
package contract_test
import (
"bytes"
"context"
"flag"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/Tencent/WeKnora/cli/cmd"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
sdk "github.com/Tencent/WeKnora/client"
)
// update is the standard Go test golden-update flag.
// go test -update ./acceptance/contract/...
// Mirrors gh / kubectl / golang-migrate convention.
var update = flag.Bool("update", false, "update golden files")
// newTestFactory builds a Factory whose Client returns mockClient.
// Caller must NOT use t.Parallel() — see iostreams.SetForTest contract.
//
// WEKNORA_BASE_URL is set when mockServer is non-nil. v0.0 buildClient does
// not currently honor this env var (it reads from config.Host); commands that
// need the mock URL must rely on the mockClient injection above. The env
// is set anyway as a forward-affordance for any direct net/http callers
// added in PR-7+ (e.g. doctor's PingBaseURL HEAD /health).
func newTestFactory(t *testing.T, mockServer *httptest.Server, mockClient *sdk.Client) *cmdutil.Factory {
t.Helper()
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("XDG_CACHE_HOME", t.TempDir())
if mockServer != nil {
t.Setenv("WEKNORA_BASE_URL", mockServer.URL)
}
f := cmdutil.New()
if mockClient != nil {
f.Client = func() (*sdk.Client, error) { return mockClient, nil }
}
return f
}
// runCmd executes the root command in-process and returns captured stdout/stderr.
// Replaces iostreams.IO singleton via SetForTest (auto-restored in t.Cleanup).
//
// Mirrors cmd.Execute() carefully: callers expect the same envelope-printing
// behavior the real entrypoint provides. The helper (a) wires the cobra Out /
// Err sinks to the same buffers it returns (the `version` leaf and any future
// command using c.OutOrStdout would otherwise leak to os.Stdout), and (b)
// re-runs the error-envelope path so failure cases produce the JSON envelope
// the contract test compares against. Without (b), every error scenario's
// golden would be empty.
func runCmd(t *testing.T, f *cmdutil.Factory, args ...string) (stdout, stderr string, exitCode int) {
t.Helper()
out, errBuf := iostreams.SetForTest(t)
root := cmd.NewRootCmd(f) // exported in cli/cmd/root.go (Task 16)
root.SetArgs(args)
root.SetContext(context.Background())
root.SetOut(out)
root.SetErr(errBuf)
leaf, err := root.ExecuteC()
if err != nil {
err = cmd.MapCobraError(err)
if cmd.WantsJSONOutput(leaf) {
cmdutil.PrintErrorEnvelope(iostreams.IO.Out, err)
} else {
cmdutil.PrintError(iostreams.IO.Err, err)
}
}
return out.String(), errBuf.String(), cmdutil.ExitCode(err)
}
// assertGolden compares got against the JSON golden file at path.
// With -update, writes got to path. Normalizes _meta.request_id to "<id>"
// before compare (only field known unstable in v0.0).
//
// CRLF normalization: Windows checkouts with the default core.autocrlf=true
// turn LF in tracked text files into CRLF on disk. The command output is
// always LF, so byte-equal would fail despite identical content. .gitattributes
// is the primary defense (forcing LF on testdata/**/*.json), but we also
// strip CR here so a misconfigured contributor checkout doesn't break the
// suite locally before they push.
func assertGolden(t *testing.T, got []byte, path string) {
t.Helper()
got = normalizeEnvelope(got)
if *update {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("mkdir testdata: %v", err)
}
if err := os.WriteFile(path, got, 0644); err != nil {
t.Fatalf("write golden: %v", err)
}
return
}
want, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read golden %s: %v (run with -update to create)", path, err)
}
want = stripCR(want)
got = stripCR(got)
if !bytes.Equal(want, got) {
t.Errorf("envelope mismatch for %s\nwant:\n%s\ngot:\n%s", path, want, got)
}
}
// stripCR removes CR bytes so CRLF golden files (from Windows autocrlf
// checkout) compare equal to LF runtime output.
func stripCR(b []byte) []byte {
return bytes.ReplaceAll(b, []byte{'\r'}, nil)
}
// normalizeEnvelope replaces unstable fields with placeholders for stable diff.
// Currently no-op (v0.0 commands don't set _meta.request_id, so output is stable).
// Hook for future fields.
func normalizeEnvelope(b []byte) []byte {
return b
}