chore(cli): add integration-style test for exp task logs and send (#20345)

Adds some coderd integration tests for `coder exp tasks (send|logs)`.
The actual agentapi interaction is faked out. I figure we don't want to
actually start a real agentapi instance here.

Authored by Claude with some manual cleanup.
This commit is contained in:
Cian Johnston
2025-10-17 12:28:45 +01:00
committed by GitHub
parent e8f0e3e4c5
commit 9ee3a9174c
3 changed files with 498 additions and 311 deletions
+146 -148
View File
@@ -1,173 +1,171 @@
package cli_test
import (
"context"
"fmt"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/google/uuid"
"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"
)
func Test_TaskSend(t *testing.T) {
t.Parallel()
var (
taskName = "task-workspace"
taskID = uuid.MustParse("11111111-1111-1111-1111-111111111111")
)
t.Run("ByWorkspaceName_WithArgument", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
tests := []struct {
args []string
stdin string
expectError string
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
}{
{
args: []string{taskName, "carry on with the task"},
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/send", taskID.String()):
var req codersdk.TaskSendRequest
if !httpapi.Read(ctx, w, r, &req) {
return
}
client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
assert.Equal(t, "carry on with the task", req.Input)
var stdout strings.Builder
inv, root := clitest.New(t, "exp", "task", "send", workspace.Name, "carry on with the task")
inv.Stdout = &stdout
clitest.SetupConfig(t, userClient, root)
httpapi.Write(ctx, w, http.StatusNoContent, nil)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
})
t.Run("ByWorkspaceID_WithArgument", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
var stdout strings.Builder
inv, root := clitest.New(t, "exp", "task", "send", workspace.ID.String(), "carry on with the task")
inv.Stdout = &stdout
clitest.SetupConfig(t, userClient, root)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
})
t.Run("ByWorkspaceName_WithStdin", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
var stdout strings.Builder
inv, root := clitest.New(t, "exp", "task", "send", workspace.Name, "--stdin")
inv.Stdout = &stdout
inv.Stdin = strings.NewReader("carry on with the task")
clitest.SetupConfig(t, userClient, root)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
})
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", "send", "doesnotexist", "some task input")
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", "send", uuid.Nil.String(), "some task input")
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("SendError", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
userClient, workspace := setupCLITaskTest(ctx, t, fakeAgentAPITaskSendErr(t, assert.AnError))
var stdout strings.Builder
inv, root := clitest.New(t, "exp", "task", "send", workspace.Name, "some task input")
inv.Stdout = &stdout
clitest.SetupConfig(t, userClient, root)
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, assert.AnError.Error())
})
}
func fakeAgentAPITaskSendOK(t *testing.T, expectMessage, returnMessage string) map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/status": func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "stable",
})
},
{
args: []string{taskID.String(), "carry on with the task"},
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/send", taskID.String()):
var req codersdk.TaskSendRequest
if !httpapi.Read(ctx, w, r, &req) {
return
}
assert.Equal(t, "carry on with the task", req.Input)
httpapi.Write(ctx, w, http.StatusNoContent, nil)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
{
args: []string{taskName, "--stdin"},
stdin: "carry on with the task",
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/send", taskID.String()):
var req codersdk.TaskSendRequest
if !httpapi.Read(ctx, w, r, &req) {
return
}
assert.Equal(t, "carry on with the task", req.Input)
httpapi.Write(ctx, w, http.StatusNoContent, nil)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
{
args: []string{"doesnotexist", "some task input"},
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)
}
}
},
},
{
args: []string{uuid.Nil.String(), "some task input"},
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/send", uuid.Nil.String()):
httpapi.ResourceNotFound(w)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
{
args: []string{uuid.Nil.String(), "some task input"},
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 fmt.Sprintf("/api/experimental/tasks/me/%s/send", uuid.Nil.String()):
httpapi.InternalServerError(w, assert.AnError)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
}
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", "send"}
err error
)
t.Cleanup(srv.Close)
inv, root := clitest.New(t, append(args, tt.args...)...)
inv.Stdin = strings.NewReader(tt.stdin)
clitest.SetupConfig(t, client, root)
err = inv.WithContext(ctx).Run()
if tt.expectError == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.expectError)
"/message": func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
var msg agentapisdk.PostMessageParams
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
})
assert.Equal(t, expectMessage, msg.Content)
message := agentapisdk.Message{
Id: 999,
Role: agentapisdk.RoleAgent,
Content: returnMessage,
Time: time.Now(),
}
_ = json.NewEncoder(w).Encode(message)
},
}
}
func fakeAgentAPITaskSendErr(t *testing.T, returnErr error) map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/status": func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "stable",
})
},
"/message": func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(returnErr.Error()))
},
}
}