banger/internal/daemon/autopull_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

152 lines
4.7 KiB
Go

package daemon
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"banger/internal/imagecat"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/system"
)
func TestFindOrAutoPullImageReturnsLocalWithoutPulling(t *testing.T) {
d := &Daemon{
layout: paths.Layout{ImagesDir: t.TempDir()},
store: openDaemonStore(t),
runner: system.NewRunner(),
}
d.img = &ImageService{
layout: d.layout,
store: d.store,
runner: d.runner,
bundleFetch: func(context.Context, string, imagecat.CatEntry) (imagecat.Manifest, error) {
t.Fatal("bundleFetch should not be called when image is local")
return imagecat.Manifest{}, nil
},
}
wireServices(d)
id, _ := model.NewID()
if err := d.store.UpsertImage(context.Background(), model.Image{
ID: id,
Name: "my-local-image",
CreatedAt: model.Now(),
UpdatedAt: model.Now(),
}); err != nil {
t.Fatal(err)
}
image, err := d.vm.findOrAutoPullImage(context.Background(), "my-local-image")
if err != nil {
t.Fatalf("findOrAutoPullImage: %v", err)
}
if image.Name != "my-local-image" {
t.Fatalf("Name = %q, want my-local-image", image.Name)
}
}
func TestFindOrAutoPullImagePullsFromCatalog(t *testing.T) {
imagesDir := t.TempDir()
kernelsDir := t.TempDir()
seedKernel(t, kernelsDir, "generic-6.12")
pullCalls := 0
d := &Daemon{
layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
store: openDaemonStore(t),
runner: system.NewRunner(),
}
d.img = &ImageService{
layout: d.layout,
store: d.store,
runner: d.runner,
bundleFetch: func(ctx context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
pullCalls++
return stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"})(ctx, destDir, entry)
},
}
wireServices(d)
// "debian-bookworm" is in the embedded imagecat catalog.
image, err := d.vm.findOrAutoPullImage(context.Background(), "debian-bookworm")
if err != nil {
t.Fatalf("findOrAutoPullImage: %v", err)
}
if image.Name != "debian-bookworm" {
t.Fatalf("Name = %q, want debian-bookworm", image.Name)
}
if pullCalls != 1 {
t.Fatalf("bundleFetch calls = %d, want 1", pullCalls)
}
}
func TestFindOrAutoPullImageReturnsOriginalErrorWhenNotInCatalog(t *testing.T) {
d := &Daemon{
layout: paths.Layout{ImagesDir: t.TempDir()},
store: openDaemonStore(t),
runner: system.NewRunner(),
}
wireServices(d)
_, err := d.vm.findOrAutoPullImage(context.Background(), "not-in-catalog-or-store")
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("err = %v, want not-found", err)
}
}
func TestReadOrAutoPullKernelReturnsLocalWithoutPulling(t *testing.T) {
kernelsDir := t.TempDir()
seedKernel(t, kernelsDir, "generic-6.12")
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
wireServices(d)
entry, err := d.img.readOrAutoPullKernel(context.Background(), "generic-6.12")
if err != nil {
t.Fatalf("readOrAutoPullKernel: %v", err)
}
if entry.Name != "generic-6.12" {
t.Fatalf("Name = %q", entry.Name)
}
}
func TestReadOrAutoPullKernelErrorsWhenNotInCatalog(t *testing.T) {
d := &Daemon{layout: paths.Layout{KernelsDir: t.TempDir()}}
wireServices(d)
_, err := d.img.readOrAutoPullKernel(context.Background(), "nonexistent-kernel")
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("err = %v, want not-found", err)
}
}
// TestReadOrAutoPullKernelSurfacesNonNotExistError covers the path where
// kernelcat.ReadLocal fails for a reason other than missing entry (e.g.
// corrupt manifest); the autopull logic should NOT try to fetch in that
// case since the entry clearly exists in some broken form.
func TestReadOrAutoPullKernelSurfacesNonNotExistError(t *testing.T) {
kernelsDir := t.TempDir()
// Seed a manifest that doesn't match the entry's own Name field —
// kernelcat.ReadLocal returns an error, not os.ErrNotExist.
dir := filepath.Join(kernelsDir, "broken-kernel")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"name":"different-name"}`), 0o644); err != nil {
t.Fatal(err)
}
d := &Daemon{layout: paths.Layout{KernelsDir: kernelsDir}}
wireServices(d)
_, err := d.img.readOrAutoPullKernel(context.Background(), "broken-kernel")
if err == nil {
t.Fatal("want error")
}
// Must not be wrapped in an "auto-pull" message — the corrupt-manifest
// failure should surface as the primary cause.
if strings.Contains(err.Error(), "not found in catalog") {
t.Fatalf("err = %v, should not claim 'not in catalog'", err)
}
// Sanity: ensure it's not os.ErrNotExist-compatible.
if errors.Is(err, os.ErrNotExist) {
t.Fatalf("err = %v, should not be os.ErrNotExist", err)
}
}