cli: delete vm run's dead import path + duplicated git inspection
The CLI carried a full second copy of the workspace import
implementation that `vm run` never actually used:
- importVMRunRepoToGuest (no callers — the live flow calls the
daemon's PrepareVMWorkspace RPC instead)
- prepareVMRunRepoCopy, vmRunCheckoutCommit, vmRunCheckoutScript,
gitFileURL, runHostCommand (all reachable only from the dead
importVMRunRepoToGuest)
Plus a duplicated repo-inspection surface that shadowed the
daemon's:
- inspectVMRunRepo ran every git query the daemon re-ran during
workspace.prepare (HEAD, branch, identity, origin, overlay list)
- gitOutput / gitTrimmedOutput / gitResolvedConfigValue /
parseNullSeparatedOutput / listSubmodules / listOverlayPaths /
resolveVMRunSourcePath — all identical to the exported
workspace.* versions
- vmRunRepoSpec — same fields as workspace.RepoSpec
Replaced with a single minimal preflight:
func vmRunPreflightRepo(ctx, rawPath) (absPath, err error)
The preflight only checks what the user can fix locally before
banger creates a VM (path exists, sits in a non-bare git repo, no
submodules). The daemon's workspace.prepare RPC does the full
inspection — and returns RepoRoot + RepoName in the response, which
the CLI now threads into the tooling harness instead of computing
them a second time.
Signature changes:
runVMRun(ctx, ..., *vmRunRepo, ...) // was: *vmRunRepoSpec
startVMRunToolingHarness(ctx, client, repoRoot, repoName, progress)
// was: (ctx, client, spec, progress)
vmRunToolingHarnessScript(plan) // was: (spec, plan)
vmRunToolingHarnessLaunchScript(repoName) // was: (spec)
Tests: the CLI-side git-inspection tests are replaced by a single
TestVMRunPreflightRejectsSubmodules that exercises the preflight.
Everything else (tooling harness script, progress renderer, SSH args,
runVMRun flows) keeps working. The shallow-copy / checkout-script
tests are gone — that code now lives only in
internal/daemon/workspace and is tested there.
Also fixed a latent bug the refactor exposed: vm run's --from flag
defaults to "HEAD", which the daemon reads as "from without branch"
and rejects. CLI now scrubs fromRef when branchName is empty.
Live verified: `banger vm run --name X . -- cmd` boots, workspace
materialises at /root/repo with matching HEAD, exit code propagates.
This commit is contained in:
parent
ae14b9499d
commit
3a5f4cd40d
2 changed files with 112 additions and 498 deletions
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"banger/internal/api"
|
||||
"banger/internal/buildinfo"
|
||||
"banger/internal/daemon/workspace"
|
||||
"banger/internal/model"
|
||||
"banger/internal/system"
|
||||
"banger/internal/toolingplan"
|
||||
|
|
@ -1133,111 +1134,21 @@ func TestValidateSSHPrereqsFailsForMissingKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveVMRunSourcePathDefaultsToCWD(t *testing.T) {
|
||||
origCWD := cwdFunc
|
||||
t.Cleanup(func() {
|
||||
cwdFunc = origCWD
|
||||
})
|
||||
// CLI-side git inspection moved to internal/daemon/workspace; the
|
||||
// CLI now runs only a minimal preflight. Those tests live in the
|
||||
// workspace package. What we still guard here is the preflight
|
||||
// policy: reject submodules before the VM is created so the user
|
||||
// gets a fast error instead of an orphaned VM.
|
||||
|
||||
want := t.TempDir()
|
||||
cwdFunc = func() (string, error) {
|
||||
return want, nil
|
||||
}
|
||||
|
||||
got, err := resolveVMRunSourcePath("")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveVMRunSourcePath: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("resolveVMRunSourcePath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not installed")
|
||||
}
|
||||
|
||||
repoRoot := t.TempDir()
|
||||
globalConfigPath := filepath.Join(t.TempDir(), "global.gitconfig")
|
||||
t.Setenv("GIT_CONFIG_GLOBAL", globalConfigPath)
|
||||
testRunGit(t, repoRoot, "config", "--global", "user.email", "global@example.com")
|
||||
testRunGit(t, repoRoot, "config", "--global", "user.name", "Global User")
|
||||
testRunGit(t, repoRoot, "init")
|
||||
testRunGit(t, repoRoot, "remote", "add", "origin", "https://example.com/repo.git")
|
||||
testRunGit(t, repoRoot, "config", "user.email", "test@example.com")
|
||||
testRunGit(t, repoRoot, "config", "user.name", "Banger Test")
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(repoRoot, "dir"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(dir): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, ".gitignore"), []byte("ignored.txt\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(.gitignore): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "tracked.txt"), []byte("tracked\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(tracked.txt): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "dir", "keep.txt"), []byte("keep\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(keep.txt): %v", err)
|
||||
}
|
||||
testRunGit(t, repoRoot, "add", ".")
|
||||
testRunGit(t, repoRoot, "commit", "-m", "init")
|
||||
testRunGit(t, repoRoot, "checkout", "-b", "trunk")
|
||||
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "tracked.txt"), []byte("tracked local\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(tracked.txt local): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "untracked.txt"), []byte("untracked\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(untracked.txt): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, "ignored.txt"), []byte("ignored\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(ignored.txt): %v", err)
|
||||
}
|
||||
|
||||
spec, err := inspectVMRunRepo(context.Background(), filepath.Join(repoRoot, "dir"), "", "HEAD")
|
||||
if err != nil {
|
||||
t.Fatalf("inspectVMRunRepo: %v", err)
|
||||
}
|
||||
|
||||
if spec.RepoRoot != repoRoot {
|
||||
t.Fatalf("RepoRoot = %q, want %q", spec.RepoRoot, repoRoot)
|
||||
}
|
||||
if spec.RepoName != filepath.Base(repoRoot) {
|
||||
t.Fatalf("RepoName = %q, want %q", spec.RepoName, filepath.Base(repoRoot))
|
||||
}
|
||||
if spec.CurrentBranch != "trunk" {
|
||||
t.Fatalf("CurrentBranch = %q, want trunk", spec.CurrentBranch)
|
||||
}
|
||||
if spec.HeadCommit == "" {
|
||||
t.Fatal("HeadCommit should not be empty")
|
||||
}
|
||||
if spec.BaseCommit != spec.HeadCommit {
|
||||
t.Fatalf("BaseCommit = %q, want head %q", spec.BaseCommit, spec.HeadCommit)
|
||||
}
|
||||
if spec.OriginURL != "https://example.com/repo.git" {
|
||||
t.Fatalf("OriginURL = %q, want https://example.com/repo.git", spec.OriginURL)
|
||||
}
|
||||
if spec.GitUserName != "Banger Test" {
|
||||
t.Fatalf("GitUserName = %q, want Banger Test", spec.GitUserName)
|
||||
}
|
||||
if spec.GitUserEmail != "test@example.com" {
|
||||
t.Fatalf("GitUserEmail = %q, want test@example.com", spec.GitUserEmail)
|
||||
}
|
||||
wantOverlay := []string{".gitignore", "dir/keep.txt", "tracked.txt", "untracked.txt"}
|
||||
if !reflect.DeepEqual(spec.OverlayPaths, wantOverlay) {
|
||||
t.Fatalf("OverlayPaths = %v, want %v", spec.OverlayPaths, wantOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
|
||||
func TestVMRunPreflightRejectsSubmodules(t *testing.T) {
|
||||
repoRoot := t.TempDir()
|
||||
|
||||
origHostCommandOutput := hostCommandOutputFunc
|
||||
origHostCommandOutput := workspace.HostCommandOutputFunc
|
||||
t.Cleanup(func() {
|
||||
hostCommandOutputFunc = origHostCommandOutput
|
||||
workspace.HostCommandOutputFunc = origHostCommandOutput
|
||||
})
|
||||
|
||||
hostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
workspace.HostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
t.Helper()
|
||||
if name != "git" {
|
||||
t.Fatalf("command = %q, want git", name)
|
||||
|
|
@ -1255,9 +1166,9 @@ func TestInspectVMRunRepoRejectsSubmodules(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
_, err := inspectVMRunRepo(context.Background(), repoRoot, "", "HEAD")
|
||||
_, err := vmRunPreflightRepo(context.Background(), repoRoot)
|
||||
if err == nil || !strings.Contains(err.Error(), "submodules") {
|
||||
t.Fatalf("inspectVMRunRepo() error = %v, want submodule rejection", err)
|
||||
t.Fatalf("vmRunPreflightRepo() error = %v, want submodule rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1321,7 +1232,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
var workspaceParams api.VMWorkspacePrepareParams
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
workspaceParams = params
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
buildVMRunToolingPlanFunc = func(context.Context, string) toolingplan.Plan {
|
||||
return toolingplan.Plan{
|
||||
|
|
@ -1338,10 +1249,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
return api.VMHealthResult{Name: "devbox", Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{
|
||||
SourcePath: repoRoot, RepoRoot: repoRoot, RepoName: "repo",
|
||||
HeadCommit: "deadbeef", CurrentBranch: "main",
|
||||
}
|
||||
repo := vmRunRepo{sourcePath: repoRoot}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1350,7 +1258,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1425,7 +1333,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
return &testVMRunGuestClient{}, nil
|
||||
}
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
sshExecFunc = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
||||
return nil
|
||||
|
|
@ -1434,7 +1342,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
return api.VMHealthResult{Name: "devbox", Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"}
|
||||
repo := vmRunRepo{sourcePath: t.TempDir()}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1443,7 +1351,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1515,7 +1423,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
return fakeClient, nil
|
||||
}
|
||||
vmWorkspacePrepareFunc = func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo"}}, nil
|
||||
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
||||
}
|
||||
sshExecCalls := 0
|
||||
sshExecFunc = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
||||
|
|
@ -1526,7 +1434,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
return api.VMHealthResult{Healthy: false}, nil
|
||||
}
|
||||
|
||||
spec := vmRunRepoSpec{RepoRoot: t.TempDir(), RepoName: "repo", HeadCommit: "deadbeef"}
|
||||
repo := vmRunRepo{sourcePath: t.TempDir()}
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
|
|
@ -1535,7 +1443,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
&repo,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
|
|
@ -1892,7 +1800,7 @@ func TestSplitVMRunArgsPartitionsOnDash(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVMRunToolingHarnessScriptUsesMiseOnly(t *testing.T) {
|
||||
script := vmRunToolingHarnessScript(vmRunRepoSpec{RepoName: "repo"}, toolingplan.Plan{
|
||||
script := vmRunToolingHarnessScript(toolingplan.Plan{
|
||||
RepoManagedTools: []string{"node"},
|
||||
Steps: []toolingplan.InstallStep{{Tool: "go", Version: "1.25.0", Source: "go.mod"}},
|
||||
Skips: []toolingplan.SkipNote{{Target: "python", Reason: "no .python-version"}},
|
||||
|
|
@ -1915,68 +1823,10 @@ func TestVMRunToolingHarnessScriptUsesMiseOnly(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
func TestPrepareVMRunRepoCopyCreatesShallowMetadataCopy(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not installed")
|
||||
}
|
||||
|
||||
repoRoot := t.TempDir()
|
||||
testRunGit(t, repoRoot, "init")
|
||||
testRunGit(t, repoRoot, "remote", "add", "origin", "https://example.com/repo.git")
|
||||
for i := 0; i < 12; i++ {
|
||||
name := fmt.Sprintf("file-%02d.txt", i)
|
||||
if err := os.WriteFile(filepath.Join(repoRoot, name), []byte(fmt.Sprintf("commit-%02d\n", i)), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%s): %v", name, err)
|
||||
}
|
||||
testRunGit(t, repoRoot, "add", name)
|
||||
testRunGit(t, repoRoot, "commit", "-m", fmt.Sprintf("commit-%02d", i))
|
||||
}
|
||||
baseCommit := strings.TrimSpace(testRunGit(t, repoRoot, "rev-parse", "HEAD~5"))
|
||||
|
||||
repoCopyDir, cleanup, err := prepareVMRunRepoCopy(context.Background(), vmRunRepoSpec{
|
||||
RepoRoot: repoRoot,
|
||||
RepoName: "repo",
|
||||
BranchName: "feature",
|
||||
BaseCommit: baseCommit,
|
||||
HeadCommit: strings.TrimSpace(testRunGit(t, repoRoot, "rev-parse", "HEAD")),
|
||||
OriginURL: "https://example.com/repo.git",
|
||||
OverlayPaths: []string{"file-11.txt"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("prepareVMRunRepoCopy: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
entries, err := os.ReadDir(repoCopyDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(repoCopyDir): %v", err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0].Name() != ".git" {
|
||||
t.Fatalf("repo copy entries = %v, want only .git", entries)
|
||||
}
|
||||
if got := strings.TrimSpace(testRunGit(t, repoCopyDir, "rev-parse", "--is-shallow-repository")); got != "true" {
|
||||
t.Fatalf("is-shallow-repository = %q, want true", got)
|
||||
}
|
||||
if got := strings.TrimSpace(testRunGit(t, repoCopyDir, "config", "--get", "remote.origin.url")); got != "https://example.com/repo.git" {
|
||||
t.Fatalf("remote.origin.url = %q, want https://example.com/repo.git", got)
|
||||
}
|
||||
if _, err := exec.Command("git", "-C", repoCopyDir, "cat-file", "-e", baseCommit+"^{commit}").CombinedOutput(); err != nil {
|
||||
t.Fatalf("cat-file -e %s^{commit}: %v", baseCommit, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMRunCheckoutScriptSkipsRepoGitIdentityWhenIncomplete(t *testing.T) {
|
||||
script := vmRunCheckoutScript(vmRunRepoSpec{
|
||||
RepoName: "repo",
|
||||
HeadCommit: "deadbeef",
|
||||
CurrentBranch: "main",
|
||||
GitUserName: "Repo User",
|
||||
})
|
||||
|
||||
if strings.Contains(script, `git -C "$DIR" config user.name`) || strings.Contains(script, `git -C "$DIR" config user.email`) {
|
||||
t.Fatalf("script = %q, want no repo-local git identity commands", script)
|
||||
}
|
||||
}
|
||||
// The shallow-repo-copy + checkout-script paths used to live in the
|
||||
// CLI. They now live in internal/daemon/workspace and are exercised
|
||||
// by that package's tests; no need to duplicate here.
|
||||
|
||||
func TestVMRunGuestDirIsFixed(t *testing.T) {
|
||||
if got := vmRunGuestDir(); got != "/root/repo" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue