Ctrl K
ComunidadGuías…

cli/cliui/agent_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cliui_test

import (
	"bufio"
	"bytes"
	"context"
	"io"
	"os"
	"regexp"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/xerrors"
	"tailscale.com/tailcfg"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/cli/cliui"
	"github.com/coder/coder/v2/coderd/healthcheck/health"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/codersdk/healthsdk"
	"github.com/coder/coder/v2/codersdk/workspacesdk"
	"github.com/coder/coder/v2/tailnet"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/serpent"
)

func TestAgent(t *testing.T) {
	t.Parallel()

	waitLines := func(t *testing.T, output <-chan string, lines ...string) error {
		t.Helper()

		var got []string
	outerLoop:
		for _, want := range lines {
			for {
				select {
				case line := <-output:
					got = append(got, line)
					if strings.Contains(line, want) {
						continue outerLoop
					}
				case <-time.After(testutil.WaitShort):
					assert.Failf(t, "timed out waiting for line", "want: %q; got: %q", want, got)
					return xerrors.Errorf("timed out waiting for line: %q; got: %q", want, got)
				}
			}
		}
		return nil
	}

	for _, tc := range []struct {
		name    string
		iter    []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error
		logs    chan []codersdk.WorkspaceAgentLog
		opts    cliui.AgentOptions
		want    []string
		wantErr bool
	}{
		{
			name: "Initial connection",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnecting
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					return nil
				},
			},
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
				"✔ Waiting for the workspace agent to connect",
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"Notice: The startup scripts are still running and your workspace may be incomplete.",
				"For more information and troubleshooting, see",
			},
		},
		{
			name: "Start timeout",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnecting
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
					agent.StartedAt = new(time.Now())
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartTimeout
					agent.FirstConnectedAt = new(time.Now())
					agent.ReadyAt = new(time.Now())
					return nil
				},
			},
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
				"✔ Waiting for the workspace agent to connect",
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"✘ Running workspace agent startup scripts (non-blocking)",
				"Warning: A startup script timed out and your workspace may be incomplete.",
			},
		},
		{
			name: "Initial connection timeout",
			opts: cliui.AgentOptions{
				FetchInterval: 1 * time.Millisecond,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnecting
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
					agent.StartedAt = new(time.Now())
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentTimeout
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
					agent.ReadyAt = new(time.Now())
					return nil
				},
			},
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
				"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
				"For more information and troubleshooting, see",
				"✔ Waiting for the workspace agent to connect",
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"✔ Running workspace agent startup scripts (non-blocking)",
			},
		},
		{
			name: "Disconnected",
			opts: cliui.AgentOptions{
				FetchInterval: 1 * time.Millisecond,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentDisconnected
					agent.FirstConnectedAt = new(time.Now().Add(-1 * time.Minute))
					agent.LastConnectedAt = new(time.Now().Add(-1 * time.Minute))
					agent.DisconnectedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
					agent.StartedAt = new(time.Now().Add(-1 * time.Minute))
					agent.ReadyAt = new(time.Now())
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ The workspace agent lost connection")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.DisconnectedAt = nil
					agent.LastConnectedAt = new(time.Now())
					return nil
				},
			},
			want: []string{
				"⧗ The workspace agent lost connection",
				"Wait for it to reconnect or restart your workspace.",
				"For more information and troubleshooting, see",
				"✔ The workspace agent lost connection",
			},
		},
		{
			name: "Startup Logs",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          true,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
					agent.StartedAt = new(time.Now())
					agent.LogSources = []codersdk.WorkspaceAgentLogSource{{
						ID:          uuid.Nil,
						DisplayName: "testing",
					}}
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Hello world",
							SourceID:  uuid.Nil,
						},
					}
					return nil
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
					agent.ReadyAt = new(time.Now())
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Bye now",
						},
					}
					return nil
				},
			},
			want: []string{
				"⧗ Running workspace agent startup scripts",
				"ℹ︎ To connect immediately, reconnect with --wait=no or CODER_SSH_WAIT=no, see --help for more information.",
				"testing: Hello world",
				"Bye now",
				"✔ Running workspace agent startup scripts",
			},
		},
		{
			name: "Startup script exited with error",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          true,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.StartedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
					agent.ReadyAt = new(time.Now())
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Hello world",
						},
					}
					return nil
				},
			},
			want: []string{
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"Hello world",
				"✘ Running workspace agent startup scripts (non-blocking)",
				"Warning: A startup script exited with an error and your workspace may be incomplete.",
				"For more information and troubleshooting, see",
			},
		},
		{
			// Verify that in non-blocking mode (Wait=false), startup script
			// logs are suppressed. This prevents dumping a wall of logs on
			// users who explicitly pass --wait=no. See issue #13580.
			name: "No logs in non-blocking mode",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          false,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.StartedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
					agent.ReadyAt = new(time.Now())
					// These logs should NOT be shown in non-blocking mode.
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Startup script log 1",
						},
						{
							CreatedAt: time.Now(),
							Output:    "Startup script log 2",
						},
					}
					return nil
				},
			},
			// Note: Log content like "Startup script log 1" should NOT appear here.
			want: []string{
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"✘ Running workspace agent startup scripts (non-blocking)",
				"Warning: A startup script exited with an error and your workspace may be incomplete.",
				"For more information and troubleshooting, see",
			},
		},
		{
			// Verify that even after waiting for the agent to connect, logs
			// are still suppressed in non-blocking mode. See issue #13580.
			name: "No logs after connection wait in non-blocking mode",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          false,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnecting
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.StartedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
					agent.ReadyAt = new(time.Now())
					// These logs should NOT be shown in non-blocking mode,
					// even though we waited for connection.
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Startup script log 1",
						},
					}
					return nil
				},
			},
			// Note: Log content should NOT appear here despite waiting for connection.
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
				"✔ Waiting for the workspace agent to connect",
				"⧗ Running workspace agent startup scripts (non-blocking)",
				"✘ Running workspace agent startup scripts (non-blocking)",
				"Warning: A startup script exited with an error and your workspace may be incomplete.",
				"For more information and troubleshooting, see",
			},
		},
		{
			name: "Error when shutting down",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentDisconnected
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleOff
					return nil
				},
			},
			wantErr: true,
		},
		{
			name: "Error when shutting down while waiting",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          true,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnected
					agent.FirstConnectedAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
					agent.StartedAt = new(time.Now())
					logs <- []codersdk.WorkspaceAgentLog{
						{
							CreatedAt: time.Now(),
							Output:    "Hello world",
						},
					}
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "Hello world")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.ReadyAt = new(time.Now())
					agent.LifecycleState = codersdk.WorkspaceAgentLifecycleShuttingDown
					return nil
				},
			},
			want: []string{
				"⧗ Running workspace agent startup scripts",
				"ℹ︎ To connect immediately, reconnect with --wait=no or CODER_SSH_WAIT=no, see --help for more information.",
				"Hello world",
				"✔ Running workspace agent startup scripts",
			},
			wantErr: true,
		},
		{
			name: "Error during fetch",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          true,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentConnecting
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return xerrors.New("bad")
				},
			},
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
			},
			wantErr: true,
		},
		{
			name: "Shows agent troubleshooting URL",
			opts: cliui.AgentOptions{
				FetchInterval: time.Millisecond,
				Wait:          true,
			},
			iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					agent.Status = codersdk.WorkspaceAgentTimeout
					agent.TroubleshootingURL = "https://troubleshoot"
					return nil
				},
				func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return waitLines(t, output, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
				},
				func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
					return xerrors.New("bad")
				},
			},
			want: []string{
				"⧗ Waiting for the workspace agent to connect",
				"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
				"https://troubleshoot",
			},
			wantErr: true,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
			defer cancel()

			r, w, err := os.Pipe()
			require.NoError(t, err, "create pipe failed")
			defer r.Close()
			defer w.Close()

			agent := codersdk.WorkspaceAgent{
				ID:             uuid.New(),
				Status:         codersdk.WorkspaceAgentConnecting,
				CreatedAt:      time.Now(),
				LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
			}
			output := make(chan string, 100) // Buffered to avoid blocking, overflow is discarded.
			logs := make(chan []codersdk.WorkspaceAgentLog, 1)

			cmd := &serpent.Command{
				Handler: func(inv *serpent.Invocation) error {
					tc.opts.Fetch = func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
						t.Log("iter", len(tc.iter))
						var err error
						if len(tc.iter) > 0 {
							err = tc.iter[0](ctx, t, &agent, output, logs)
							tc.iter = tc.iter[1:]
						}
						return agent, err
					}
					tc.opts.FetchLogs = func(ctx context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
						if follow {
							return logs, closeFunc(func() error { return nil }), nil
						}

						fetchLogs := make(chan []codersdk.WorkspaceAgentLog, 1)
						select {
						case <-ctx.Done():
							return nil, nil, ctx.Err()
						case l := <-logs:
							fetchLogs <- l
						default:
						}
						close(fetchLogs)
						return fetchLogs, closeFunc(func() error { return nil }), nil
					}
					err := cliui.Agent(inv.Context(), w, uuid.Nil, tc.opts)
					_ = w.Close()
					return err
				},
			}
			inv := cmd.Invoke()

			waiter := clitest.StartWithWaiter(t, inv)

			s := bufio.NewScanner(r)
			for s.Scan() {
				line := s.Text()
				t.Log(line)
				select {
				case output <- line:
				default:
					t.Logf("output overflow: %s", line)
				}
				if len(tc.want) == 0 {
					require.Fail(t, "unexpected line", line)
				}
				require.Contains(t, line, tc.want[0])
				tc.want = tc.want[1:]
			}
			require.NoError(t, s.Err())
			if len(tc.want) > 0 {
				require.Fail(t, "missing lines: "+strings.Join(tc.want, ", "))
			}

			if tc.wantErr {
				waiter.RequireError()
			} else {
				waiter.RequireSuccess()
			}
		})
	}

	t.Run("NotInfinite", func(t *testing.T) {
		t.Parallel()
		var fetchCalled atomic.Uint64

		cmd := &serpent.Command{
			Handler: func(inv *serpent.Invocation) error {
				buf := bytes.Buffer{}
				err := cliui.Agent(inv.Context(), &buf, uuid.Nil, cliui.AgentOptions{
					FetchInterval: 10 * time.Millisecond,
					Fetch: func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error) {
						fetchCalled.Add(1)

						return codersdk.WorkspaceAgent{
							Status:         codersdk.WorkspaceAgentConnected,
							LifecycleState: codersdk.WorkspaceAgentLifecycleReady,
						}, nil
					},
				})
				if err != nil {
					return err
				}

				require.Never(t, func() bool {
					called := fetchCalled.Load()
					return called > 5 || called == 0
				}, time.Second, 100*time.Millisecond)

				return nil
			},
		}
		require.NoError(t, cmd.Invoke().Run())
	})

	t.Run("ContextCancelDuringLogStreaming", func(t *testing.T) {
		t.Parallel()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		agent := codersdk.WorkspaceAgent{
			ID:               uuid.New(),
			Status:           codersdk.WorkspaceAgentConnected,
			FirstConnectedAt: new(time.Now()),
			CreatedAt:        time.Now(),
			LifecycleState:   codersdk.WorkspaceAgentLifecycleStarting,
			StartedAt:        new(time.Now()),
		}

		logs := make(chan []codersdk.WorkspaceAgentLog, 1)
		logStreamStarted := make(chan struct{})

		cmd := &serpent.Command{
			Handler: func(inv *serpent.Invocation) error {
				return cliui.Agent(inv.Context(), io.Discard, agent.ID, cliui.AgentOptions{
					FetchInterval: time.Millisecond,
					Wait:          true,
					Fetch: func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
						return agent, nil
					},
					FetchLogs: func(_ context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
						// Signal that log streaming has started.
						select {
						case <-logStreamStarted:
						default:
							close(logStreamStarted)
						}
						return logs, closeFunc(func() error { return nil }), nil
					},
				})
			},
		}

		inv := cmd.Invoke().WithContext(ctx)
		done := make(chan error, 1)
		go func() {
			done <- inv.Run()
		}()

		// Wait for log streaming to start.
		select {
		case <-logStreamStarted:
		case <-time.After(testutil.WaitShort):
			t.Fatal("timed out waiting for log streaming to start")
		}

		// Cancel the context while streaming logs.
		cancel()

		// Verify that the agent function returns with a context error.
		select {
		case err := <-done:
			require.ErrorIs(t, err, context.Canceled)
		case <-time.After(testutil.WaitShort):
			t.Fatal("timed out waiting for agent to return after context cancellation")
		}
	})
}

