11package cli_test
22
33import (
4- "context"
54"encoding/json"
6- "fmt"
75"net/http"
8- "net/http/httptest"
96"strings"
107"testing"
118"time"
@@ -14,7 +11,10 @@ import (
1411"github.com/stretchr/testify/assert"
1512"github.com/stretchr/testify/require"
1613
14+ agentapisdk"github.com/coder/agentapi-sdk-go"
15+
1716"github.com/coder/coder/v2/cli/clitest"
17+ "github.com/coder/coder/v2/coderd/coderdtest"
1818"github.com/coder/coder/v2/coderd/httpapi"
1919"github.com/coder/coder/v2/codersdk"
2020"github.com/coder/coder/v2/testutil"
@@ -23,178 +23,165 @@ import (
2323func Test_TaskLogs (t * testing.T ) {
2424t .Parallel ()
2525
26- var (
27- clock = time .Date (2025 ,8 ,26 ,12 ,34 ,56 ,0 ,time .UTC )
28-
29- taskID = uuid .MustParse ("11111111-1111-1111-1111-111111111111" )
30- taskName = "task-workspace"
31-
32- taskLogs = []codersdk.TaskLogEntry {
33- {
34- ID :0 ,
35- Content :"What is 1 + 1?" ,
36- Type :codersdk .TaskLogTypeInput ,
37- Time :clock ,
38- },
39- {
40- ID :1 ,
41- Content :"2" ,
42- Type :codersdk .TaskLogTypeOutput ,
43- Time :clock .Add (1 * time .Second ),
44- },
45- }
46- )
47-
48- tests := []struct {
49- args []string
50- expectTable string
51- expectLogs []codersdk.TaskLogEntry
52- expectError string
53- handler func (t * testing.T ,ctx context.Context ) http.HandlerFunc
54- }{
55- {
56- args : []string {taskName ,"--output" ,"json" },
57- expectLogs :taskLogs ,
58- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
59- return func (w http.ResponseWriter ,r * http.Request ) {
60- switch r .URL .Path {
61- case fmt .Sprintf ("/api/v2/users/me/workspace/%s" ,taskName ):
62- httpapi .Write (ctx ,w ,http .StatusOK , codersdk.Workspace {
63- ID :taskID ,
64- })
65- case fmt .Sprintf ("/api/experimental/tasks/me/%s/logs" ,taskID .String ()):
66- httpapi .Write (ctx ,w ,http .StatusOK , codersdk.TaskLogsResponse {
67- Logs :taskLogs ,
68- })
69- default :
70- t .Errorf ("unexpected path: %s" ,r .URL .Path )
71- }
72- }
73- },
74- },
75- {
76- args : []string {taskID .String (),"--output" ,"json" },
77- expectLogs :taskLogs ,
78- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
79- return func (w http.ResponseWriter ,r * http.Request ) {
80- switch r .URL .Path {
81- case fmt .Sprintf ("/api/experimental/tasks/me/%s/logs" ,taskID .String ()):
82- httpapi .Write (ctx ,w ,http .StatusOK , codersdk.TaskLogsResponse {
83- Logs :taskLogs ,
84- })
85- default :
86- t .Errorf ("unexpected path: %s" ,r .URL .Path )
87- }
88- }
89- },
90- },
91- {
92- args : []string {taskID .String ()},
93- expectTable :`
94- TYPE CONTENT
95- input What is 1 + 1?
96- output 2` ,
97- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
98- return func (w http.ResponseWriter ,r * http.Request ) {
99- switch r .URL .Path {
100- case fmt .Sprintf ("/api/experimental/tasks/me/%s/logs" ,taskID .String ()):
101- httpapi .Write (ctx ,w ,http .StatusOK , codersdk.TaskLogsResponse {
102- Logs :taskLogs ,
103- })
104- default :
105- t .Errorf ("unexpected path: %s" ,r .URL .Path )
106- }
107- }
108- },
109- },
26+ testMessages := []agentapisdk.Message {
11027{
111- args : []string {"doesnotexist" },
112- expectError :httpapi .ResourceNotFoundResponse .Message ,
113- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
114- return func (w http.ResponseWriter ,r * http.Request ) {
115- switch r .URL .Path {
116- case "/api/v2/users/me/workspace/doesnotexist" :
117- httpapi .ResourceNotFound (w )
118- default :
119- t .Errorf ("unexpected path: %s" ,r .URL .Path )
120- }
121- }
122- },
28+ Id :0 ,
29+ Role :agentapisdk .RoleUser ,
30+ Content :"What is 1 + 1?" ,
31+ Time :time .Now ().Add (- 2 * time .Minute ),
12332},
12433{
125- args : []string {uuid .Nil .String ()},// uuid does not exist
126- expectError :httpapi .ResourceNotFoundResponse .Message ,
127- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
128- return func (w http.ResponseWriter ,r * http.Request ) {
129- switch r .URL .Path {
130- case fmt .Sprintf ("/api/experimental/tasks/me/%s/logs" ,uuid .Nil .String ()):
131- httpapi .ResourceNotFound (w )
132- default :
133- t .Errorf ("unexpected path: %s" ,r .URL .Path )
134- }
135- }
136- },
34+ Id :1 ,
35+ Role :agentapisdk .RoleAgent ,
36+ Content :"2" ,
37+ Time :time .Now ().Add (- 1 * time .Minute ),
13738},
138- {
139- args : []string {"err-fetching-logs" },
140- expectError :assert .AnError .Error (),
141- handler :func (t * testing.T ,ctx context.Context ) http.HandlerFunc {
142- return func (w http.ResponseWriter ,r * http.Request ) {
143- switch r .URL .Path {
144- case "/api/v2/users/me/workspace/err-fetching-logs" :
145- httpapi .Write (ctx ,w ,http .StatusOK , codersdk.Workspace {
146- ID :taskID ,
147- })
148- case fmt .Sprintf ("/api/experimental/tasks/me/%s/logs" ,taskID .String ()):
149- httpapi .InternalServerError (w ,assert .AnError )
150- default :
151- t .Errorf ("unexpected path: %s" ,r .URL .Path )
152- }
153- }
154- },
39+ }
40+
41+ t .Run ("ByWorkspaceName_JSON" ,func (t * testing.T ) {
42+ t .Parallel ()
43+ ctx := testutil .Context (t ,testutil .WaitLong )
44+
45+ client ,workspace := setupCLITaskTest (ctx ,t ,fakeAgentAPITaskLogsOK (testMessages ))
46+ userClient := client // user already has access to their own workspace
47+
48+ var stdout strings.Builder
49+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,workspace .Name ,"--output" ,"json" )
50+ inv .Stdout = & stdout
51+ clitest .SetupConfig (t ,userClient ,root )
52+
53+ err := inv .WithContext (ctx ).Run ()
54+ require .NoError (t ,err )
55+
56+ var logs []codersdk.TaskLogEntry
57+ err = json .NewDecoder (strings .NewReader (stdout .String ())).Decode (& logs )
58+ require .NoError (t ,err )
59+
60+ require .Len (t ,logs ,2 )
61+ require .Equal (t ,"What is 1 + 1?" ,logs [0 ].Content )
62+ require .Equal (t ,codersdk .TaskLogTypeInput ,logs [0 ].Type )
63+ require .Equal (t ,"2" ,logs [1 ].Content )
64+ require .Equal (t ,codersdk .TaskLogTypeOutput ,logs [1 ].Type )
65+ })
66+
67+ t .Run ("ByWorkspaceID_JSON" ,func (t * testing.T ) {
68+ t .Parallel ()
69+ ctx := testutil .Context (t ,testutil .WaitLong )
70+
71+ client ,workspace := setupCLITaskTest (ctx ,t ,fakeAgentAPITaskLogsOK (testMessages ))
72+ userClient := client
73+
74+ var stdout strings.Builder
75+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,workspace .ID .String (),"--output" ,"json" )
76+ inv .Stdout = & stdout
77+ clitest .SetupConfig (t ,userClient ,root )
78+
79+ err := inv .WithContext (ctx ).Run ()
80+ require .NoError (t ,err )
81+
82+ var logs []codersdk.TaskLogEntry
83+ err = json .NewDecoder (strings .NewReader (stdout .String ())).Decode (& logs )
84+ require .NoError (t ,err )
85+
86+ require .Len (t ,logs ,2 )
87+ require .Equal (t ,"What is 1 + 1?" ,logs [0 ].Content )
88+ require .Equal (t ,codersdk .TaskLogTypeInput ,logs [0 ].Type )
89+ require .Equal (t ,"2" ,logs [1 ].Content )
90+ require .Equal (t ,codersdk .TaskLogTypeOutput ,logs [1 ].Type )
91+ })
92+
93+ t .Run ("ByWorkspaceID_Table" ,func (t * testing.T ) {
94+ t .Parallel ()
95+ ctx := testutil .Context (t ,testutil .WaitLong )
96+
97+ client ,workspace := setupCLITaskTest (ctx ,t ,fakeAgentAPITaskLogsOK (testMessages ))
98+ userClient := client
99+
100+ var stdout strings.Builder
101+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,workspace .ID .String ())
102+ inv .Stdout = & stdout
103+ clitest .SetupConfig (t ,userClient ,root )
104+
105+ err := inv .WithContext (ctx ).Run ()
106+ require .NoError (t ,err )
107+
108+ output := stdout .String ()
109+ require .Contains (t ,output ,"What is 1 + 1?" )
110+ require .Contains (t ,output ,"2" )
111+ require .Contains (t ,output ,"input" )
112+ require .Contains (t ,output ,"output" )
113+ })
114+
115+ t .Run ("WorkspaceNotFound_ByName" ,func (t * testing.T ) {
116+ t .Parallel ()
117+ ctx := testutil .Context (t ,testutil .WaitLong )
118+
119+ client := coderdtest .New (t ,& coderdtest.Options {IncludeProvisionerDaemon :true })
120+ owner := coderdtest .CreateFirstUser (t ,client )
121+ userClient ,_ := coderdtest .CreateAnotherUser (t ,client ,owner .OrganizationID )
122+
123+ var stdout strings.Builder
124+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,"doesnotexist" )
125+ inv .Stdout = & stdout
126+ clitest .SetupConfig (t ,userClient ,root )
127+
128+ err := inv .WithContext (ctx ).Run ()
129+ require .Error (t ,err )
130+ require .ErrorContains (t ,err ,httpapi .ResourceNotFoundResponse .Message )
131+ })
132+
133+ t .Run ("WorkspaceNotFound_ByID" ,func (t * testing.T ) {
134+ t .Parallel ()
135+ ctx := testutil .Context (t ,testutil .WaitLong )
136+
137+ client := coderdtest .New (t ,& coderdtest.Options {IncludeProvisionerDaemon :true })
138+ owner := coderdtest .CreateFirstUser (t ,client )
139+ userClient ,_ := coderdtest .CreateAnotherUser (t ,client ,owner .OrganizationID )
140+
141+ var stdout strings.Builder
142+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,uuid .Nil .String ())
143+ inv .Stdout = & stdout
144+ clitest .SetupConfig (t ,userClient ,root )
145+
146+ err := inv .WithContext (ctx ).Run ()
147+ require .Error (t ,err )
148+ require .ErrorContains (t ,err ,httpapi .ResourceNotFoundResponse .Message )
149+ })
150+
151+ t .Run ("ErrorFetchingLogs" ,func (t * testing.T ) {
152+ t .Parallel ()
153+ ctx := testutil .Context (t ,testutil .WaitLong )
154+
155+ client ,workspace := setupCLITaskTest (ctx ,t ,fakeAgentAPITaskLogsErr (assert .AnError ))
156+ userClient := client
157+
158+ inv ,root := clitest .New (t ,"exp" ,"task" ,"logs" ,workspace .ID .String ())
159+ clitest .SetupConfig (t ,userClient ,root )
160+
161+ err := inv .WithContext (ctx ).Run ()
162+ require .ErrorContains (t ,err ,assert .AnError .Error ())
163+ })
164+ }
165+
166+ func fakeAgentAPITaskLogsOK (messages []agentapisdk.Message )map [string ]http.HandlerFunc {
167+ return map [string ]http.HandlerFunc {
168+ "/messages" :func (w http.ResponseWriter ,r * http.Request ) {
169+ w .Header ().Set ("Content-Type" ,"application/json" )
170+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
171+ "messages" :messages ,
172+ })
155173},
156174}
175+ }
157176
158- for _ ,tt := range tests {
159- t .Run (strings .Join (tt .args ,"," ),func (t * testing.T ) {
160- t .Parallel ()
161-
162- var (
163- ctx = testutil .Context (t ,testutil .WaitShort )
164- srv = httptest .NewServer (tt .handler (t ,ctx ))
165- client = codersdk .New (testutil .MustURL (t ,srv .URL ))
166- args = []string {"exp" ,"task" ,"logs" }
167- stdout strings.Builder
168- err error
169- )
170-
171- t .Cleanup (srv .Close )
172-
173- inv ,root := clitest .New (t ,append (args ,tt .args ... )... )
174- inv .Stdout = & stdout
175- inv .Stderr = & stdout
176- clitest .SetupConfig (t ,client ,root )
177-
178- err = inv .WithContext (ctx ).Run ()
179- if tt .expectError == "" {
180- assert .NoError (t ,err )
181- }else {
182- assert .ErrorContains (t ,err ,tt .expectError )
183- }
184-
185- if tt .expectTable != "" {
186- if diff := tableDiff (tt .expectTable ,stdout .String ());diff != "" {
187- t .Errorf ("unexpected output diff (-want +got):\n %s" ,diff )
188- }
189- }
190-
191- if tt .expectLogs != nil {
192- var logs []codersdk.TaskLogEntry
193- err = json .NewDecoder (strings .NewReader (stdout .String ())).Decode (& logs )
194- require .NoError (t ,err )
195-
196- assert .Equal (t ,tt .expectLogs ,logs )
197- }
198- })
177+ func fakeAgentAPITaskLogsErr (err error )map [string ]http.HandlerFunc {
178+ return map [string ]http.HandlerFunc {
179+ "/messages" :func (w http.ResponseWriter ,r * http.Request ) {
180+ w .WriteHeader (http .StatusInternalServerError )
181+ w .Header ().Set ("Content-Type" ,"application/json" )
182+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
183+ "error" :err .Error (),
184+ })
185+ },
199186}
200187}