vm defaults: host-aware sizing + spec line on spawn + doctor check

Replaces the static model.Default* constants that drove --vcpu / --memory
/ --disk-size with a three-layer resolver:

  1. [vm_defaults] in ~/.config/banger/config.toml (if set)
  2. host-derived heuristics (cpus/4 capped at 4; ram/8 capped at 8 GiB)
  3. baked-in constants (floor)

Visibility:

- Every `vm run` / `vm create` prints a `spec:` line before progress
  begins: `spec: 4 vcpu · 8192 MiB · 8G disk`. Matches the VM that
  actually gets created because the CLI is now the single source of
  truth — it resolves, populates the flag defaults, and forwards the
  explicit values to the daemon.
- `banger doctor` adds a "vm defaults" check showing per-field
  provenance (config|auto|builtin) and the config file path for
  overrides.
- `--help` shows the resolved defaults (e.g. `--vcpu int (default 4)`
  on an 8-core host).

No `banger config init` command, no first-run side effects, no writes
to the user's filesystem behind their back. Users who want explicit
control set the keys; everyone else gets sensible numbers that track
their hardware.
This commit is contained in:
Thales Maciel 2026-04-19 13:06:51 -03:00
parent 78ff482bfa
commit 21b74639d8
No known key found for this signature in database
GPG key ID: 33112E6833C34679
10 changed files with 594 additions and 56 deletions

View file

@ -268,7 +268,11 @@ func TestVMRunFlagsExist(t *testing.T) {
}
}
func TestVMCreateFlagsShowStaticDefaults(t *testing.T) {
func TestVMCreateFlagsShowResolvedDefaults(t *testing.T) {
// Defaults are resolved at command-build time from config + host
// heuristics. Guarantee only that the values are sensible-positive
// and match the resolver's output — the exact numbers depend on
// the host the tests run on.
root := NewBangerCommand()
vm, _, err := root.Find([]string{"vm"})
if err != nil {
@ -279,17 +283,23 @@ func TestVMCreateFlagsShowStaticDefaults(t *testing.T) {
t.Fatalf("find create: %v", err)
}
if got := create.Flags().Lookup("vcpu").DefValue; got != fmt.Sprintf("%d", model.DefaultVCPUCount) {
t.Fatalf("vcpu default = %q, want %d", got, model.DefaultVCPUCount)
for _, flagName := range []string{"vcpu", "memory"} {
flag := create.Flags().Lookup(flagName)
if flag == nil {
t.Fatalf("flag %q missing", flagName)
}
if flag.DefValue == "" || flag.DefValue == "0" {
t.Errorf("flag %q default = %q, want a positive integer", flagName, flag.DefValue)
}
}
if got := create.Flags().Lookup("memory").DefValue; got != fmt.Sprintf("%d", model.DefaultMemoryMiB) {
t.Fatalf("memory default = %q, want %d", got, model.DefaultMemoryMiB)
}
if got := create.Flags().Lookup("system-overlay-size").DefValue; got != model.FormatSizeBytes(model.DefaultSystemOverlaySize) {
t.Fatalf("system-overlay-size default = %q, want %q", got, model.FormatSizeBytes(model.DefaultSystemOverlaySize))
}
if got := create.Flags().Lookup("disk-size").DefValue; got != model.FormatSizeBytes(model.DefaultWorkDiskSize) {
t.Fatalf("disk-size default = %q, want %q", got, model.FormatSizeBytes(model.DefaultWorkDiskSize))
for _, flagName := range []string{"system-overlay-size", "disk-size"} {
flag := create.Flags().Lookup(flagName)
if flag == nil {
t.Fatalf("flag %q missing", flagName)
}
if !strings.ContainsAny(flag.DefValue, "GMK") {
t.Errorf("flag %q default = %q, want a formatted size like '8G'", flagName, flag.DefValue)
}
}
}
@ -385,7 +395,11 @@ func TestVMSetParamsFromFlags(t *testing.T) {
}
}
func TestVMCreateParamsFromFlagsOmitsStaticDefaultsWhenFlagsAreUnchanged(t *testing.T) {
func TestVMCreateParamsFromFlagsAlwaysPopulatesResolvedValues(t *testing.T) {
// Post-resolver behavior: the CLI is the single source of truth for
// effective defaults. Whether or not the user changed a flag, the
// daemon receives the explicit value so the spec printed to the
// user matches the VM that gets created.
cmd := NewBangerCommand()
vm, _, err := cmd.Find([]string{"vm"})
if err != nil {
@ -400,18 +414,46 @@ func TestVMCreateParamsFromFlagsOmitsStaticDefaultsWhenFlagsAreUnchanged(t *test
create,
"devbox",
"default",
model.DefaultVCPUCount,
model.DefaultMemoryMiB,
model.FormatSizeBytes(model.DefaultSystemOverlaySize),
model.FormatSizeBytes(model.DefaultWorkDiskSize),
3,
4096,
"10G",
"20G",
false,
false,
)
if err != nil {
t.Fatalf("vmCreateParamsFromFlags: %v", err)
}
if params.VCPUCount != nil || params.MemoryMiB != nil || params.SystemOverlaySize != "" || params.WorkDiskSize != "" {
t.Fatalf("expected unchanged defaults to stay omitted: %+v", params)
if params.VCPUCount == nil || *params.VCPUCount != 3 {
t.Errorf("VCPUCount = %v, want 3", params.VCPUCount)
}
if params.MemoryMiB == nil || *params.MemoryMiB != 4096 {
t.Errorf("MemoryMiB = %v, want 4096", params.MemoryMiB)
}
if params.SystemOverlaySize != "10G" {
t.Errorf("SystemOverlaySize = %q, want 10G", params.SystemOverlaySize)
}
if params.WorkDiskSize != "20G" {
t.Errorf("WorkDiskSize = %q, want 20G", params.WorkDiskSize)
}
}
func TestVMCreateParamsFromFlagsRejectsNonPositive(t *testing.T) {
cmd := NewBangerCommand()
vm, _, err := cmd.Find([]string{"vm"})
if err != nil {
t.Fatalf("find vm: %v", err)
}
create, _, err := vm.Find([]string{"create"})
if err != nil {
t.Fatalf("find create: %v", err)
}
if _, err := vmCreateParamsFromFlags(create, "x", "", 0, 1024, "8G", "8G", false, false); err == nil {
t.Error("expected error for vcpu=0")
}
if _, err := vmCreateParamsFromFlags(create, "x", "", 2, 0, "8G", "8G", false, false); err == nil {
t.Error("expected error for memory=0")
}
}