banger/internal/daemon/kernels_test.go
Thales Maciel 16702bd5e1
daemon split (6/n): extract wireServices + drop lazy service getters
Factor the service + capability wiring out of Daemon.Open() into
wireServices(d), an idempotent helper that constructs HostNetwork,
ImageService, WorkspaceService, and VMService from whatever
infrastructure (runner, store, config, layout, logger, closing) is
already set on d. Open() calls it once after filling the composition
root; tests that build &Daemon{...} literals call it to get a working
service graph, preinstalling stubs on the fields they want to fake.

Drops the four lazy-init getters on *Daemon — d.hostNet(),
d.imageSvc(), d.workspaceSvc(), d.vmSvc() — whose sole purpose was
keeping test literals working. Every production call site now reads
d.net / d.img / d.ws / d.vm directly; the services are guaranteed
non-nil once Open returns. No behavior change.

Mechanical: all existing `d.xxxSvc()` calls (production + tests)
rewritten to field access; each `d := &Daemon{...}` in tests gets a
trailing wireServices(d) so the literal + wiring are side-by-side.
Tests that override a pre-built service (e.g. d.img = &ImageService{
bundleFetch: stub}) now set the override before wireServices so the
replacement propagates into VMService's peer pointer.

Also nil-guards HostNetwork.stopVMDNS and d.store in Close() so
partially-initialised daemons (pre-reconcile open failure) still
tear down cleanly — same contract the old lazy getters provided.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:55:28 -03:00

285 lines
8.6 KiB
Go

