package daemon import ( "bufio" "bytes" "context" "encoding/json" "net" "os" "path/filepath" "strings" "testing" "time" "banger/internal/api" "banger/internal/model" "banger/internal/paths" "banger/internal/rpc" "banger/internal/store" ) func TestEnsureDefaultImageUsesConfiguredDefaultRootfs(t *testing.T) { dir := t.TempDir() rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) d := &Daemon{ config: model.DaemonConfig{ DefaultImageName: "default", DefaultRootfs: rootfs, DefaultKernel: kernel, }, store: db, } if err := d.ensureDefaultImage(context.Background()); err != nil { t.Fatalf("ensureDefaultImage: %v", err) } image, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if image.RootfsPath != rootfs { t.Fatalf("RootfsPath = %q, want %q", image.RootfsPath, rootfs) } if image.KernelPath != kernel { t.Fatalf("KernelPath = %q, want %q", image.KernelPath, kernel) } if image.Managed { t.Fatal("default image should be unmanaged") } } func TestEnsureDefaultImageLeavesCurrentUnmanagedDefaultUntouched(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) image := model.Image{ ID: "default-id", Name: "default", Managed: false, RootfsPath: rootfs, KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, PackagesPath: packages, Docker: true, CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), image); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ config: model.DaemonConfig{ DefaultImageName: "default", DefaultRootfs: rootfs, DefaultKernel: kernel, DefaultInitrd: initrd, DefaultModulesDir: modulesDir, DefaultPackagesFile: packages, }, store: db, } if err := d.ensureDefaultImage(context.Background()); err != nil { t.Fatalf("ensureDefaultImage: %v", err) } got, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if got.ID != image.ID { t.Fatalf("ID = %q, want %q", got.ID, image.ID) } if !got.UpdatedAt.Equal(image.UpdatedAt) { t.Fatalf("UpdatedAt = %s, want unchanged %s", got.UpdatedAt, image.UpdatedAt) } } func TestEnsureDefaultImageReconcilesStaleUnmanagedDefaultInPlace(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) stale := model.Image{ ID: "default-id", Name: "default", Managed: false, RootfsPath: "/home/thales/projects/personal/banger/build/runtime/rootfs-docker.ext4", KernelPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/vmlinux-6.8.0-94-generic", InitrdPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/initrd.img-6.8.0-94-generic", ModulesDir: "/home/thales/projects/personal/banger/build/runtime/wtf/root/lib/modules/6.8.0-94-generic", PackagesPath: "/home/thales/projects/personal/banger/build/runtime/packages.apt", Docker: true, CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), stale); err != nil { t.Fatalf("UpsertImage: %v", err) } vm := testVM("uses-default", stale.ID, "172.16.0.25") if err := db.UpsertVM(context.Background(), vm); err != nil { t.Fatalf("UpsertVM: %v", err) } d := &Daemon{ config: model.DaemonConfig{ DefaultImageName: "default", DefaultRootfs: rootfs, DefaultKernel: kernel, DefaultInitrd: initrd, DefaultModulesDir: modulesDir, DefaultPackagesFile: packages, }, store: db, } if err := d.ensureDefaultImage(context.Background()); err != nil { t.Fatalf("ensureDefaultImage: %v", err) } got, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if got.ID != stale.ID { t.Fatalf("ID = %q, want preserved %q", got.ID, stale.ID) } if !got.CreatedAt.Equal(stale.CreatedAt) { t.Fatalf("CreatedAt = %s, want preserved %s", got.CreatedAt, stale.CreatedAt) } if got.RootfsPath != rootfs || got.KernelPath != kernel || got.InitrdPath != initrd || got.ModulesDir != modulesDir || got.PackagesPath != packages { t.Fatalf("stale default not reconciled: %+v", got) } if !got.UpdatedAt.After(stale.UpdatedAt) { t.Fatalf("UpdatedAt = %s, want newer than %s", got.UpdatedAt, stale.UpdatedAt) } gotVM, err := db.GetVMByID(context.Background(), vm.ID) if err != nil { t.Fatalf("GetVMByID: %v", err) } if gotVM.ImageID != stale.ID { t.Fatalf("VM image ID = %q, want preserved %q", gotVM.ImageID, stale.ID) } } func TestEnsureDefaultImageLeavesManagedDefaultUntouched(t *testing.T) { dir := t.TempDir() rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) managed := model.Image{ ID: "managed-default", Name: "default", Managed: true, RootfsPath: "/managed/rootfs.ext4", KernelPath: "/managed/vmlinux", CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), managed); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ config: model.DaemonConfig{ DefaultImageName: "default", DefaultRootfs: rootfs, DefaultKernel: kernel, }, store: db, } if err := d.ensureDefaultImage(context.Background()); err != nil { t.Fatalf("ensureDefaultImage: %v", err) } got, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if got.RootfsPath != managed.RootfsPath || got.KernelPath != managed.KernelPath { t.Fatalf("managed default was rewritten: %+v", got) } } func TestEnsureDefaultImageSkipsRewriteWhenCurrentArtifactsMissing(t *testing.T) { dir := t.TempDir() db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) stale := model.Image{ ID: "default-id", Name: "default", Managed: false, RootfsPath: "/old/rootfs.ext4", KernelPath: "/old/vmlinux", CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), stale); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ config: model.DaemonConfig{ DefaultImageName: "default", DefaultRootfs: filepath.Join(dir, "missing-rootfs.ext4"), DefaultKernel: filepath.Join(dir, "missing-vmlinux"), }, store: db, } if err := d.ensureDefaultImage(context.Background()); err != nil { t.Fatalf("ensureDefaultImage: %v", err) } got, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if got.RootfsPath != stale.RootfsPath || got.KernelPath != stale.KernelPath { t.Fatalf("default image should have stayed stale when no current artifacts exist: %+v", got) } } func TestRegisterImageCreatesUnmanagedImage(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, _ := writeDefaultImageArtifacts(t, dir) workSeed := filepath.Join(dir, "rootfs-void.work-seed.ext4") packages := filepath.Join(dir, "packages.void") if err := os.WriteFile(workSeed, []byte("seed"), 0o644); err != nil { t.Fatalf("WriteFile(workSeed): %v", err) } if err := os.WriteFile(packages, []byte("base-minimal\nopenssh\n"), 0o644); err != nil { t.Fatalf("WriteFile(packages): %v", err) } db := openDefaultImageStore(t, dir) d := &Daemon{ config: model.DaemonConfig{ DefaultKernel: kernel, DefaultInitrd: initrd, DefaultModulesDir: modulesDir, }, store: db, } image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{ Name: "void-exp", RootfsPath: rootfs, WorkSeedPath: workSeed, PackagesPath: packages, }) if err != nil { t.Fatalf("RegisterImage: %v", err) } if image.Managed { t.Fatal("registered image should be unmanaged") } if image.Name != "void-exp" || image.RootfsPath != rootfs || image.WorkSeedPath != workSeed || image.KernelPath != kernel { t.Fatalf("registered image = %+v", image) } } func TestRegisterImageUpdatesExistingUnmanagedImageInPlace(t *testing.T) { dir := t.TempDir() _, kernel, initrd, modulesDir, _ := writeDefaultImageArtifacts(t, dir) newRootfs := filepath.Join(dir, "rootfs-void-next.ext4") newWorkSeed := filepath.Join(dir, "rootfs-void-next.work-seed.ext4") packages := filepath.Join(dir, "packages.void") for _, path := range []string{newRootfs, newWorkSeed} { if err := os.WriteFile(path, []byte("next"), 0o644); err != nil { t.Fatalf("WriteFile(%s): %v", path, err) } } if err := os.WriteFile(packages, []byte("base-minimal\n"), 0o644); err != nil { t.Fatalf("WriteFile(packages): %v", err) } db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) existing := model.Image{ ID: "void-image-id", Name: "void-exp", Managed: false, RootfsPath: filepath.Join(dir, "old-rootfs.ext4"), KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, PackagesPath: packages, CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), existing); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ config: model.DaemonConfig{ DefaultKernel: kernel, DefaultInitrd: initrd, DefaultModulesDir: modulesDir, }, store: db, } image, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{ Name: "void-exp", RootfsPath: newRootfs, WorkSeedPath: newWorkSeed, PackagesPath: packages, }) if err != nil { t.Fatalf("RegisterImage: %v", err) } if image.ID != existing.ID || !image.CreatedAt.Equal(existing.CreatedAt) { t.Fatalf("updated image identity changed: %+v", image) } if image.RootfsPath != newRootfs || image.WorkSeedPath != newWorkSeed { t.Fatalf("updated image paths not applied: %+v", image) } } func TestRegisterImageRejectsManagedOverwrite(t *testing.T) { dir := t.TempDir() rootfs, kernel, _, _, _ := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 16, 12, 0, 0, 0, time.UTC) if err := db.UpsertImage(context.Background(), model.Image{ ID: "managed-id", Name: "void-exp", Managed: true, RootfsPath: rootfs, KernelPath: kernel, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{config: model.DaemonConfig{DefaultKernel: kernel}, store: db} _, err := d.RegisterImage(context.Background(), api.ImageRegisterParams{ Name: "void-exp", RootfsPath: rootfs, }) if err == nil || !strings.Contains(err.Error(), "cannot be updated via register") { t.Fatalf("RegisterImage(managed) error = %v", err) } } func TestPromoteImageCopiesArtifactsAndPreservesIdentity(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir) workSeed := filepath.Join(dir, "rootfs-docker.work-seed.ext4") workSeedContent := []byte("seed-data") if err := os.WriteFile(workSeed, workSeedContent, 0o644); err != nil { t.Fatalf("WriteFile(workSeed): %v", err) } db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC) existing := model.Image{ ID: "promote-image-id", Name: "default", Managed: false, RootfsPath: rootfs, WorkSeedPath: workSeed, KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, PackagesPath: packages, Docker: true, CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), existing); err != nil { t.Fatalf("UpsertImage: %v", err) } vm := testVM("uses-default", existing.ID, "172.16.0.44") if err := db.UpsertVM(context.Background(), vm); err != nil { t.Fatalf("UpsertVM: %v", err) } d := &Daemon{ layout: modelPathsLayoutForTest(dir), store: db, } image, err := d.PromoteImage(context.Background(), "default") if err != nil { t.Fatalf("PromoteImage: %v", err) } if !image.Managed { t.Fatal("promoted image should be managed") } if image.ID != existing.ID || image.Name != existing.Name { t.Fatalf("promoted image identity changed: %+v", image) } if !image.CreatedAt.Equal(existing.CreatedAt) { t.Fatalf("CreatedAt = %s, want preserved %s", image.CreatedAt, existing.CreatedAt) } if !image.UpdatedAt.After(existing.UpdatedAt) { t.Fatalf("UpdatedAt = %s, want newer than %s", image.UpdatedAt, existing.UpdatedAt) } wantArtifactDir := filepath.Join(d.layout.ImagesDir, existing.ID) if image.ArtifactDir != wantArtifactDir { t.Fatalf("ArtifactDir = %q, want %q", image.ArtifactDir, wantArtifactDir) } if image.RootfsPath != filepath.Join(wantArtifactDir, "rootfs.ext4") { t.Fatalf("RootfsPath = %q, want managed copy", image.RootfsPath) } if image.WorkSeedPath != filepath.Join(wantArtifactDir, "work-seed.ext4") { t.Fatalf("WorkSeedPath = %q, want managed copy", image.WorkSeedPath) } if image.KernelPath != kernel || image.InitrdPath != initrd || image.ModulesDir != modulesDir || image.PackagesPath != packages { t.Fatalf("boot support paths changed unexpectedly: %+v", image) } rootfsContent, err := os.ReadFile(rootfs) if err != nil { t.Fatalf("ReadFile(rootfs): %v", err) } managedRootfsContent, err := os.ReadFile(image.RootfsPath) if err != nil { t.Fatalf("ReadFile(managed rootfs): %v", err) } if !bytes.Equal(managedRootfsContent, rootfsContent) { t.Fatal("managed rootfs copy content mismatch") } managedWorkSeedContent, err := os.ReadFile(image.WorkSeedPath) if err != nil { t.Fatalf("ReadFile(managed work seed): %v", err) } if !bytes.Equal(managedWorkSeedContent, workSeedContent) { t.Fatal("managed work seed copy content mismatch") } got, err := db.GetImageByName(context.Background(), "default") if err != nil { t.Fatalf("GetImageByName: %v", err) } if got.RootfsPath != image.RootfsPath || !got.Managed || got.ArtifactDir != image.ArtifactDir { t.Fatalf("stored promoted image = %+v, want %+v", got, image) } gotVM, err := db.GetVMByID(context.Background(), vm.ID) if err != nil { t.Fatalf("GetVMByID: %v", err) } if gotVM.ImageID != existing.ID { t.Fatalf("VM image ID = %q, want preserved %q", gotVM.ImageID, existing.ID) } } func TestPromoteImageRejectsManagedImage(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC) if err := db.UpsertImage(context.Background(), model.Image{ ID: "managed-id", Name: "default", Managed: true, ArtifactDir: filepath.Join(dir, "images", "managed-id"), RootfsPath: rootfs, KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, PackagesPath: packages, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ layout: modelPathsLayoutForTest(dir), store: db, } _, err := d.PromoteImage(context.Background(), "default") if err == nil || !strings.Contains(err.Error(), "already managed") { t.Fatalf("PromoteImage(managed) error = %v", err) } } func TestPromoteImageSkipsMissingWorkSeed(t *testing.T) { dir := t.TempDir() rootfs, kernel, initrd, modulesDir, packages := writeDefaultImageArtifacts(t, dir) db := openDefaultImageStore(t, dir) now := time.Date(2026, time.March, 20, 12, 0, 0, 0, time.UTC) existing := model.Image{ ID: "promote-missing-seed", Name: "default", Managed: false, RootfsPath: rootfs, WorkSeedPath: filepath.Join(dir, "missing.work-seed.ext4"), KernelPath: kernel, InitrdPath: initrd, ModulesDir: modulesDir, PackagesPath: packages, CreatedAt: now, UpdatedAt: now, } if err := db.UpsertImage(context.Background(), existing); err != nil { t.Fatalf("UpsertImage: %v", err) } d := &Daemon{ layout: modelPathsLayoutForTest(dir), store: db, } image, err := d.PromoteImage(context.Background(), "default") if err != nil { t.Fatalf("PromoteImage: %v", err) } if image.WorkSeedPath != "" { t.Fatalf("WorkSeedPath = %q, want empty for missing source work seed", image.WorkSeedPath) } if _, err := os.Stat(filepath.Join(image.ArtifactDir, "work-seed.ext4")); !os.IsNotExist(err) { t.Fatalf("managed work-seed should not exist, stat error = %v", err) } } func openDefaultImageStore(t *testing.T, dir string) *store.Store { t.Helper() db, err := store.Open(filepath.Join(dir, "state.db")) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { _ = db.Close() }) return db } func writeDefaultImageArtifacts(t *testing.T, dir string) (rootfs, kernel, initrd, modulesDir, packages string) { t.Helper() rootfs = filepath.Join(dir, "rootfs-docker.ext4") kernel = filepath.Join(dir, "vmlinux") initrd = filepath.Join(dir, "initrd.img") modulesDir = filepath.Join(dir, "modules") packages = filepath.Join(dir, "packages.apt") files := []string{ rootfs, kernel, initrd, packages, filepath.Join(modulesDir, "modules.dep"), } for _, path := range files { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir %s: %v", filepath.Dir(path), err) } if err := os.WriteFile(path, []byte("test"), 0o644); err != nil { t.Fatalf("write %s: %v", path, err) } } return rootfs, kernel, initrd, modulesDir, packages } func modelPathsLayoutForTest(dir string) paths.Layout { return paths.Layout{ ImagesDir: filepath.Join(dir, "images"), } } func TestStartVMDNSFailsWhenAddressBusy(t *testing.T) { t.Parallel() packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatalf("ListenPacket: %v", err) } defer packetConn.Close() d := &Daemon{} if err := d.startVMDNS(packetConn.LocalAddr().String()); err == nil { t.Fatal("startVMDNS() succeeded on occupied address, want failure") } } func TestSetDNSPublishesIntoDaemonServer(t *testing.T) { t.Parallel() d := &Daemon{} if err := d.startVMDNS("127.0.0.1:0"); err != nil { t.Fatalf("startVMDNS: %v", err) } defer d.stopVMDNS() if err := d.setDNS(context.Background(), "devbox", "172.16.0.8"); err != nil { t.Fatalf("setDNS: %v", err) } if _, ok := d.vmDNS.Lookup("devbox.vm"); !ok { t.Fatal("devbox.vm missing after setDNS") } } func TestDispatchUsesPassedContext(t *testing.T) { t.Parallel() db := openDefaultImageStore(t, t.TempDir()) d := &Daemon{store: db} ctx, cancel := context.WithCancel(context.Background()) cancel() resp := d.dispatch(ctx, rpc.Request{ Version: rpc.Version, Method: "vm.list", Params: mustJSON(t, api.Empty{}), }) if resp.OK { t.Fatal("dispatch() succeeded with canceled context") } if resp.Error == nil || !strings.Contains(resp.Error.Message, context.Canceled.Error()) { t.Fatalf("dispatch() error = %+v, want context canceled", resp.Error) } } func TestHandleConnCancelsRequestWhenClientDisconnects(t *testing.T) { t.Parallel() server, client := net.Pipe() defer client.Close() requestCanceled := make(chan struct{}) done := make(chan struct{}) d := &Daemon{ closing: make(chan struct{}), requestHandler: func(ctx context.Context, req rpc.Request) rpc.Response { if req.Method != "block" { t.Errorf("request method = %q, want block", req.Method) } <-ctx.Done() close(requestCanceled) return rpc.NewError("operation_failed", ctx.Err().Error()) }, } go func() { d.handleConn(server) close(done) }() if err := json.NewEncoder(client).Encode(rpc.Request{Version: rpc.Version, Method: "block"}); err != nil { t.Fatalf("encode request: %v", err) } if err := client.Close(); err != nil { t.Fatalf("close client: %v", err) } select { case <-requestCanceled: case <-time.After(2 * time.Second): t.Fatal("request context was not canceled after client disconnect") } select { case <-done: case <-time.After(2 * time.Second): t.Fatal("handleConn did not return after client disconnect") } } func TestWatchRequestDisconnectCancelsContextOnEOF(t *testing.T) { t.Parallel() server, client := net.Pipe() defer server.Close() reader := bufio.NewReader(server) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) d := &Daemon{closing: make(chan struct{})} stop := d.watchRequestDisconnect(server, reader, "block", cancel) defer stop() if err := client.Close(); err != nil { t.Fatalf("close client: %v", err) } select { case <-ctx.Done(): if !strings.Contains(ctx.Err().Error(), context.Canceled.Error()) { t.Fatalf("ctx.Err() = %v, want canceled", ctx.Err()) } case <-time.After(2 * time.Second): t.Fatal("watchRequestDisconnect did not cancel context") } } func mustJSON(t *testing.T, v any) []byte { t.Helper() data, err := json.Marshal(v) if err != nil { t.Fatalf("json.Marshal(%T): %v", v, err) } return data }