banger/internal/daemon/workspace_service.go
Thales Maciel ecb18ce6ca
seams: move the last four package globals onto instance fields
Three test seams were still package-level mutable vars, which tests
had to swap before use. That's the classic path to flaky parallel
tests — two goroutines fighting over the same global fake. Push each
down to the struct that owns the behaviour.

internal/daemon/dns_routing.go
  lookupExecutableFunc + vmDNSAddrFunc → fields on *HostNetwork,
  defaulted at newHostNetwork time. dns_routing_test builds
  HostNetwork{..., lookupExecutable: stub, vmDNSAddr: stub} inline,
  no more t.Cleanup dance around package-level vars.

internal/daemon/preflight.go + doctor.go
  vsockHostDevicePath (mutable string) → vsockHostDevice field on
  *VMService, defaulted via defaultVsockHostDevice constant in
  newVMService. Preflight reads s.vsockHostDevice; doctor reads
  d.vm.vsockHostDevice. Logger test sets d.vm.vsockHostDevice = tmp
  after wireServices.

internal/daemon/workspace/workspace.go
  HostCommandOutputFunc → *Inspector struct with a Runner field.
  Every git-using helper (GitOutput, GitTrimmedOutput,
  GitResolvedConfigValue, RunHostCommand, ListSubmodules,
  ListOverlayPaths, CountUntrackedPaths, InspectRepo,
  ImportRepoToGuest, PrepareRepoCopy) is now a method on *Inspector.
  NewInspector() wraps the real host runner for production;
  WorkspaceService holds one via repoInspector, CLI deps holds one
  too. cli_test.go's submodule-rejection test builds its own
  Inspector with a scripted Runner instead of patching a global.
  Pure helpers (FinalizeScript, ResolveSourcePath, ParsePrepareMode,
  ShellQuote, FormatStepError, GitFileURL, ParseNullSeparatedOutput)
  stay free functions since they don't touch the host.

Sentinel: grep for HostCommandOutputFunc, lookupExecutableFunc,
vmDNSAddrFunc, vsockHostDevicePath is now empty across internal/.
make lint test green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:07:14 -03:00

94 lines
3.9 KiB
Go

package daemon
import (
"context"
"log/slog"
"time"
ws "banger/internal/daemon/workspace"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/store"
"banger/internal/system"
)
// WorkspaceService owns workspace.prepare / workspace.export plus the
// ssh-key + git-identity sync that runs as part of VM start's
// prepare_work_disk capability hook. The workspaceLocks set lives here
// so its scope (serialise concurrent tar imports on the same VM) is
// obvious at the field definition.
//
// The inspect/import test seams are per-service fields so tests inject
// fakes without mutating package-level state.
type WorkspaceService struct {
runner system.CommandRunner
logger *slog.Logger
config model.DaemonConfig
layout paths.Layout
store *store.Store
// workspaceLocks serialises concurrent workspace.prepare /
// workspace.export on the same VM. Separate from vmLocks so slow
// guest I/O doesn't block lifecycle ops.
workspaceLocks vmLockSet
// Peer-service access via narrow function-typed dependencies.
// WorkspaceService doesn't hold pointers to the full VMService or
// HostNetwork; it only sees the exact operations it needs.
vmResolver func(ctx context.Context, idOrName string) (model.VMRecord, error)
aliveChecker func(vm model.VMRecord) bool
waitGuestSSH func(ctx context.Context, address string, interval time.Duration) error
dialGuest func(ctx context.Context, address string) (guestSSHClient, error)
imageResolver func(ctx context.Context, idOrName string) (model.Image, error)
imageWorkSeed func(ctx context.Context, image model.Image, fingerprint string) error
withVMLockByRef func(ctx context.Context, idOrName string, fn func(model.VMRecord) (model.VMRecord, error)) (model.VMRecord, error)
beginOperation func(name string, attrs ...any) *operationLog
// repoInspector is the Inspector used by the real InspectRepo /
// ImportRepoToGuest fallbacks when the test seams below aren't
// set. wireServices installs the production one; tests that want
// to intercept only the host-command surface (not the whole
// inspect/import hook) can assign a stub-runner Inspector here.
repoInspector *ws.Inspector
// Test seams.
workspaceInspectRepo func(ctx context.Context, sourcePath, branchName, fromRef string, includeUntracked bool) (ws.RepoSpec, error)
workspaceImport func(ctx context.Context, client ws.GuestClient, spec ws.RepoSpec, guestPath string, mode model.WorkspacePrepareMode) error
}
type workspaceServiceDeps struct {
runner system.CommandRunner
logger *slog.Logger
config model.DaemonConfig
layout paths.Layout
store *store.Store
repoInspector *ws.Inspector
vmResolver func(ctx context.Context, idOrName string) (model.VMRecord, error)
aliveChecker func(vm model.VMRecord) bool
waitGuestSSH func(ctx context.Context, address string, interval time.Duration) error
dialGuest func(ctx context.Context, address string) (guestSSHClient, error)
imageResolver func(ctx context.Context, idOrName string) (model.Image, error)
imageWorkSeed func(ctx context.Context, image model.Image, fingerprint string) error
withVMLockByRef func(ctx context.Context, idOrName string, fn func(model.VMRecord) (model.VMRecord, error)) (model.VMRecord, error)
beginOperation func(name string, attrs ...any) *operationLog
}
func newWorkspaceService(deps workspaceServiceDeps) *WorkspaceService {
return &WorkspaceService{
runner: deps.runner,
logger: deps.logger,
config: deps.config,
layout: deps.layout,
store: deps.store,
repoInspector: deps.repoInspector,
vmResolver: deps.vmResolver,
aliveChecker: deps.aliveChecker,
waitGuestSSH: deps.waitGuestSSH,
dialGuest: deps.dialGuest,
imageResolver: deps.imageResolver,
imageWorkSeed: deps.imageWorkSeed,
withVMLockByRef: deps.withVMLockByRef,
beginOperation: deps.beginOperation,
}
}