package daemon
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"banger/internal/api"
"banger/internal/kernelcat"
"banger/internal/paths"
"banger/internal/rpc"
"banger/internal/system"
)
func seedKernelEntry(t *testing.T, kernelsDir, name string) {
t.Helper()
entry := kernelcat.Entry{
Name: name,
Distro: "void",
Arch: "x86_64",
KernelVersion: "6.12.0",
Source: "test",
}
if err := kernelcat.WriteLocal(kernelsDir, entry); err != nil {
t.Fatalf("seed WriteLocal: %v", err)
}
if err := os.WriteFile(filepath.Join(kernelsDir, name, "vmlinux"), []byte("kernel-bytes"), 0o644); err != nil {
t.Fatalf("seed vmlinux: %v", err)
}
}
func TestKernelListReturnsSeededEntries(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
seedKernelEntry(t, kernelsDir, "alpine-3.23")
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
wireServices(d)
result, err := d.img.KernelList(context.Background())
if err != nil {
t.Fatalf("KernelList: %v", err)
}
if len(result.Entries) != 2 {
t.Fatalf("entries = %d, want 2", len(result.Entries))
}
// sorted alphabetically by kernelcat
if result.Entries[0].Name != "alpine-3.23" || result.Entries[1].Name != "void-6.12" {
t.Fatalf("entries order = %+v", result.Entries)
}
if result.Entries[0].KernelPath == "" || !strings.HasSuffix(result.Entries[0].KernelPath, "vmlinux") {
t.Fatalf("KernelPath not populated: %+v", result.Entries[0])
}
}
func TestKernelShowAndDeleteThroughDispatch(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
wireServices(d)
showParams, _ := json.Marshal(api.KernelRefParams{Name: "void-6.12"})
resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "kernel.show", Params: showParams})
if !resp.OK {
t.Fatalf("kernel.show dispatch failed: %+v", resp)
}
var show api.KernelShowResult
if err := json.Unmarshal(resp.Result, &show); err != nil {
t.Fatalf("unmarshal show: %v", err)
}
if show.Entry.Name != "void-6.12" || show.Entry.Distro != "void" {
t.Fatalf("show.Entry = %+v", show.Entry)
}
delParams, _ := json.Marshal(api.KernelRefParams{Name: "void-6.12"})
del := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "kernel.delete", Params: delParams})
if !del.OK {
t.Fatalf("kernel.delete dispatch failed: %+v", del)
}
if _, err := kernelcat.ReadLocal(kernelsDir, "void-6.12"); !os.IsNotExist(err) {
t.Fatalf("entry still present after delete: err=%v", err)
}
}
func TestKernelShowMissingEntry(t *testing.T) {
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
wireServices(d)
_, err := d.img.KernelShow(context.Background(), "nope")
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("KernelShow missing: err=%v", err)
}
}
func TestKernelDeleteRejectsInvalidName(t *testing.T) {
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
wireServices(d)
if err := d.img.KernelDelete(context.Background(), "../escape"); err == nil {
t.Fatalf("KernelDelete should reject traversal")
}
}
func TestRegisterImageResolvesKernelRef(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("write rootfs: %v", err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
store: openDaemonStore(t),
}
wireServices(d)
image, err := d.img.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "void-6.12",
})
if err != nil {
t.Fatalf("RegisterImage: %v", err)
}
want := filepath.Join(kernelsDir, "void-6.12", "vmlinux")
if image.KernelPath != want {
t.Fatalf("image.KernelPath = %q, want %q", image.KernelPath, want)
}
}
func TestRegisterImageRejectsKernelRefAndPath(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatal(err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
store: openDaemonStore(t),
}
wireServices(d)
_, err := d.img.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "void-6.12",
KernelPath: "/some/other/vmlinux",
})
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
t.Fatalf("RegisterImage kernel-ref+kernel: err=%v, want mutually-exclusive error", err)
}
}
func TestKernelImportCopiesArtifactsAndWritesManifest(t *testing.T) {
src := t.TempDir()
if err := os.MkdirAll(filepath.Join(src, "boot"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "boot", "vmlinux-6.12.79_1"), []byte("kernel-bytes"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "boot", "initramfs-6.12.79_1"), []byte("initrd-bytes"), 0o644); err != nil {
t.Fatal(err)
}
modulesSource := filepath.Join(src, "lib", "modules", "6.12.79_1")
if err := os.MkdirAll(modulesSource, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(modulesSource, "modules.dep"), []byte(""), 0o644); err != nil {
t.Fatal(err)
}
kernelsDir := t.TempDir()
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
runner: system.NewRunner(),
}
wireServices(d)
entry, err := d.img.KernelImport(context.Background(), api.KernelImportParams{
Name: "void-6.12",
FromDir: src,
Distro: "void",
Arch: "x86_64",
})
if err != nil {
t.Fatalf("KernelImport: %v", err)
}
if entry.Name != "void-6.12" || entry.Distro != "void" || entry.Arch != "x86_64" {
t.Fatalf("entry metadata = %+v", entry)
}
if entry.KernelVersion != "6.12.79_1" {
t.Errorf("KernelVersion = %q, want 6.12.79_1 (from modules dir)", entry.KernelVersion)
}
if entry.SHA256 == "" {
t.Errorf("SHA256 not populated")
}
if _, err := os.Stat(filepath.Join(kernelsDir, "void-6.12", "vmlinux")); err != nil {
t.Errorf("kernel not copied: %v", err)
}
if _, err := os.Stat(filepath.Join(kernelsDir, "void-6.12", "initrd.img")); err != nil {
t.Errorf("initrd not copied: %v", err)
}
if _, err := os.Stat(filepath.Join(kernelsDir, "void-6.12", "modules", "modules.dep")); err != nil {
t.Errorf("modules not copied: %v", err)
}
}
func TestKernelPullRejectsUnknownCatalogEntry(t *testing.T) {
d := &Daemon{
layout: paths.Layout{KernelsDir: t.TempDir()},
runner: system.NewRunner(),
}
wireServices(d)
_, err := d.img.KernelPull(context.Background(), api.KernelPullParams{Name: "unknown"})
if err == nil || !strings.Contains(err.Error(), "not in catalog") {
t.Fatalf("KernelPull unknown: err=%v", err)
}
}
func TestKernelPullRefusesOverwriteWithoutForce(t *testing.T) {
kernelsDir := t.TempDir()
seedKernelEntry(t, kernelsDir, "void-6.12")
d := &Daemon{
layout: paths.Layout{KernelsDir: kernelsDir},
runner: system.NewRunner(),
}
wireServices(d)
_, err := d.img.KernelPull(context.Background(), api.KernelPullParams{Name: "void-6.12"})
if err == nil || !strings.Contains(err.Error(), "already pulled") {
t.Fatalf("KernelPull without --force: err=%v", err)
}
}
func TestKernelCatalogReportsPulledStatus(t *testing.T) {
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
wireServices(d)
result, err := d.img.KernelCatalog(context.Background())
if err != nil {
t.Fatalf("KernelCatalog: %v", err)
}
// Embedded catalog ships empty; CI (phase 5) populates it.
if result.Entries == nil {
t.Fatalf("Entries should be non-nil even when catalog is empty")
}
}
func TestKernelImportRejectsMissingFromDir(t *testing.T) {
d := &Daemon{
layout: paths.Layout{KernelsDir: t.TempDir()},
runner: system.NewRunner(),
}
wireServices(d)
_, err := d.img.KernelImport(context.Background(), api.KernelImportParams{Name: "x"})
if err == nil || !strings.Contains(err.Error(), "--from") {
t.Fatalf("KernelImport without --from: err=%v", err)
}
}
func TestRegisterImageMissingKernelRef(t *testing.T) {
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
t.Fatal(err)
}
d := &Daemon{
layout: paths.Layout{KernelsDir: t.TempDir()},
store: openDaemonStore(t),
}
wireServices(d)
_, err := d.img.RegisterImage(context.Background(), api.ImageRegisterParams{
Name: "testbox",
RootfsPath: rootfs,
KernelRef: "never-imported",
})
if err == nil || !strings.Contains(err.Error(), "not found in catalog") {
t.Fatalf("RegisterImage missing kernel-ref: err=%v", err)
}
}