278 lines
8.3 KiB
Go
278 lines
8.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/model"
|
|
"banger/internal/toolingplan"
|
|
)
|
|
|
|
func TestVMRunRejectsDetachWithRm(t *testing.T) {
|
|
cmd := NewBangerCommand()
|
|
cmd.SetArgs([]string{"vm", "run", "-d", "--rm"})
|
|
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "cannot combine --detach with --rm") {
|
|
t.Fatalf("Execute() error = %v, want --detach + --rm rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestVMRunRejectsDetachWithCommand(t *testing.T) {
|
|
cmd := NewBangerCommand()
|
|
cmd.SetArgs([]string{"vm", "run", "-d", "--", "whoami"})
|
|
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "cannot combine --detach with a guest command") {
|
|
t.Fatalf("Execute() error = %v, want --detach + command rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestRepoHasMiseFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
got, err := repoHasMiseFiles(dir)
|
|
if err != nil {
|
|
t.Fatalf("repoHasMiseFiles(empty): %v", err)
|
|
}
|
|
if got {
|
|
t.Fatalf("repoHasMiseFiles(empty) = true, want false")
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(dir, ".mise.toml"), []byte(""), 0o600); err != nil {
|
|
t.Fatalf("write .mise.toml: %v", err)
|
|
}
|
|
got, err = repoHasMiseFiles(dir)
|
|
if err != nil {
|
|
t.Fatalf("repoHasMiseFiles(.mise.toml): %v", err)
|
|
}
|
|
if !got {
|
|
t.Fatalf("repoHasMiseFiles(.mise.toml) = false, want true")
|
|
}
|
|
|
|
dir2 := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir2, ".tool-versions"), []byte(""), 0o600); err != nil {
|
|
t.Fatalf("write .tool-versions: %v", err)
|
|
}
|
|
got, err = repoHasMiseFiles(dir2)
|
|
if err != nil {
|
|
t.Fatalf("repoHasMiseFiles(.tool-versions): %v", err)
|
|
}
|
|
if !got {
|
|
t.Fatalf("repoHasMiseFiles(.tool-versions) = false, want true")
|
|
}
|
|
}
|
|
|
|
// runVMRunDepsRunningVM returns a deps wired so runVMRun reaches a
|
|
// point where it would create a VM and proceed — used by precondition
|
|
// tests that should refuse before any of these fakes get called.
|
|
func runVMRunDepsRunningVM(t *testing.T) (*deps, *model.VMRecord) {
|
|
t.Helper()
|
|
d := defaultDeps()
|
|
vm := &model.VMRecord{
|
|
ID: "vm-id",
|
|
Name: "devbox",
|
|
Runtime: model.VMRuntime{
|
|
State: model.VMStateRunning,
|
|
GuestIP: "172.16.0.2",
|
|
DNSName: "devbox.vm",
|
|
},
|
|
}
|
|
d.vmCreateBegin = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
|
|
return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Done: true, Success: true, VM: vm}}, nil
|
|
}
|
|
d.guestWaitForSSH = func(context.Context, string, string, time.Duration) error { return nil }
|
|
d.vmWorkspacePrepare = func(context.Context, string, api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
|
return api.VMWorkspacePrepareResult{Workspace: model.WorkspacePrepareResult{VMID: vm.ID, GuestPath: "/root/repo", RepoName: "repo", RepoRoot: "/tmp/repo"}}, nil
|
|
}
|
|
d.buildVMRunToolingPlan = func(context.Context, string) toolingplan.Plan {
|
|
return toolingplan.Plan{}
|
|
}
|
|
d.vmHealth = func(context.Context, string, string) (api.VMHealthResult, error) {
|
|
return api.VMHealthResult{Healthy: true}, nil
|
|
}
|
|
d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error { return nil }
|
|
return d, vm
|
|
}
|
|
|
|
func TestRunVMRunRefusesBootstrapWithoutNAT(t *testing.T) {
|
|
repoRoot := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(repoRoot, ".mise.toml"), []byte(""), 0o600); err != nil {
|
|
t.Fatalf("write .mise.toml: %v", err)
|
|
}
|
|
|
|
d := defaultDeps()
|
|
d.vmCreateBegin = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
|
|
t.Fatal("vmCreateBegin should not be called when NAT precondition refuses")
|
|
return api.VMCreateBeginResult{}, nil
|
|
}
|
|
|
|
repo := vmRunRepo{sourcePath: repoRoot}
|
|
var stdout, stderr bytes.Buffer
|
|
err := d.runVMRun(
|
|
context.Background(),
|
|
"/tmp/bangerd.sock",
|
|
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
|
strings.NewReader(""),
|
|
&stdout, &stderr,
|
|
api.VMCreateParams{Name: "devbox", NATEnabled: false},
|
|
&repo,
|
|
nil,
|
|
false, false, false, false,
|
|
)
|
|
if err == nil || !strings.Contains(err.Error(), "tooling bootstrap requires --nat") {
|
|
t.Fatalf("runVMRun = %v, want NAT precondition refusal", err)
|
|
}
|
|
}
|
|
|
|
func TestRunVMRunBootstrapPreconditionRespectsNoBootstrap(t *testing.T) {
|
|
repoRoot := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(repoRoot, ".mise.toml"), []byte(""), 0o600); err != nil {
|
|
t.Fatalf("write .mise.toml: %v", err)
|
|
}
|
|
|
|
d, _ := runVMRunDepsRunningVM(t)
|
|
dialed := false
|
|
d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
|
|
dialed = true
|
|
return &testVMRunGuestClient{}, nil
|
|
}
|
|
|
|
repo := vmRunRepo{sourcePath: repoRoot}
|
|
var stdout, stderr bytes.Buffer
|
|
err := d.runVMRun(
|
|
context.Background(),
|
|
"/tmp/bangerd.sock",
|
|
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
|
strings.NewReader(""),
|
|
&stdout, &stderr,
|
|
api.VMCreateParams{Name: "devbox", NATEnabled: false},
|
|
&repo,
|
|
nil,
|
|
false, false, true, false, // skipBootstrap = true
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("runVMRun: %v", err)
|
|
}
|
|
if dialed {
|
|
t.Fatal("guestDial should not be called when --no-bootstrap is set")
|
|
}
|
|
}
|
|
|
|
func TestRunVMRunBootstrapPreconditionPassesWithoutMiseFiles(t *testing.T) {
|
|
repoRoot := t.TempDir() // empty repo, no mise files
|
|
|
|
d, _ := runVMRunDepsRunningVM(t)
|
|
dialed := false
|
|
d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
|
|
dialed = true
|
|
return &testVMRunGuestClient{}, nil
|
|
}
|
|
|
|
repo := vmRunRepo{sourcePath: repoRoot}
|
|
var stdout, stderr bytes.Buffer
|
|
err := d.runVMRun(
|
|
context.Background(),
|
|
"/tmp/bangerd.sock",
|
|
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
|
strings.NewReader(""),
|
|
&stdout, &stderr,
|
|
api.VMCreateParams{Name: "devbox", NATEnabled: false},
|
|
&repo,
|
|
nil,
|
|
false, false, false, false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("runVMRun: %v", err)
|
|
}
|
|
// Bootstrap dispatch happens (no mise file gating) but dial still
|
|
// gets called because the harness pipeline runs.
|
|
if !dialed {
|
|
t.Fatal("guestDial should be called for bootstrap dispatch")
|
|
}
|
|
}
|
|
|
|
func TestRunVMRunDetachSkipsSshAttach(t *testing.T) {
|
|
d, _ := runVMRunDepsRunningVM(t)
|
|
d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
|
|
return &testVMRunGuestClient{}, nil
|
|
}
|
|
sshExecCalls := 0
|
|
d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
|
sshExecCalls++
|
|
return nil
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
err := d.runVMRun(
|
|
context.Background(),
|
|
"/tmp/bangerd.sock",
|
|
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
|
strings.NewReader(""),
|
|
&stdout, &stderr,
|
|
api.VMCreateParams{Name: "devbox"},
|
|
nil, // bare mode
|
|
nil, // no command
|
|
false, true, false, false, // detach = true
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("runVMRun: %v", err)
|
|
}
|
|
if sshExecCalls != 0 {
|
|
t.Fatalf("sshExec called %d times, want 0 in detach mode", sshExecCalls)
|
|
}
|
|
if !strings.Contains(stderr.String(), "reconnect with: banger vm ssh devbox") {
|
|
t.Fatalf("stderr = %q, want reconnect hint", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRunVMRunDetachUsesSyncBootstrapPath(t *testing.T) {
|
|
repoRoot := t.TempDir()
|
|
|
|
d, _ := runVMRunDepsRunningVM(t)
|
|
fakeClient := &testVMRunGuestClient{}
|
|
d.guestDial = func(context.Context, string, string) (vmRunGuestClient, error) {
|
|
return fakeClient, nil
|
|
}
|
|
sshExecCalls := 0
|
|
d.sshExec = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error {
|
|
sshExecCalls++
|
|
return nil
|
|
}
|
|
|
|
repo := vmRunRepo{sourcePath: repoRoot}
|
|
var stdout, stderr bytes.Buffer
|
|
err := d.runVMRun(
|
|
context.Background(),
|
|
"/tmp/bangerd.sock",
|
|
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
|
strings.NewReader(""),
|
|
&stdout, &stderr,
|
|
api.VMCreateParams{Name: "devbox", NATEnabled: true},
|
|
&repo,
|
|
nil,
|
|
false, true, false, false, // detach = true
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("runVMRun: %v", err)
|
|
}
|
|
if sshExecCalls != 0 {
|
|
t.Fatalf("sshExec called %d times, want 0 in detach mode", sshExecCalls)
|
|
}
|
|
if len(fakeClient.uploads) != 1 {
|
|
t.Fatalf("uploads = %d, want 1 (harness upload)", len(fakeClient.uploads))
|
|
}
|
|
// Sync mode should invoke the tee'd wrapper, not the nohup launcher.
|
|
if strings.Contains(fakeClient.launchScript, "nohup") {
|
|
t.Fatalf("detach mode should not use nohup launcher; got: %q", fakeClient.launchScript)
|
|
}
|
|
if !strings.Contains(fakeClient.launchScript, "tee") {
|
|
t.Fatalf("detach mode should tee output to log; got: %q", fakeClient.launchScript)
|
|
}
|
|
}
|