vm run: bound the ssh wait and give a useful error on timeout

Before: `guestWaitForSSHFunc` loops forever bounded only by context
cancellation, so if sshd fails to start in the guest `vm run` hangs
indefinitely — which burned a long debugging session during the
golden-image bring-up.

After: the ssh wait gets its own 90s deadline. On guest-side timeout
the error names the VM, explains sshd is the likely suspect, points
at `banger vm logs <name>` for the console output, and notes the VM
is still alive for inspection (or `vm delete` to clean up). Parent
context cancellation (Ctrl-C, caller timeout) still surfaces as-is
without the hint.

`vmRunSSHTimeout` is a var rather than a const so tests can shrink
it; the new TestRunVMRunSSHTimeoutReturnsActionableError sets it to
50ms and asserts the error message contains the actionable bits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-18 15:59:27 -03:00
parent ac7974f5b9
commit 3aa64a63c1
No known key found for this signature in database
GPG key ID: 33112E6833C34679
2 changed files with 79 additions and 2 deletions

View file

@ -1616,6 +1616,58 @@ func TestRunVMRunBareModeSkipsWorkspaceAndTooling(t *testing.T) {
}
}
func TestRunVMRunSSHTimeoutReturnsActionableError(t *testing.T) {
origBegin := vmCreateBeginFunc
origWaitForSSH := guestWaitForSSHFunc
origTimeout := vmRunSSHTimeout
vmRunSSHTimeout = 50 * time.Millisecond
t.Cleanup(func() {
vmCreateBeginFunc = origBegin
guestWaitForSSHFunc = origWaitForSSH
vmRunSSHTimeout = origTimeout
})
vm := model.VMRecord{
ID: "vm-id", Name: "slowvm",
Runtime: model.VMRuntime{State: model.VMStateRunning, GuestIP: "172.16.0.2"},
}
vmCreateBeginFunc = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Done: true, Success: true, VM: &vm}}, nil
}
// Simulate the guest never bringing sshd up — the wait-for-ssh
// child context fires its deadline, returning a DeadlineExceeded.
guestWaitForSSHFunc = func(ctx context.Context, _, _ string, _ time.Duration) error {
<-ctx.Done()
return ctx.Err()
}
var stdout, stderr bytes.Buffer
err := runVMRun(
context.Background(),
"/tmp/bangerd.sock",
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
strings.NewReader(""),
&stdout, &stderr,
api.VMCreateParams{Name: "slowvm"},
nil,
nil,
)
if err == nil {
t.Fatal("want timeout error")
}
msg := err.Error()
for _, want := range []string{
"slowvm",
"did not come up",
"banger vm logs slowvm",
"banger vm delete slowvm",
} {
if !strings.Contains(msg, want) {
t.Fatalf("err = %q, want contains %q", msg, want)
}
}
}
func TestRunVMRunCommandModePropagatesExitCode(t *testing.T) {
origBegin := vmCreateBeginFunc
origWaitForSSH := guestWaitForSSHFunc