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

@ -0,0 +1,107 @@
package model
import (
"strings"
"testing"
)
func TestResolveVMDefaultsBuiltinFallback(t *testing.T) {
// No config override, no host info → every field is "builtin".
d := ResolveVMDefaults(VMDefaultsOverride{}, 0, 0)
if d.VCPUCount != DefaultVCPUCount || d.VCPUSource != "builtin" {
t.Errorf("vcpu = %d (%s), want %d (builtin)", d.VCPUCount, d.VCPUSource, DefaultVCPUCount)
}
if d.MemoryMiB != DefaultMemoryMiB || d.MemorySource != "builtin" {
t.Errorf("memory = %d (%s), want %d (builtin)", d.MemoryMiB, d.MemorySource, DefaultMemoryMiB)
}
if d.WorkDiskSizeBytes != DefaultWorkDiskSize || d.WorkDiskSource != "builtin" {
t.Errorf("disk = %d (%s), want %d (builtin)", d.WorkDiskSizeBytes, d.WorkDiskSource, DefaultWorkDiskSize)
}
}
func TestResolveVMDefaultsAutoFromHost(t *testing.T) {
// 8 host cores, 16 GiB RAM → 2 vcpus, 2 GiB memory.
d := ResolveVMDefaults(VMDefaultsOverride{}, 8, 16*gib)
if d.VCPUCount != 2 || d.VCPUSource != "auto" {
t.Errorf("vcpu = %d (%s), want 2 (auto)", d.VCPUCount, d.VCPUSource)
}
if d.MemoryMiB != 2048 || d.MemorySource != "auto" {
t.Errorf("memory = %d (%s), want 2048 (auto)", d.MemoryMiB, d.MemorySource)
}
// Disk has no auto policy — still builtin.
if d.WorkDiskSource != "builtin" {
t.Errorf("disk source = %s, want builtin", d.WorkDiskSource)
}
}
func TestResolveVMDefaultsConfigWinsOverAuto(t *testing.T) {
override := VMDefaultsOverride{VCPUCount: 6, MemoryMiB: 4096, WorkDiskSizeBytes: 16 * gib}
d := ResolveVMDefaults(override, 8, 16*gib)
if d.VCPUCount != 6 || d.VCPUSource != "config" {
t.Errorf("vcpu = %d (%s), want 6 (config)", d.VCPUCount, d.VCPUSource)
}
if d.MemoryMiB != 4096 || d.MemorySource != "config" {
t.Errorf("memory = %d (%s), want 4096 (config)", d.MemoryMiB, d.MemorySource)
}
if d.WorkDiskSizeBytes != 16*gib || d.WorkDiskSource != "config" {
t.Errorf("disk = %d (%s), want 16*gib (config)", d.WorkDiskSizeBytes, d.WorkDiskSource)
}
}
func TestAutoVCPUClamps(t *testing.T) {
cases := []struct {
host, want int
}{
{1, 1}, // floor
{2, 1},
{4, 1},
{5, 1},
{7, 1},
{8, 2},
{16, 4},
{32, 4}, // ceiling
{128, 4}, // ceiling sticks
}
for _, tc := range cases {
if got := autoVCPU(tc.host); got != tc.want {
t.Errorf("autoVCPU(%d) = %d, want %d", tc.host, got, tc.want)
}
}
}
func TestAutoMemoryCappedAndFloor(t *testing.T) {
// 4 GiB host → floor 1024 MiB.
if got := autoMemoryMiB(4 * gib); got != 1024 {
t.Errorf("4 GiB → got %d, want 1024", got)
}
// 32 GiB host → 32/8 = 4 GiB = 4096 MiB.
if got := autoMemoryMiB(32 * gib); got != 4096 {
t.Errorf("32 GiB → got %d, want 4096", got)
}
// 128 GiB host → 128/8 = 16 GiB, capped at 8 GiB = 8192 MiB.
if got := autoMemoryMiB(128 * gib); got != 8192 {
t.Errorf("128 GiB → got %d, want 8192", got)
}
}
func TestAutoMemoryRoundsTo256MiB(t *testing.T) {
// 17 GiB host → 17/8 = 2.125 GiB ≈ 2176 MiB → rounded to 2048.
if got := autoMemoryMiB(17 * gib); got%256 != 0 {
t.Errorf("%d MiB not a 256 multiple", got)
}
}
func TestFormatSpecLine(t *testing.T) {
d := VMDefaults{VCPUCount: 2, MemoryMiB: 2048, WorkDiskSizeBytes: 8 * gib}
line := d.FormatSpecLine()
for _, want := range []string{"2 vcpu", "2048 MiB", "disk"} {
if !strings.Contains(line, want) {
t.Errorf("line %q missing %q", line, want)
}
}
}
const gib = int64(1024 * 1024 * 1024)