func TestPeerDiagnostics(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name  string
		diags tailnet.PeerDiagnostics
		want  []*regexp.Regexp // must be ordered, can omit lines
	}{
		{
			name: "noPreferredDERP",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        make(map[int]string),
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Now(),
			},
			want: []*regexp.Regexp{
				regexp.MustCompile("^✘ not connected to DERP$"),
			},
		},
		{
			name: "preferredDERP",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP: 23,
				DERPRegionNames: map[int]string{
					23: "testo",
				},
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Now(),
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ preferred DERP region: 23 \(testo\)$`),
			},
		},
		{
			name: "sentNode",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ sent local data to Coder networking coordinator$`),
			},
		},
		{
			name: "didntSendNode",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               false,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✘ have not sent local data to Coder networking coordinator$`),
			},
		},
		{
			name: "receivedNodeDERPOKNoEndpoints",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{999: "Embedded"},
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
				regexp.MustCompile(`preferred DERP region: 999 \(Embedded\)$`),
				regexp.MustCompile(`endpoints: $`),
			},
		},
		{
			name: "receivedNodeDERPUnknownNoEndpoints",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{DERP: "127.3.3.40:999"},
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
				regexp.MustCompile(`preferred DERP region: 999 \(unknown\)$`),
				regexp.MustCompile(`endpoints: $`),
			},
		},
		{
			name: "receivedNodeEndpointsNoDERP",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{999: "Embedded"},
				SentNode:               true,
				ReceivedNode:           &tailcfg.Node{Endpoints: []string{"99.88.77.66:4555", "33.22.11.0:3444"}},
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
				regexp.MustCompile(`preferred DERP region:\s*$`),
				regexp.MustCompile(`endpoints: 99\.88\.77\.66:4555, 33\.22\.11\.0:3444$`),
			},
		},
		{
			name: "didntReceiveNode",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               false,
				ReceivedNode:           nil,
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✘ have not received remote agent data from Coder networking coordinator$`),
			},
		},
		{
			name: "noWireguardHandshake",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               false,
				ReceivedNode:           nil,
				LastWireguardHandshake: time.Time{},
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✘ Wireguard is not connected$`),
			},
		},
		{
			name: "wireguardHandshakeRecent",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               false,
				ReceivedNode:           nil,
				LastWireguardHandshake: time.Now().Add(-5 * time.Second),
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^✔ Wireguard handshake \d+s ago$`),
			},
		},
		{
			name: "wireguardHandshakeOld",
			diags: tailnet.PeerDiagnostics{
				PreferredDERP:          0,
				DERPRegionNames:        map[int]string{},
				SentNode:               false,
				ReceivedNode:           nil,
				LastWireguardHandshake: time.Now().Add(-450 * time.Second), // 7m30s
			},
			want: []*regexp.Regexp{
				regexp.MustCompile(`^⚠ Wireguard handshake 7m\d+s ago$`),
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			r, w := io.Pipe()
			go func() {
				defer w.Close()
				cliui.PeerDiagnostics(w, tc.diags)
			}()
			s := bufio.NewScanner(r)
			i := 0
			got := make([]string, 0)
			for s.Scan() {
				got = append(got, s.Text())
				if i < len(tc.want) {
					reg := tc.want[i]
					if reg.Match(s.Bytes()) {
						i++
					}
				}
			}
			if i < len(tc.want) {
				t.Logf("failed to match regexp: %s\ngot:\n%s", tc.want[i].String(), strings.Join(got, "\n"))
				t.FailNow()
			}
		})
	}
}

