diff --git a/cli/support.go b/cli/support.go index ac0e2041cb6ae..ab6687e442967 100644 --- a/cli/support.go +++ b/cli/support.go @@ -137,15 +137,19 @@ func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*coders } func writeBundle(src *support.Bundle, dest *zip.Writer) error { + // We JSON-encode the following: for k, v := range map[string]any{ "deployment/buildinfo.json": src.Deployment.BuildInfo, "deployment/config.json": src.Deployment.Config, "deployment/experiments.json": src.Deployment.Experiments, "deployment/health.json": src.Deployment.HealthReport, - "network/netcheck_local.json": src.Network.NetcheckLocal, - "network/netcheck_remote.json": src.Network.NetcheckRemote, + "network/netcheck.json": src.Network.Netcheck, "workspace/workspace.json": src.Workspace.Workspace, - "workspace/agent.json": src.Workspace.Agent, + "agent/agent.json": src.Agent.Agent, + "agent/listening_ports.json": src.Agent.ListeningPorts, + "agent/manifest.json": src.Agent.Manifest, + "agent/peer_diagnostics.json": src.Agent.PeerDiagnostics, + "agent/ping_result.json": src.Agent.PingResult, "workspace/template.json": src.Workspace.Template, "workspace/template_version.json": src.Workspace.TemplateVersion, "workspace/parameters.json": src.Workspace.Parameters, @@ -166,13 +170,16 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error { return xerrors.Errorf("decode template zip from base64") } + // The below we just write as we have them: for k, v := range map[string]string{ - "network/coordinator_debug.html": src.Network.CoordinatorDebug, - "network/tailnet_debug.html": src.Network.TailnetDebug, - "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), - "workspace/agent_startup_logs.txt": humanizeAgentLogs(src.Workspace.AgentStartupLogs), - "workspace/template_file.zip": string(templateVersionBytes), - "logs.txt": strings.Join(src.Logs, "\n"), + "network/coordinator_debug.html": src.Network.CoordinatorDebug, + "network/tailnet_debug.html": src.Network.TailnetDebug, + "workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs), + "agent/logs.txt": string(src.Agent.Logs), + "agent/magicsock.html": string(src.Agent.MagicsockHTML), + "agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs), + "workspace/template_file.zip": string(templateVersionBytes), + "logs.txt": strings.Join(src.Logs, "\n"), } { f, err := dest.Create(k) if err != nil { diff --git a/cli/support_test.go b/cli/support_test.go index b36d17ad7c695..b19cbaebc57f9 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -4,19 +4,26 @@ import ( "archive/zip" "encoding/json" "io" + "os" "path/filepath" "runtime" "testing" "time" + "tailscale.com/ipn/ipnstate" + "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/testutil" ) @@ -37,7 +44,14 @@ func TestSupportBundle(t *testing.T) { }).WithAgent().Do() ws, err := client.Workspace(ctx, r.Workspace.ID) require.NoError(t, err) - agt := ws.LatestBuild.Resources[0].Agents[0] + tempDir := t.TempDir() + logPath := filepath.Join(tempDir, "coder-agent.log") + require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600)) + agt := agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { + o.LogDir = tempDir + }) + defer agt.Close() + coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() // Insert a provisioner job log _, err = db.InsertProvisionerJobLogs(ctx, database.InsertProvisionerJobLogsParams{ @@ -51,7 +65,7 @@ func TestSupportBundle(t *testing.T) { require.NoError(t, err) // Insert an agent log _, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{ - AgentID: agt.ID, + AgentID: ws.LatestBuild.Resources[0].Agents[0].ID, CreatedAt: dbtime.Now(), Output: []string{"started up"}, Level: []database.LogLevel{database.LogLevelInfo}, @@ -141,10 +155,10 @@ func assertBundleContents(t *testing.T, path string) { case "network/tailnet_debug.html": bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "tailnet debug should not be empty") - case "network/netcheck_local.json", "network/netcheck_remote.json": - // TODO: setup fake agent? - bs := readBytesFromZip(t, f) - require.NotEmpty(t, bs, "netcheck should not be empty") + case "network/netcheck.json": + var v codersdk.WorkspaceAgentConnectionInfo + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "connection info should not be empty") case "workspace/workspace.json": var v codersdk.Workspace decodeJSONFromZip(t, f, &v) @@ -152,11 +166,33 @@ func assertBundleContents(t *testing.T, path string) { case "workspace/build_logs.txt": bs := readBytesFromZip(t, f) require.Contains(t, string(bs), "provision done") - case "workspace/agent.json": + case "agent/agent.json": var v codersdk.WorkspaceAgent decodeJSONFromZip(t, f, &v) require.NotEmpty(t, v, "agent should not be empty") - case "workspace/agent_startup_logs.txt": + case "agent/listening_ports.json": + var v codersdk.WorkspaceAgentListeningPortsResponse + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "agent listening ports should not be empty") + case "agent/logs.txt": + bs := readBytesFromZip(t, f) + require.NotEmpty(t, bs, "logs should not be empty") + case "agent/magicsock.html": + bs := readBytesFromZip(t, f) + require.NotEmpty(t, bs, "agent magicsock should not be empty") + case "agent/manifest.json": + var v agentsdk.Manifest + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "agent manifest should not be empty") + case "agent/peer_diagnostics.json": + var v *tailnet.PeerDiagnostics + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "peer diagnostics should not be empty") + case "agent/ping_result.json": + var v *ipnstate.PingResult + decodeJSONFromZip(t, f, &v) + require.NotEmpty(t, v, "ping result should not be empty") + case "agent/startup_logs.txt": bs := readBytesFromZip(t, f) require.Contains(t, string(bs), "started up") case "workspace/template.json": @@ -178,7 +214,7 @@ func assertBundleContents(t *testing.T, path string) { bs := readBytesFromZip(t, f) require.NotEmpty(t, bs, "logs should not be empty") default: - require.Failf(t, "unexpected file in bundle: %q", f.Name) + require.Failf(t, "unexpected file in bundle", f.Name) } } } diff --git a/support/support.go b/support/support.go index 30aa77eef2ff7..2a2c3c55f8a2c 100644 --- a/support/support.go +++ b/support/support.go @@ -1,14 +1,17 @@ package support import ( + "bytes" "context" "encoding/base64" + "encoding/json" "io" "net/http" "strings" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" + "tailscale.com/ipn/ipnstate" "github.com/google/uuid" @@ -16,6 +19,8 @@ import ( "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/tailnet" ) // Bundle is a set of information discovered about a deployment. @@ -25,6 +30,7 @@ type Bundle struct { Deployment Deployment `json:"deployment"` Network Network `json:"network"` Workspace Workspace `json:"workspace"` + Agent Agent `json:"agent"` Logs []string `json:"logs"` } @@ -38,8 +44,7 @@ type Deployment struct { type Network struct { CoordinatorDebug string `json:"coordinator_debug"` TailnetDebug string `json:"tailnet_debug"` - NetcheckLocal *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_local"` - NetcheckRemote *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck_remote"` + Netcheck *codersdk.WorkspaceAgentConnectionInfo `json:"netcheck"` } type Workspace struct { @@ -49,8 +54,17 @@ type Workspace struct { TemplateVersion codersdk.TemplateVersion `json:"template_version"` TemplateFileBase64 string `json:"template_file_base64"` BuildLogs []codersdk.ProvisionerJobLog `json:"build_logs"` - Agent codersdk.WorkspaceAgent `json:"agent"` - AgentStartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` +} + +type Agent struct { + Agent *codersdk.WorkspaceAgent `json:"agent"` + ListeningPorts *codersdk.WorkspaceAgentListeningPortsResponse `json:"listening_ports"` + Logs []byte `json:"logs"` + MagicsockHTML []byte `json:"magicsock_html"` + Manifest *agentsdk.Manifest `json:"manifest"` + PeerDiagnostics *tailnet.PeerDiagnostics `json:"peer_diagnostics"` + PingResult *ipnstate.PingResult `json:"ping_result"` + StartupLogs []codersdk.WorkspaceAgentLog `json:"startup_logs"` } // Deps is a set of dependencies for discovering information @@ -159,7 +173,7 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, if err != nil { return xerrors.Errorf("fetch agent conn info: %w", err) } - n.NetcheckLocal = &connInfo + n.Netcheck = &connInfo return nil }) @@ -170,7 +184,7 @@ func NetworkInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, return n } -func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID, agentID uuid.UUID) Workspace { +func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, workspaceID uuid.UUID) Workspace { var ( w Workspace eg errgroup.Group @@ -181,10 +195,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return w } - if agentID == uuid.Nil { - log.Error(ctx, "no agent id specified") - } - // dependency, cannot fetch concurrently ws, err := client.Workspace(ctx, workspaceID) if err != nil { @@ -198,15 +208,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger } w.Workspace = ws - eg.Go(func() error { - agt, err := client.WorkspaceAgent(ctx, agentID) - if err != nil { - return xerrors.Errorf("fetch workspace agent: %w", err) - } - w.Agent = agt - return nil - }) - eg.Go(func() error { buildLogCh, closer, err := client.WorkspaceBuildLogsAfter(ctx, ws.LatestBuild.ID, 0) if err != nil { @@ -221,24 +222,6 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return nil }) - eg.Go(func() error { - if len(w.Workspace.LatestBuild.Resources) == 0 { - log.Warn(ctx, "workspace build has no resources") - return nil - } - agentLogCh, closer, err := client.WorkspaceAgentLogsAfter(ctx, agentID, 0, false) - if err != nil { - return xerrors.Errorf("fetch agent startup logs: %w", err) - } - defer closer.Close() - var logs []codersdk.WorkspaceAgentLog - for logChunk := range agentLogCh { - logs = append(w.AgentStartupLogs, logChunk...) - } - w.AgentStartupLogs = logs - return nil - }) - eg.Go(func() error { if w.Workspace.TemplateActiveVersionID == uuid.Nil { return xerrors.Errorf("workspace has nil template active version id") @@ -296,6 +279,119 @@ func WorkspaceInfo(ctx context.Context, client *codersdk.Client, log slog.Logger return w } +func AgentInfo(ctx context.Context, client *codersdk.Client, log slog.Logger, agentID uuid.UUID) Agent { + var ( + a Agent + eg errgroup.Group + ) + + if agentID == uuid.Nil { + log.Error(ctx, "no agent id specified") + return a + } + + eg.Go(func() error { + agt, err := client.WorkspaceAgent(ctx, agentID) + if err != nil { + return xerrors.Errorf("fetch workspace agent: %w", err) + } + a.Agent = &agt + return nil + }) + + eg.Go(func() error { + agentLogCh, closer, err := client.WorkspaceAgentLogsAfter(ctx, agentID, 0, false) + if err != nil { + return xerrors.Errorf("fetch agent startup logs: %w", err) + } + defer closer.Close() + var logs []codersdk.WorkspaceAgentLog + for logChunk := range agentLogCh { + logs = append(logs, logChunk...) + } + a.StartupLogs = logs + return nil + }) + + conn, err := client.DialWorkspaceAgent(ctx, agentID, &codersdk.DialWorkspaceAgentOptions{ + Logger: log.Named("dial-agent"), + BlockEndpoints: false, + }) + if err != nil { + log.Error(ctx, "dial agent", slog.Error(err)) + } else { + defer func() { + if err := conn.Close(); err != nil { + log.Error(ctx, "failed to close agent connection", slog.Error(err)) + } + <-conn.Closed() + }() + if !conn.AwaitReachable(ctx) { + log.Error(ctx, "timed out waiting for agent") + } else { + eg.Go(func() error { + _, _, pingRes, err := conn.Ping(ctx) + if err != nil { + return xerrors.Errorf("ping agent: %w", err) + } + a.PingResult = pingRes + return nil + }) + + eg.Go(func() error { + pds := conn.GetPeerDiagnostics() + a.PeerDiagnostics = &pds + return nil + }) + + eg.Go(func() error { + msBytes, err := conn.DebugMagicsock(ctx) + if err != nil { + return xerrors.Errorf("get agent magicsock page: %w", err) + } + a.MagicsockHTML = msBytes + return nil + }) + + eg.Go(func() error { + manifestRes, err := conn.DebugManifest(ctx) + if err != nil { + return xerrors.Errorf("fetch manifest: %w", err) + } + if err := json.NewDecoder(bytes.NewReader(manifestRes)).Decode(&a.Manifest); err != nil { + return xerrors.Errorf("decode agent manifest: %w", err) + } + + return nil + }) + + eg.Go(func() error { + logBytes, err := conn.DebugLogs(ctx) + if err != nil { + return xerrors.Errorf("fetch coder agent logs: %w", err) + } + a.Logs = logBytes + return nil + }) + + eg.Go(func() error { + lps, err := conn.ListeningPorts(ctx) + if err != nil { + return xerrors.Errorf("get listening ports: %w", err) + } + a.ListeningPorts = &lps + return nil + }) + } + } + + if err := eg.Wait(); err != nil { + log.Error(ctx, "fetch agent information", slog.Error(err)) + } + + return a +} + // Run generates a support bundle with the given dependencies. func Run(ctx context.Context, d *Deps) (*Bundle, error) { var b Bundle @@ -337,7 +433,7 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { return nil }) eg.Go(func() error { - wi := WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID, d.AgentID) + wi := WorkspaceInfo(ctx, d.Client, d.Log, d.WorkspaceID) b.Workspace = wi return nil }) @@ -346,6 +442,11 @@ func Run(ctx context.Context, d *Deps) (*Bundle, error) { b.Network = ni return nil }) + eg.Go(func() error { + ai := AgentInfo(ctx, d.Client, d.Log, d.AgentID) + b.Agent = ai + return nil + }) _ = eg.Wait() diff --git a/support/support_test.go b/support/support_test.go index ce21bfe999c06..6876bafbc50c2 100644 --- a/support/support_test.go +++ b/support/support_test.go @@ -5,15 +5,20 @@ import ( "context" "io" "net/http" + "os" + "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/goleak" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent" + "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" @@ -24,6 +29,10 @@ import ( "github.com/coder/coder/v2/testutil" ) +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + func TestRun(t *testing.T) { t.Parallel() @@ -31,7 +40,7 @@ func TestRun(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo"} - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ DeploymentValues: cfg, Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), @@ -46,33 +55,38 @@ func TestRun(t *testing.T) { AgentID: agt.ID, }) require.NoError(t, err) - require.NotEmpty(t, bun) - require.NotEmpty(t, bun.Deployment.BuildInfo) - require.NotEmpty(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.Config.Options) + assertNotNilNotEmpty(t, bun, "bundle should be present") + assertNotNilNotEmpty(t, bun.Deployment.BuildInfo, "deployment build info should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config, "deployment config should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config.Options, "deployment config should be present") assertSanitizedDeploymentConfig(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.HealthReport) - require.NotEmpty(t, bun.Deployment.Experiments) - require.NotEmpty(t, bun.Network.CoordinatorDebug) - require.NotEmpty(t, bun.Network.TailnetDebug) - require.NotNil(t, bun.Network.NetcheckLocal) - require.NotNil(t, bun.Workspace.Workspace) + assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present") + assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present") + assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present") + assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present") + assertNotNilNotEmpty(t, bun.Network.Netcheck, "network netcheck should be present") + assertNotNilNotEmpty(t, bun.Workspace.Workspace, "workspace should be present") assertSanitizedWorkspace(t, bun.Workspace.Workspace) - require.NotEmpty(t, bun.Workspace.BuildLogs) - require.NotNil(t, bun.Workspace.Agent) - require.NotEmpty(t, bun.Workspace.AgentStartupLogs) - require.NotEmpty(t, bun.Workspace.Template) - require.NotEmpty(t, bun.Workspace.TemplateVersion) - require.NotEmpty(t, bun.Workspace.TemplateFileBase64) - require.NotNil(t, bun.Workspace.Parameters) - require.NotEmpty(t, bun.Logs) + assertNotNilNotEmpty(t, bun.Workspace.BuildLogs, "workspace build logs should be present") + assertNotNilNotEmpty(t, bun.Workspace.Template, "workspace template should be present") + assertNotNilNotEmpty(t, bun.Workspace.TemplateVersion, "workspace template version should be present") + assertNotNilNotEmpty(t, bun.Workspace.TemplateFileBase64, "workspace template file should be present") + require.NotNil(t, bun.Workspace.Parameters, "workspace parameters should be present") + assertNotNilNotEmpty(t, bun.Agent.Agent, "agent should be present") + assertNotNilNotEmpty(t, bun.Agent.ListeningPorts, "agent listening ports should be present") + assertNotNilNotEmpty(t, bun.Agent.Logs, "agent logs should be present") + assertNotNilNotEmpty(t, bun.Agent.MagicsockHTML, "agent magicsock should be present") + assertNotNilNotEmpty(t, bun.Agent.PeerDiagnostics, "agent peer diagnostics should be present") + assertNotNilNotEmpty(t, bun.Agent.PingResult, "agent ping result should be present") + assertNotNilNotEmpty(t, bun.Agent.StartupLogs, "agent startup logs should be present") + assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present") }) - t.Run("OK_NoAgent", func(t *testing.T) { + t.Run("OK_NoWorkspace", func(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo"} - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: cfg, Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), @@ -83,23 +97,24 @@ func TestRun(t *testing.T) { Log: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Named("bundle").Leveled(slog.LevelDebug), }) require.NoError(t, err) - require.NotEmpty(t, bun) - require.NotEmpty(t, bun.Deployment.BuildInfo) - require.NotEmpty(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.Config.Options) + assertNotNilNotEmpty(t, bun, "bundle should be present") + assertNotNilNotEmpty(t, bun.Deployment.BuildInfo, "deployment build info should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config, "deployment config should be present") + assertNotNilNotEmpty(t, bun.Deployment.Config.Options, "deployment config should be present") assertSanitizedDeploymentConfig(t, bun.Deployment.Config) - require.NotEmpty(t, bun.Deployment.HealthReport) - require.NotEmpty(t, bun.Deployment.Experiments) - require.NotEmpty(t, bun.Network.CoordinatorDebug) - require.NotEmpty(t, bun.Network.TailnetDebug) - require.NotNil(t, bun.Workspace) - assertSanitizedWorkspace(t, bun.Workspace.Workspace) - require.NotEmpty(t, bun.Logs) + assertNotNilNotEmpty(t, bun.Deployment.HealthReport, "deployment health report should be present") + assertNotNilNotEmpty(t, bun.Deployment.Experiments, "deployment experiments should be present") + assertNotNilNotEmpty(t, bun.Network.CoordinatorDebug, "network coordinator debug should be present") + assertNotNilNotEmpty(t, bun.Network.TailnetDebug, "network tailnet debug should be present") + assert.Empty(t, bun.Network.Netcheck, "did not expect netcheck to be present") + assert.Empty(t, bun.Workspace.Workspace, "did not expect workspace to be present") + assert.Empty(t, bun.Agent, "did not expect agent to be present") + assertNotNilNotEmpty(t, bun.Logs, "bundle logs should be present") }) t.Run("NoAuth", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), }) @@ -117,7 +132,7 @@ func TestRun(t *testing.T) { t.Run("MissingPrivilege", func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{ Logger: ptr.Ref(slog.Make(sloghuman.Sink(io.Discard))), }) @@ -200,5 +215,21 @@ func setupWorkspaceAndAgent(ctx context.Context, t *testing.T, client *codersdk. }) require.NoError(t, err) + tempDir := t.TempDir() + logPath := filepath.Join(tempDir, "coder-agent.log") + require.NoError(t, os.WriteFile(logPath, []byte("hello from the agent"), 0o600)) + _ = agenttest.New(t, client.URL, wbr.AgentToken, func(o *agent.Options) { + o.LogDir = tempDir + }) + coderdtest.NewWorkspaceAgentWaiter(t, client, wbr.Workspace.ID).Wait() + return ws, agt } + +func assertNotNilNotEmpty[T any](t *testing.T, v T, msg string) { + t.Helper() + + if assert.NotNil(t, v, msg+" but was nil") { + assert.NotEmpty(t, v, msg+" but was empty") + } +}