@@ -1,11 +1,8 @@ package cli_test import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" Expand All @@ -14,7 +11,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" agentapisdk "github.com/coder/agentapi-sdk-go" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" Expand All @@ -23,178 +23,165 @@ import ( func Test_TaskLogs(t *testing.T) { t.Parallel() var ( clock = time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC) taskID = uuid.MustParse("11111111-1111-1111-1111-111111111111") taskName = "task-workspace" taskLogs = []codersdk.TaskLogEntry{ { ID: 0, Content: "What is 1 + 1?", Type: codersdk.TaskLogTypeInput, Time: clock, }, { ID: 1, Content: "2", Type: codersdk.TaskLogTypeOutput, Time: clock.Add(1 * time.Second), }, } ) tests := []struct { args []string expectTable string expectLogs []codersdk.TaskLogEntry expectError string handler func(t *testing.T, ctx context.Context) http.HandlerFunc }{ { args: []string{taskName, "--output", "json"}, expectLogs: taskLogs, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case fmt.Sprintf("/api/v2/users/me/workspace/%s", taskName): httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{ ID: taskID, }) case fmt.Sprintf("/api/experimental/tasks/me/%s/logs", taskID.String()): httpapi.Write(ctx, w, http.StatusOK, codersdk.TaskLogsResponse{ Logs: taskLogs, }) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, }, { args: []string{taskID.String(), "--output", "json"}, expectLogs: taskLogs, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case fmt.Sprintf("/api/experimental/tasks/me/%s/logs", taskID.String()): httpapi.Write(ctx, w, http.StatusOK, codersdk.TaskLogsResponse{ Logs: taskLogs, }) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, }, { args: []string{taskID.String()}, expectTable: ` TYPE CONTENT input What is 1 + 1? output 2`, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case fmt.Sprintf("/api/experimental/tasks/me/%s/logs", taskID.String()): httpapi.Write(ctx, w, http.StatusOK, codersdk.TaskLogsResponse{ Logs: taskLogs, }) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, }, testMessages := []agentapisdk.Message{ { args: []string{"doesnotexist"}, expectError: httpapi.ResourceNotFoundResponse.Message, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v2/users/me/workspace/doesnotexist": httpapi.ResourceNotFound(w) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, Id: 0, Role: agentapisdk.RoleUser, Content: "What is 1 + 1?", Time: time.Now().Add(-2 * time.Minute), }, { args: []string{uuid.Nil.String()}, // uuid does not exist expectError: httpapi.ResourceNotFoundResponse.Message, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case fmt.Sprintf("/api/experimental/tasks/me/%s/logs", uuid.Nil.String()): httpapi.ResourceNotFound(w) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, Id: 1, Role: agentapisdk.RoleAgent, Content: "2", Time: time.Now().Add(-1 * time.Minute), }, { args: []string{"err-fetching-logs"}, expectError: assert.AnError.Error(), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v2/users/me/workspace/err-fetching-logs": httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{ ID: taskID, }) case fmt.Sprintf("/api/experimental/tasks/me/%s/logs", taskID.String()): httpapi.InternalServerError(w, assert.AnError) default: t.Errorf("unexpected path: %s", r.URL.Path) } } }, } t.Run("ByWorkspaceName_JSON", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskLogsOK(testMessages)) userClient := client // user already has access to their own workspace var stdout strings.Builder inv, root := clitest.New(t, "exp", "task", "logs", workspace.Name, "--output", "json") inv.Stdout = &stdout clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.NoError(t, err) var logs []codersdk.TaskLogEntry err = json.NewDecoder(strings.NewReader(stdout.String())).Decode(&logs) require.NoError(t, err) require.Len(t, logs, 2) require.Equal(t, "What is 1 + 1?", logs[0].Content) require.Equal(t, codersdk.TaskLogTypeInput, logs[0].Type) require.Equal(t, "2", logs[1].Content) require.Equal(t, codersdk.TaskLogTypeOutput, logs[1].Type) }) t.Run("ByWorkspaceID_JSON", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskLogsOK(testMessages)) userClient := client var stdout strings.Builder inv, root := clitest.New(t, "exp", "task", "logs", workspace.ID.String(), "--output", "json") inv.Stdout = &stdout clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.NoError(t, err) var logs []codersdk.TaskLogEntry err = json.NewDecoder(strings.NewReader(stdout.String())).Decode(&logs) require.NoError(t, err) require.Len(t, logs, 2) require.Equal(t, "What is 1 + 1?", logs[0].Content) require.Equal(t, codersdk.TaskLogTypeInput, logs[0].Type) require.Equal(t, "2", logs[1].Content) require.Equal(t, codersdk.TaskLogTypeOutput, logs[1].Type) }) t.Run("ByWorkspaceID_Table", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskLogsOK(testMessages)) userClient := client var stdout strings.Builder inv, root := clitest.New(t, "exp", "task", "logs", workspace.ID.String()) inv.Stdout = &stdout clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.NoError(t, err) output := stdout.String() require.Contains(t, output, "What is 1 + 1?") require.Contains(t, output, "2") require.Contains(t, output, "input") require.Contains(t, output, "output") }) t.Run("WorkspaceNotFound_ByName", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) owner := coderdtest.CreateFirstUser(t, client) userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) var stdout strings.Builder inv, root := clitest.New(t, "exp", "task", "logs", "doesnotexist") inv.Stdout = &stdout clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.Error(t, err) require.ErrorContains(t, err, httpapi.ResourceNotFoundResponse.Message) }) t.Run("WorkspaceNotFound_ByID", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) owner := coderdtest.CreateFirstUser(t, client) userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) var stdout strings.Builder inv, root := clitest.New(t, "exp", "task", "logs", uuid.Nil.String()) inv.Stdout = &stdout clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.Error(t, err) require.ErrorContains(t, err, httpapi.ResourceNotFoundResponse.Message) }) t.Run("ErrorFetchingLogs", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskLogsErr(assert.AnError)) userClient := client inv, root := clitest.New(t, "exp", "task", "logs", workspace.ID.String()) clitest.SetupConfig(t, userClient, root) err := inv.WithContext(ctx).Run() require.ErrorContains(t, err, assert.AnError.Error()) }) } func fakeAgentAPITaskLogsOK(messages []agentapisdk.Message) map[string]http.HandlerFunc { return map[string]http.HandlerFunc{ "/messages": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "messages": messages, }) }, } } for _, tt := range tests { t.Run(strings.Join(tt.args, ","), func(t *testing.T) { t.Parallel() var ( ctx = testutil.Context(t, testutil.WaitShort) srv = httptest.NewServer(tt.handler(t, ctx)) client = codersdk.New(testutil.MustURL(t, srv.URL)) args = []string{"exp", "task", "logs"} stdout strings.Builder err error ) t.Cleanup(srv.Close) inv, root := clitest.New(t, append(args, tt.args...)...) inv.Stdout = &stdout inv.Stderr = &stdout clitest.SetupConfig(t, client, root) err = inv.WithContext(ctx).Run() if tt.expectError == "" { assert.NoError(t, err) } else { assert.ErrorContains(t, err, tt.expectError) } if tt.expectTable != "" { if diff := tableDiff(tt.expectTable, stdout.String()); diff != "" { t.Errorf("unexpected output diff (-want +got):\n%s", diff) } } if tt.expectLogs != nil { var logs []codersdk.TaskLogEntry err = json.NewDecoder(strings.NewReader(stdout.String())).Decode(&logs) require.NoError(t, err) assert.Equal(t, tt.expectLogs, logs) } }) func fakeAgentAPITaskLogsErr(err error) map[string]http.HandlerFunc { return map[string]http.HandlerFunc{ "/messages": func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "error": err.Error(), }) }, } }