cli + daemon: move test seams off package globals onto injected structs

CLI: introduce internal/cli.deps which owns every RPC/SSH/host-command
seam the tree used to reach through mutable package vars. Command
builders, orchestrators, and the completion helpers become methods on
*deps. Tests construct their own deps per case, so fakes no longer leak
across cases and tests are free to run in parallel.

Daemon: move workspaceInspectRepoFunc + workspaceImportFunc onto the
Daemon struct (workspaceInspectRepo / workspaceImport), mirroring the
existing guestWaitForSSH / guestDial pattern. Workspace-prepare tests
drop t.Parallel() guards now that they no longer mutate process-wide
state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-19 19:03:55 -03:00
parent d38f580e00
commit c42fcbe012
No known key found for this signature in database
GPG key ID: 33112E6833C34679
19 changed files with 664 additions and 733 deletions

View file

@ -80,9 +80,9 @@ func (e ExitCodeError) Error() string {
// - it sits inside a non-bare git repository,
// - the repository has no submodules (unsupported in the shallow
// overlay mode vm run uses).
func vmRunPreflightRepo(ctx context.Context, rawPath string) (string, error) {
func (d *deps) vmRunPreflightRepo(ctx context.Context, rawPath string) (string, error) {
if strings.TrimSpace(rawPath) == "" {
wd, err := cwdFunc()
wd, err := d.cwd()
if err != nil {
return "", err
}
@ -131,9 +131,9 @@ func splitVMRunArgs(cmd *cobra.Command, args []string) (pathArgs, commandArgs []
// for guest ssh, optionally materialise a workspace and kick off the
// tooling bootstrap, then either attach interactively or run the
// user's command and propagate its exit status.
func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, params api.VMCreateParams, repo *vmRunRepo, command []string, removeOnExit bool) error {
func (d *deps) runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, params api.VMCreateParams, repo *vmRunRepo, command []string, removeOnExit bool) error {
progress := newVMRunProgressRenderer(stderr)
vm, err := runVMCreate(ctx, socketPath, stderr, params)
vm, err := d.runVMCreate(ctx, socketPath, stderr, params)
if err != nil {
return err
}
@ -155,7 +155,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
// doesn't abort the delete RPC.
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := vmDeleteFunc(cleanupCtx, socketPath, vmRef); err != nil {
if err := d.vmDelete(cleanupCtx, socketPath, vmRef); err != nil {
printVMRunWarning(stderr, fmt.Sprintf("--rm cleanup failed: %v (leaked vm %q; delete manually)", err, vmRef))
}
}()
@ -163,7 +163,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
sshAddress := net.JoinHostPort(vm.Runtime.GuestIP, "22")
progress.render("waiting for guest ssh")
sshCtx, cancelSSH := context.WithTimeout(ctx, vmRunSSHTimeout)
if err := guestWaitForSSHFunc(sshCtx, sshAddress, cfg.SSHKeyPath, 250*time.Millisecond); err != nil {
if err := d.guestWaitForSSH(sshCtx, sshAddress, cfg.SSHKeyPath, 250*time.Millisecond); err != nil {
cancelSSH()
// Surface parent-context cancellation (Ctrl-C, caller
// timeout) as-is. Only the guest-side timeout needs the
@ -193,7 +193,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
if strings.TrimSpace(repo.branchName) != "" {
fromRef = repo.fromRef
}
prepared, err := vmWorkspacePrepareFunc(ctx, socketPath, api.VMWorkspacePrepareParams{
prepared, err := d.vmWorkspacePrepare(ctx, socketPath, api.VMWorkspacePrepareParams{
IDOrName: vmRef,
SourcePath: repo.sourcePath,
GuestPath: vmRunGuestDir(),
@ -208,11 +208,11 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
// daemon side; grab what the tooling harness needs from its
// result instead of re-inspecting here.
if len(command) == 0 {
client, err := guestDialFunc(ctx, sshAddress, cfg.SSHKeyPath)
client, err := d.guestDial(ctx, sshAddress, cfg.SSHKeyPath)
if err != nil {
return fmt.Errorf("vm %q is running but guest ssh is unavailable: %w", vmRef, err)
}
if err := startVMRunToolingHarness(ctx, client, prepared.Workspace.RepoRoot, prepared.Workspace.RepoName, progress); err != nil {
if err := d.startVMRunToolingHarness(ctx, client, prepared.Workspace.RepoRoot, prepared.Workspace.RepoName, progress); err != nil {
printVMRunWarning(stderr, fmt.Sprintf("guest tooling bootstrap start failed: %v", err))
}
_ = client.Close()
@ -224,7 +224,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
}
if len(command) > 0 {
progress.render("running command in guest")
if err := sshExecFunc(ctx, stdin, stdout, stderr, sshArgs); err != nil {
if err := d.sshExec(ctx, stdin, stdout, stderr, sshArgs); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return ExitCodeError{Code: exitErr.ExitCode()}
@ -234,7 +234,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
return nil
}
progress.render("attaching to guest")
return runSSHSession(ctx, socketPath, vmRef, stdin, stdout, stderr, sshArgs, removeOnExit)
return d.runSSHSession(ctx, socketPath, vmRef, stdin, stdout, stderr, sshArgs, removeOnExit)
}
func vmRunGuestDir() string {
@ -253,11 +253,11 @@ func vmRunToolingHarnessLogPath(repoName string) string {
// script inside the guest. repoRoot / repoName both come from the
// daemon's workspace.prepare RPC response — the CLI no longer does
// its own git inspection.
func startVMRunToolingHarness(ctx context.Context, client vmRunGuestClient, repoRoot, repoName string, progress *vmRunProgressRenderer) error {
func (d *deps) startVMRunToolingHarness(ctx context.Context, client vmRunGuestClient, repoRoot, repoName string, progress *vmRunProgressRenderer) error {
if progress != nil {
progress.render("starting guest tooling bootstrap")
}
plan := buildVMRunToolingPlanFunc(ctx, repoRoot)
plan := d.buildVMRunToolingPlan(ctx, repoRoot)
var uploadLog bytes.Buffer
if err := client.UploadFile(ctx, vmRunToolingHarnessPath(repoName), 0o755, []byte(vmRunToolingHarnessScript(plan)), &uploadLog); err != nil {
return formatVMRunStepError("upload guest tooling bootstrap", err, uploadLog.String())