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
406 lines
15 KiB
Go
406 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// capturedEdit records the (tool, args) the edit verb lowered to.
|
|
type capturedEdit struct {
|
|
repo string
|
|
tool string
|
|
args map[string]any
|
|
}
|
|
|
|
// runEdit drives the real command tree (rootCmd → edit → subcommand) with the
|
|
// given subcommand argv, stubbing the daemon seam so the call never leaves the
|
|
// process. It returns the captured call (or nil if none was made), the combined
|
|
// out/err buffer, and any error from RunE. A canned JSON result is returned from
|
|
// the stub so the renderer path runs end-to-end.
|
|
//
|
|
// argv is the edit-subcommand argv (e.g. "context", "internal/foo.go"); the
|
|
// helper prepends "edit" so cobra routes through the real edit parent — calling
|
|
// editCmd.Execute() directly would re-root at rootCmd and collide with the
|
|
// unrelated top-level `context` command.
|
|
func runEdit(t *testing.T, stdin io.Reader, argv ...string) (*capturedEdit, *bytes.Buffer, error) {
|
|
t.Helper()
|
|
resetEditFlags()
|
|
|
|
orig := editDaemonTool
|
|
t.Cleanup(func() { editDaemonTool = orig })
|
|
|
|
var cap *capturedEdit
|
|
editDaemonTool = func(repo, tool string, args map[string]any) (json.RawMessage, error) {
|
|
cap = &capturedEdit{repo: repo, tool: tool, args: args}
|
|
return json.RawMessage(`{"status":"ok"}`), nil
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
rootCmd.SetOut(buf)
|
|
rootCmd.SetErr(buf)
|
|
if stdin != nil {
|
|
rootCmd.SetIn(stdin)
|
|
} else {
|
|
rootCmd.SetIn(strings.NewReader(""))
|
|
}
|
|
rootCmd.SetArgs(append([]string{"edit"}, argv...))
|
|
err := rootCmd.Execute()
|
|
return cap, buf, err
|
|
}
|
|
|
|
// resetEditFlags restores every edit-group flag global to its zero/default so
|
|
// tests don't leak state into each other (cobra persists parsed values on the
|
|
// shared command vars). It also clears each flag's cobra-internal Changed bit,
|
|
// which Execute() leaves set across runs — Changed() is load-bearing for the
|
|
// --old/--new "required" check, so a stale bit would mask a missing flag.
|
|
func resetEditFlags() {
|
|
resetCobraFlags(editCmd)
|
|
|
|
editIndex = "."
|
|
editFormat = "text"
|
|
|
|
editContextDetail, editContextCompress = "", false
|
|
|
|
editChanges = nil
|
|
editVerifyChangesInline, editVerifyChangesFile, editVerifyCompact = "", "", false
|
|
|
|
editPlanIDs, editPlanDepth = "", 3
|
|
|
|
editPreviewWEInline, editPreviewWEFile = "", ""
|
|
editPreviewNoDiag, editPreviewInheritOver = false, false
|
|
|
|
editSimulateStepsInline, editSimulateStepsFile = "", ""
|
|
editSimulateKeep, editSimulateNoStop, editSimulateInheritOver = false, false, false
|
|
|
|
editBatchInline, editBatchFile, editBatchDryRun, editBatchCompact = "", "", false, false
|
|
|
|
editApplyOld, editApplyNew = "", ""
|
|
editApplyReplaceAll, editApplyDryRun, editApplyAllowParse, editApplyExpected = false, false, false, 0
|
|
|
|
editSymbolOld, editSymbolNew, editSymbolDryRun = "", "", false
|
|
|
|
editRenameTo = ""
|
|
|
|
editGuardsIDs, editGuardsCompact = "", false
|
|
|
|
editTestsIDs, editTestsDepth = "", 3
|
|
|
|
editContractSource, editContractLens = "", ""
|
|
editContractRiskGate, editContractAck = false, false
|
|
editContractBase, editContractWEInline, editContractWEFile = "", "", ""
|
|
editContractSymbols, editContractRanges, editContractRangeFile = "", "", ""
|
|
editContractPath, editContractStartLine, editContractEndLine = "", 0, 0
|
|
|
|
editSafeDeleteApply, editSafeDeleteForce = false, false
|
|
editSafeDeleteCascade, editSafeDeleteIntoTests, editSafeDeletePropagate = "", false, false
|
|
}
|
|
|
|
// resetCobraFlags clears the Changed bit on every flag of cmd and its
|
|
// subcommands, recursively. cobra sets Changed in place during Execute() and
|
|
// never clears it, so without this a flag set in one test stays "Changed" in
|
|
// the next — which would mask the --old/--new "required" Changed() check. The
|
|
// flag VALUES are restored by the explicit Go-variable resets above (the flag
|
|
// vars are package globals bound via *Var), so this only touches Changed; in
|
|
// particular it avoids re-Set-ing a StringArray default, which would append.
|
|
func resetCobraFlags(cmd *cobra.Command) {
|
|
clear := func(f *pflag.Flag) { f.Changed = false }
|
|
cmd.Flags().VisitAll(clear)
|
|
cmd.PersistentFlags().VisitAll(clear)
|
|
for _, c := range cmd.Commands() {
|
|
resetCobraFlags(c)
|
|
}
|
|
}
|
|
|
|
// --- per-subcommand lowering assertions ------------------------------------
|
|
|
|
func TestEditContext_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "context", "internal/foo.go", "--detail", "full", "--compress")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cap)
|
|
require.Equal(t, "get_editing_context", cap.tool)
|
|
require.Equal(t, "internal/foo.go", cap.args["path"])
|
|
require.Equal(t, "full", cap.args["detail"])
|
|
require.Equal(t, true, cap.args["compress_bodies"])
|
|
}
|
|
|
|
func TestEditContext_OmitsUnsetFlags(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "context", "internal/foo.go")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "get_editing_context", cap.tool)
|
|
require.NotContains(t, cap.args, "detail")
|
|
require.NotContains(t, cap.args, "compress_bodies")
|
|
}
|
|
|
|
func TestEditVerify_ChangeSugarBuildsArray(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "verify",
|
|
"--change", "pkg/a.go::A=sig1",
|
|
"--change", "pkg/b.go::B=func(x int) error")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "verify_change", cap.tool)
|
|
|
|
changesJSON, ok := cap.args["changes"].(string)
|
|
require.True(t, ok, "changes must be a JSON string")
|
|
var got []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(changesJSON), &got))
|
|
require.Equal(t, []map[string]string{
|
|
{"symbol_id": "pkg/a.go::A", "new_signature": "sig1"},
|
|
{"symbol_id": "pkg/b.go::B", "new_signature": "func(x int) error"},
|
|
}, got)
|
|
}
|
|
|
|
func TestEditVerify_SplitsOnFirstEquals(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "verify", "--change", "id=func() (a=b, error)")
|
|
require.NoError(t, err)
|
|
changesJSON := cap.args["changes"].(string)
|
|
var got []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(changesJSON), &got))
|
|
require.Equal(t, "id", got[0]["symbol_id"])
|
|
require.Equal(t, "func() (a=b, error)", got[0]["new_signature"])
|
|
}
|
|
|
|
func TestEditVerify_ChangesInlineJSON(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "verify",
|
|
"--changes", `[{"symbol_id":"x","new_signature":"y"}]`, "--compact")
|
|
require.NoError(t, err)
|
|
require.Equal(t, `[{"symbol_id":"x","new_signature":"y"}]`, cap.args["changes"])
|
|
require.Equal(t, true, cap.args["compact"])
|
|
}
|
|
|
|
func TestEditVerify_ChangesStdin(t *testing.T) {
|
|
cap, _, err := runEdit(t, strings.NewReader(`[{"symbol_id":"s","new_signature":"sig"}]`),
|
|
"verify", "--changes", "-")
|
|
require.NoError(t, err)
|
|
require.Equal(t, `[{"symbol_id":"s","new_signature":"sig"}]`, cap.args["changes"])
|
|
}
|
|
|
|
func TestEditVerify_RequiresChanges(t *testing.T) {
|
|
_, _, err := runEdit(t, nil, "verify")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEditPlan_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "plan", "--ids", "a,b", "--depth", "5")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "get_edit_plan", cap.tool)
|
|
require.Equal(t, "a,b", cap.args["ids"])
|
|
require.Equal(t, 5, cap.args["depth"])
|
|
}
|
|
|
|
func TestEditPlan_DefaultDepth(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "plan", "--ids", "a")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, cap.args["depth"])
|
|
}
|
|
|
|
func TestEditPlan_RequiresIDs(t *testing.T) {
|
|
_, _, err := runEdit(t, nil, "plan")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEditPreview_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "preview",
|
|
"--workspace-edit", `{"changes":{}}`, "--no-diagnostics", "--inherit-overlay")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "preview_edit", cap.tool)
|
|
require.Equal(t, `{"changes":{}}`, cap.args["workspace_edit"])
|
|
require.Equal(t, false, cap.args["diagnostics"])
|
|
require.Equal(t, true, cap.args["inherit_overlay"])
|
|
}
|
|
|
|
func TestEditPreview_DiagnosticsDefaultOmitted(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "preview", "--workspace-edit", `{"changes":{}}`)
|
|
require.NoError(t, err)
|
|
// diagnostics defaults to true on the daemon — the CLI omits it unless
|
|
// --no-diagnostics turns it off.
|
|
require.NotContains(t, cap.args, "diagnostics")
|
|
}
|
|
|
|
func TestEditSimulate_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "simulate",
|
|
"--steps", `[{"changes":{}}]`, "--keep", "--no-stop-on-error")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "simulate_chain", cap.tool)
|
|
require.Equal(t, `[{"changes":{}}]`, cap.args["steps"])
|
|
require.Equal(t, true, cap.args["keep"])
|
|
require.Equal(t, false, cap.args["stop_on_error"])
|
|
}
|
|
|
|
func TestEditBatch_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "batch",
|
|
"--edits", `[{"id":"x","old_source":"a","new_source":"b"}]`, "--dry-run", "--compact")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "batch_edit", cap.tool)
|
|
require.Equal(t, `[{"id":"x","old_source":"a","new_source":"b"}]`, cap.args["edits"])
|
|
require.Equal(t, true, cap.args["dry_run"])
|
|
require.Equal(t, true, cap.args["compact"])
|
|
}
|
|
|
|
func TestEditBatch_EditsFromFile(t *testing.T) {
|
|
path := writeTempJSON(t, `[{"id":"x","old_source":"a","new_source":"b"}]`)
|
|
cap, _, err := runEdit(t, nil, "batch", "--edits-file", path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `[{"id":"x","old_source":"a","new_source":"b"}]`, cap.args["edits"])
|
|
}
|
|
|
|
func TestEditApply_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "apply", "internal/foo.go",
|
|
"--old", "before", "--new", "after",
|
|
"--replace-all", "--dry-run", "--allow-parse-errors", "--expected", "3")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "edit_file", cap.tool)
|
|
require.Equal(t, "internal/foo.go", cap.args["path"])
|
|
require.Equal(t, "before", cap.args["old_string"])
|
|
require.Equal(t, "after", cap.args["new_string"])
|
|
require.Equal(t, true, cap.args["replace_all"])
|
|
require.Equal(t, true, cap.args["dry_run"])
|
|
require.Equal(t, true, cap.args["allow_parse_errors"])
|
|
require.Equal(t, 3, cap.args["expected_occurrences"])
|
|
}
|
|
|
|
func TestEditApply_RequiresOldAndNew(t *testing.T) {
|
|
_, _, err := runEdit(t, nil, "apply", "internal/foo.go", "--old", "x")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEditApply_EmptyNewIsAllowed(t *testing.T) {
|
|
// An empty --new (deletion) is a legitimate edit — the Changed() check,
|
|
// not a non-empty value, gates it.
|
|
cap, _, err := runEdit(t, nil, "apply", "internal/foo.go", "--old", "x", "--new", "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", cap.args["new_string"])
|
|
}
|
|
|
|
func TestEditSymbol_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "symbol", "pkg/a.go::Foo",
|
|
"--old", "oldbody", "--new", "newbody", "--dry-run")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "edit_symbol", cap.tool)
|
|
require.Equal(t, "pkg/a.go::Foo", cap.args["id"])
|
|
require.Equal(t, "oldbody", cap.args["old_source"])
|
|
require.Equal(t, "newbody", cap.args["new_source"])
|
|
require.Equal(t, true, cap.args["dry_run"])
|
|
}
|
|
|
|
func TestEditRename_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "rename", "pkg/a.go::Foo", "--to", "Bar")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "rename_symbol", cap.tool)
|
|
require.Equal(t, "pkg/a.go::Foo", cap.args["id"])
|
|
require.Equal(t, "Bar", cap.args["new_name"])
|
|
}
|
|
|
|
func TestEditRename_RequiresTo(t *testing.T) {
|
|
_, _, err := runEdit(t, nil, "rename", "pkg/a.go::Foo")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEditGuards_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "guards", "--ids", "a,b,c", "--compact")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "check_guards", cap.tool)
|
|
require.Equal(t, "a,b,c", cap.args["ids"])
|
|
require.Equal(t, true, cap.args["compact"])
|
|
}
|
|
|
|
func TestEditTests_Lowers(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "tests", "--ids", "a,b", "--depth", "4")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "get_test_targets", cap.tool)
|
|
require.Equal(t, "a,b", cap.args["ids"])
|
|
require.Equal(t, 4, cap.args["depth"])
|
|
}
|
|
|
|
func TestEditContract_SymbolsSource(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "contract",
|
|
"--source", "symbols", "--symbols", "pkg/a.go::A,pkg/b.go::B",
|
|
"--lens", "api", "--risk-gate")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "change_contract", cap.tool)
|
|
require.Equal(t, "symbols", cap.args["source"])
|
|
require.Equal(t, "pkg/a.go::A,pkg/b.go::B", cap.args["symbols"])
|
|
require.Equal(t, "api", cap.args["lens"])
|
|
require.Equal(t, true, cap.args["risk_gate"])
|
|
}
|
|
|
|
func TestEditContract_RangesSingleFile(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "contract",
|
|
"--source", "ranges", "--path", "internal/foo.go",
|
|
"--start-line", "10", "--end-line", "20")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "internal/foo.go", cap.args["path"])
|
|
require.Equal(t, 10, cap.args["start_line"])
|
|
require.Equal(t, 20, cap.args["end_line"])
|
|
}
|
|
|
|
func TestEditContract_EditSource(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "contract",
|
|
"--source", "edit", "--workspace-edit", `{"changes":{}}`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, `{"changes":{}}`, cap.args["workspace_edit"])
|
|
}
|
|
|
|
func TestEditContract_NoRequiredFlags(t *testing.T) {
|
|
// change_contract has no required flags — an argument-free call (source=auto)
|
|
// must lower cleanly.
|
|
cap, _, err := runEdit(t, nil, "contract")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "change_contract", cap.tool)
|
|
require.NotContains(t, cap.args, "source")
|
|
}
|
|
|
|
func TestEditSafeDelete_DryRunByDefault(t *testing.T) {
|
|
cap, buf, err := runEdit(t, nil, "safe-delete", "pkg/a.go::Foo")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "safe_delete_symbol", cap.tool)
|
|
require.Equal(t, "pkg/a.go::Foo", cap.args["id"])
|
|
require.Equal(t, true, cap.args["dry_run"], "default must be a dry run")
|
|
require.Contains(t, buf.String(), "dry run")
|
|
}
|
|
|
|
func TestEditSafeDelete_ApplyFlipsDryRun(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "safe-delete", "pkg/a.go::Foo", "--apply")
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, cap.args["dry_run"], "--apply must commit (dry_run=false)")
|
|
}
|
|
|
|
func TestEditSafeDelete_CascadeAndPropagate(t *testing.T) {
|
|
cap, _, err := runEdit(t, nil, "safe-delete", "pkg/a.go::Foo",
|
|
"--cascade", "preview", "--cascade-into-tests", "--propagate", "--force")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "preview", cap.args["cascade"])
|
|
require.Equal(t, true, cap.args["cascade_into_tests"])
|
|
require.Equal(t, true, cap.args["propagate"])
|
|
require.Equal(t, true, cap.args["force"])
|
|
}
|
|
|
|
// TestEdit_FormatJSONRenders asserts --format json pretty-prints the daemon
|
|
// result through emitDaemonJSON.
|
|
func TestEdit_FormatJSONRenders(t *testing.T) {
|
|
_, buf, err := runEdit(t, nil, "guards", "--ids", "a", "--format", "json")
|
|
require.NoError(t, err)
|
|
require.Contains(t, buf.String(), `"status": "ok"`)
|
|
}
|
|
|
|
// TestEditDaemonRequired asserts that, with no daemon and no stub, a subcommand
|
|
// returns the actionable daemon-required error (mirrors
|
|
// query_daemon_required_test.go / call_test.go).
|
|
func TestEditDaemonRequired(t *testing.T) {
|
|
resetEditFlags()
|
|
// No stub: the real editDaemonTool runs against a temp dir no daemon tracks.
|
|
buf := &bytes.Buffer{}
|
|
rootCmd.SetOut(buf)
|
|
rootCmd.SetErr(buf)
|
|
rootCmd.SetIn(strings.NewReader(""))
|
|
rootCmd.SetArgs([]string{"edit", "plan", "--ids", "a", "--index", t.TempDir()})
|
|
err := rootCmd.Execute()
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "gortex track")
|
|
}
|