func TestConnDiagnostics(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name  string
		diags cliui.ConnDiags
		want  []string
	}{
		{
			name: "DirectBlocked",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap:                  &tailcfg.DERPMap{},
					DisableDirectConnections: true,
				},
			},
			want: []string{
				`❗ Your Coder administrator has blocked direct connections`,
			},
		},
		{
			name: "NoStun",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				LocalNetInfo: &tailcfg.NetInfo{},
			},
			want: []string{
				`The DERP map is not configured to use STUN`,
			},
		},
		{
			name: "ClientHasStunNoUDP",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{
						Regions: map[int]*tailcfg.DERPRegion{
							999: {
								Nodes: []*tailcfg.DERPNode{
									{
										STUNPort: 1337,
									},
								},
							},
						},
					},
				},
				LocalNetInfo: &tailcfg.NetInfo{
					UDP: false,
				},
			},
			want: []string{
				`Client could not connect to STUN over UDP`,
			},
		},
		{
			name: "AgentHasStunNoUDP",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{
						Regions: map[int]*tailcfg.DERPRegion{
							999: {
								Nodes: []*tailcfg.DERPNode{
									{
										STUNPort: 1337,
									},
								},
							},
						},
					},
				},
				AgentNetcheck: &healthsdk.AgentNetcheckReport{
					NetInfo: &tailcfg.NetInfo{
						UDP: false,
					},
				},
			},
			want: []string{
				`Agent could not connect to STUN over UDP`,
			},
		},
		{
			name: "ClientHardNat",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				LocalNetInfo: &tailcfg.NetInfo{
					MappingVariesByDestIP: "true",
				},
			},
			want: []string{
				`Client is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers`,
			},
		},
		{
			name: "AgentHardNat",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				LocalNetInfo: &tailcfg.NetInfo{},
				AgentNetcheck: &healthsdk.AgentNetcheckReport{
					NetInfo: &tailcfg.NetInfo{MappingVariesByDestIP: "true"},
				},
			},
			want: []string{
				`Agent is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers`,
			},
		},
		{
			name: "AgentInterfaceWarnings",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				AgentNetcheck: &healthsdk.AgentNetcheckReport{
					Interfaces: healthsdk.InterfacesReport{
						BaseReport: healthsdk.BaseReport{
							Warnings: []health.Message{
								health.Messagef(health.CodeInterfaceSmallMTU, "Network interface eth0 has MTU 1280, (less than 1378), which may degrade the quality of direct connections"),
							},
						},
					},
				},
			},
			want: []string{
				`Network interface eth0 has MTU 1280, (less than 1378), which may degrade the quality of direct connections`,
			},
		},
		{
			name: "LocalInterfaceWarnings",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				LocalInterfaces: &healthsdk.InterfacesReport{
					BaseReport: healthsdk.BaseReport{
						Warnings: []health.Message{
							health.Messagef(health.CodeInterfaceSmallMTU, "Network interface eth1 has MTU 1310, (less than 1378), which may degrade the quality of direct connections"),
						},
					},
				},
			},
			want: []string{
				`Network interface eth1 has MTU 1310, (less than 1378), which may degrade the quality of direct connections`,
			},
		},
		{
			name: "ClientAWSIP",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				ClientIPIsAWS: true,
				AgentIPIsAWS:  false,
			},
			want: []string{
				`Client IP address is within an AWS range (AWS uses hard NAT)`,
			},
		},
		{
			name: "AgentAWSIP",
			diags: cliui.ConnDiags{
				ConnInfo: workspacesdk.AgentConnectionInfo{
					DERPMap: &tailcfg.DERPMap{},
				},
				ClientIPIsAWS: false,
				AgentIPIsAWS:  true,
			},
			want: []string{
				`Agent IP address is within an AWS range (AWS uses hard NAT)`,
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			r, w := io.Pipe()
			go func() {
				defer w.Close()
				tc.diags.Write(w)
			}()
			bytes, err := io.ReadAll(r)
			require.NoError(t, err)
			output := string(bytes)
			for _, want := range tc.want {
				require.Contains(t, output, want)
			}
		})
	}
}

func TestGetProgressiveInterval(t *testing.T) {
	t.Parallel()

	baseInterval := 500 * time.Millisecond

	testCases := []struct {
		name     string
		elapsed  time.Duration
		expected time.Duration
	}{
		{"first_minute", 30 * time.Second, baseInterval},
		{"second_minute", 90 * time.Second, baseInterval * 2},
		{"third_to_fifth_minute", 3 * time.Minute, baseInterval * 4},
		{"sixth_to_tenth_minute", 7 * time.Minute, baseInterval * 8},
		{"after_ten_minutes", 15 * time.Minute, baseInterval * 16},
		{"boundary_first_minute", 59 * time.Second, baseInterval},
		{"boundary_second_minute", 61 * time.Second, baseInterval * 2},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			result := cliui.GetProgressiveInterval(baseInterval, tc.elapsed)
			require.Equal(t, tc.expected, result)
		})
	}
}