Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb7e0b2a

Browse files
feat(cli): add exp task logs (#19915)
Closescoder/internal#894
1 parent82bebc7 commitb7e0b2a

File tree

4 files changed

+280
-6
lines changed

4 files changed

+280
-6
lines changed

‎cli/exp_task.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
1313
returni.Command.HelpHandler(i)
1414
},
1515
Children: []*serpent.Command{
16-
r.taskList(),
1716
r.taskCreate(),
18-
r.taskStatus(),
1917
r.taskDelete(),
18+
r.taskList(),
19+
r.taskLogs(),
2020
r.taskSend(),
21+
r.taskStatus(),
2122
},
2223
}
2324
returncmd

‎cli/exp_task_logs.go‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/coder/serpent"
12+
)
13+
14+
func (r*RootCmd)taskLogs()*serpent.Command {
15+
formatter:=cliui.NewOutputFormatter(
16+
cliui.TableFormat(
17+
[]codersdk.TaskLogEntry{},
18+
[]string{
19+
"type",
20+
"content",
21+
},
22+
),
23+
cliui.JSONFormat(),
24+
)
25+
26+
cmd:=&serpent.Command{
27+
Use:"logs <task>",
28+
Short:"Show a task's logs",
29+
Middleware:serpent.Chain(
30+
serpent.RequireNArgs(1),
31+
),
32+
Handler:func(inv*serpent.Invocation)error {
33+
client,err:=r.InitClient(inv)
34+
iferr!=nil {
35+
returnerr
36+
}
37+
38+
var (
39+
ctx=inv.Context()
40+
exp=codersdk.NewExperimentalClient(client)
41+
task=inv.Args[0]
42+
taskID uuid.UUID
43+
)
44+
45+
ifid,err:=uuid.Parse(task);err==nil {
46+
taskID=id
47+
}else {
48+
ws,err:=namedWorkspace(ctx,client,task)
49+
iferr!=nil {
50+
returnxerrors.Errorf("resolve task %q: %w",task,err)
51+
}
52+
53+
taskID=ws.ID
54+
}
55+
56+
logs,err:=exp.TaskLogs(ctx,codersdk.Me,taskID)
57+
iferr!=nil {
58+
returnxerrors.Errorf("get task logs: %w",err)
59+
}
60+
61+
out,err:=formatter.Format(ctx,logs.Logs)
62+
iferr!=nil {
63+
returnxerrors.Errorf("format task logs: %w",err)
64+
}
65+
66+
_,_=fmt.Fprintln(inv.Stdout,out)
67+
returnnil
68+
},
69+
}
70+
71+
formatter.AttachOptions(&cmd.Options)
72+
returncmd
73+
}

‎cli/exp_task_logs_test.go‎

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
"time"
12+
13+
"github.com/google/uuid"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/coder/coder/v2/cli/clitest"
18+
"github.com/coder/coder/v2/coderd/httpapi"
19+
"github.com/coder/coder/v2/codersdk"
20+
"github.com/coder/coder/v2/testutil"
21+
)
22+
23+
funcTest_TaskLogs(t*testing.T) {
24+
t.Parallel()
25+
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+
expectTablestring
51+
expectLogs []codersdk.TaskLogEntry
52+
expectErrorstring
53+
handlerfunc(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+
returnfunc(w http.ResponseWriter,r*http.Request) {
60+
switchr.URL.Path {
61+
casefmt.Sprintf("/api/v2/users/me/workspace/%s",taskName):
62+
httpapi.Write(ctx,w,http.StatusOK, codersdk.Workspace{
63+
ID:taskID,
64+
})
65+
casefmt.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+
returnfunc(w http.ResponseWriter,r*http.Request) {
80+
switchr.URL.Path {
81+
casefmt.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+
returnfunc(w http.ResponseWriter,r*http.Request) {
99+
switchr.URL.Path {
100+
casefmt.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+
},
110+
{
111+
args: []string{"doesnotexist"},
112+
expectError:httpapi.ResourceNotFoundResponse.Message,
113+
handler:func(t*testing.T,ctx context.Context) http.HandlerFunc {
114+
returnfunc(w http.ResponseWriter,r*http.Request) {
115+
switchr.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+
},
123+
},
124+
{
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+
returnfunc(w http.ResponseWriter,r*http.Request) {
129+
switchr.URL.Path {
130+
casefmt.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+
},
137+
},
138+
{
139+
args: []string{"err-fetching-logs"},
140+
expectError:assert.AnError.Error(),
141+
handler:func(t*testing.T,ctx context.Context) http.HandlerFunc {
142+
returnfunc(w http.ResponseWriter,r*http.Request) {
143+
switchr.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+
casefmt.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+
},
155+
},
156+
}
157+
158+
for_,tt:=rangetests {
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+
errerror
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+
iftt.expectError=="" {
180+
assert.NoError(t,err)
181+
}else {
182+
assert.ErrorContains(t,err,tt.expectError)
183+
}
184+
185+
iftt.expectTable!="" {
186+
ifdiff:=tableDiff(tt.expectTable,stdout.String());diff!="" {
187+
t.Errorf("unexpected output diff (-want +got):\n%s",diff)
188+
}
189+
}
190+
191+
iftt.expectLogs!=nil {
192+
varlogs []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+
})
199+
}
200+
}

‎codersdk/aitasks.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ const (
261261
//
262262
// Experimental: This type is experimental and may change in the future.
263263
typeTaskLogEntrystruct {
264-
IDint`json:"id"`
265-
Contentstring`json:"content"`
266-
TypeTaskLogType`json:"type" enum:"input,output"`
267-
Time time.Time`json:"time" format:"date-time"`
264+
IDint`json:"id" table:"id"`
265+
Contentstring`json:"content" table:"content"`
266+
TypeTaskLogType`json:"type" enum:"input,output" table:"type"`
267+
Time time.Time`json:"time" format:"date-time" table:"time,default_sort"`
268268
}
269269

270270
// TaskLogsResponse contains the logs for a task.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp