mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(coderd): use task data model for send/logs (#20381)
Updates coder/internal#976
This commit is contained in:
committed by
GitHub
parent
1cb2ac65e5
commit
2c6cbf15e2
@@ -23,7 +23,7 @@ import (
|
||||
func Test_TaskLogs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
|
||||
t.Skip("TODO(mafredri): Remove, fixed up-stack!")
|
||||
|
||||
testMessages := []agentapisdk.Message{
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
func Test_TaskSend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
|
||||
t.Skip("TODO(mafredri): Remove, fixed up-stack!")
|
||||
|
||||
t.Run("ByWorkspaceName_WithArgument", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
+35
-71
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -755,15 +754,7 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
|
||||
// workspace and validate the sidebar app health.
|
||||
func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
idStr := chi.URLParam(r, "task")
|
||||
taskID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid UUID %q for task ID.", idStr),
|
||||
})
|
||||
return
|
||||
}
|
||||
task := httpmw.TaskParam(r)
|
||||
|
||||
var req codersdk.TaskSendRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
@@ -776,7 +767,7 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = api.authAndDoWithTaskSidebarAppClient(r, taskID, func(ctx context.Context, client *http.Client, appURL *url.URL) error {
|
||||
if err := api.authAndDoWithTaskAppClient(r, task, func(ctx context.Context, client *http.Client, appURL *url.URL) error {
|
||||
agentAPIClient, err := aiagentapi.NewClient(appURL.String(), aiagentapi.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
|
||||
@@ -835,18 +826,10 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
|
||||
// We enforce ApplicationConnect RBAC on the workspace and validate the sidebar app health.
|
||||
func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
idStr := chi.URLParam(r, "task")
|
||||
taskID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid UUID %q for task ID.", idStr),
|
||||
})
|
||||
return
|
||||
}
|
||||
task := httpmw.TaskParam(r)
|
||||
|
||||
var out codersdk.TaskLogsResponse
|
||||
if err := api.authAndDoWithTaskSidebarAppClient(r, taskID, func(ctx context.Context, client *http.Client, appURL *url.URL) error {
|
||||
if err := api.authAndDoWithTaskAppClient(r, task, func(ctx context.Context, client *http.Client, appURL *url.URL) error {
|
||||
agentAPIClient, err := aiagentapi.NewClient(appURL.String(), aiagentapi.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
|
||||
@@ -894,7 +877,7 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// authAndDoWithTaskSidebarAppClient centralizes the shared logic to:
|
||||
// authAndDoWithTaskAppClient centralizes the shared logic to:
|
||||
//
|
||||
// - Fetch the task workspace
|
||||
// - Authorize ApplicationConnect on the workspace
|
||||
@@ -903,15 +886,31 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// The provided callback receives the context, an HTTP client that dials via the
|
||||
// agent, and the base app URL (as a value URL) to perform any request.
|
||||
func (api *API) authAndDoWithTaskSidebarAppClient(
|
||||
func (api *API) authAndDoWithTaskAppClient(
|
||||
r *http.Request,
|
||||
taskID uuid.UUID,
|
||||
task database.Task,
|
||||
do func(ctx context.Context, client *http.Client, appURL *url.URL) error,
|
||||
) error {
|
||||
ctx := r.Context()
|
||||
|
||||
workspaceID := taskID
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID)
|
||||
if task.Status != database.TaskStatusActive {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task status must be active.",
|
||||
Detail: fmt.Sprintf("Task status is %q, it must be %q to interact with the task.", task.Status, codersdk.TaskStatusActive),
|
||||
})
|
||||
}
|
||||
if !task.WorkspaceID.Valid {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task does not have a workspace.",
|
||||
})
|
||||
}
|
||||
if !task.WorkspaceAppID.Valid {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task does not have a workspace app.",
|
||||
})
|
||||
}
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, task.WorkspaceID.UUID)
|
||||
if err != nil {
|
||||
if httpapi.Is404Error(err) {
|
||||
return httperror.ErrResourceNotFound
|
||||
@@ -927,65 +926,30 @@ func (api *API) authAndDoWithTaskSidebarAppClient(
|
||||
return httperror.ErrResourceNotFound
|
||||
}
|
||||
|
||||
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
||||
apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, task.WorkspaceAgentID.UUID)
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace resources.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(data.builds) == 0 || len(data.templates) == 0 {
|
||||
return httperror.ErrResourceNotFound
|
||||
}
|
||||
build := data.builds[0]
|
||||
if build.HasAITask == nil || !*build.HasAITask || build.AITaskSidebarAppID == nil || *build.AITaskSidebarAppID == uuid.Nil {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task is not configured with a sidebar app.",
|
||||
})
|
||||
}
|
||||
|
||||
// Find the sidebar app details to get the URL and validate app health.
|
||||
sidebarAppID := *build.AITaskSidebarAppID
|
||||
agentID, sidebarApp, ok := func() (uuid.UUID, codersdk.WorkspaceApp, bool) {
|
||||
for _, res := range build.Resources {
|
||||
for _, agent := range res.Agents {
|
||||
for _, app := range agent.Apps {
|
||||
if app.ID == sidebarAppID {
|
||||
return agent.ID, app, true
|
||||
}
|
||||
}
|
||||
}
|
||||
var app *database.WorkspaceApp
|
||||
for _, a := range apps {
|
||||
if a.ID == task.WorkspaceAppID.UUID {
|
||||
app = &a
|
||||
break
|
||||
}
|
||||
return uuid.Nil, codersdk.WorkspaceApp{}, false
|
||||
}()
|
||||
if !ok {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task sidebar app not found in latest build.",
|
||||
})
|
||||
}
|
||||
|
||||
// Return an informative error if the app isn't healthy rather than trying
|
||||
// and failing.
|
||||
switch sidebarApp.Health {
|
||||
case codersdk.WorkspaceAppHealthDisabled:
|
||||
// No health check, pass through.
|
||||
case codersdk.WorkspaceAppHealthInitializing:
|
||||
return httperror.NewResponseError(http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "Task sidebar app is initializing. Try again shortly.",
|
||||
})
|
||||
case codersdk.WorkspaceAppHealthUnhealthy:
|
||||
return httperror.NewResponseError(http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "Task sidebar app is unhealthy.",
|
||||
})
|
||||
}
|
||||
|
||||
// Build the direct app URL and dial the agent.
|
||||
if sidebarApp.URL == "" {
|
||||
appURL := app.Url.String
|
||||
if appURL == "" {
|
||||
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Task sidebar app URL is not configured.",
|
||||
})
|
||||
}
|
||||
parsedURL, err := url.Parse(sidebarApp.URL)
|
||||
parsedURL, err := url.Parse(appURL)
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error parsing task app URL.",
|
||||
@@ -1000,7 +964,7 @@ func (api *API) authAndDoWithTaskSidebarAppClient(
|
||||
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer dialCancel()
|
||||
agentConn, release, err := api.agentProvider.AgentConn(dialCtx, agentID)
|
||||
agentConn, release, err := api.agentProvider.AgentConn(dialCtx, task.WorkspaceAgentID.UUID)
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{
|
||||
Message: "Failed to reach task app endpoint.",
|
||||
|
||||
+187
-210
@@ -2,7 +2,7 @@ package coderd_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
agentapisdk "github.com/coder/agentapi-sdk-go"
|
||||
"github.com/coder/coder/v2/agent"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
@@ -210,8 +211,8 @@ func TestTasks(t *testing.T) {
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Id: taskAppID.String(),
|
||||
Slug: "task-sidebar",
|
||||
DisplayName: "Task Sidebar",
|
||||
Slug: "task-app",
|
||||
DisplayName: "Task App",
|
||||
Url: opt.appURL,
|
||||
},
|
||||
},
|
||||
@@ -221,9 +222,7 @@ func TestTasks(t *testing.T) {
|
||||
},
|
||||
AiTasks: []*proto.AITask{
|
||||
{
|
||||
SidebarApp: &proto.AITaskSidebarApp{
|
||||
Id: taskAppID.String(),
|
||||
},
|
||||
AppId: taskAppID.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -469,41 +468,40 @@ func TestTasks(t *testing.T) {
|
||||
t.Run("Send", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
|
||||
|
||||
t.Run("IntegrationOK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
createStatusResponse := func(status string) string {
|
||||
return `
|
||||
{
|
||||
"$schema": "http://localhost:3284/schemas/StatusResponseBody.json",
|
||||
"status": "` + status + `"
|
||||
}
|
||||
`
|
||||
}
|
||||
statusResponse := createStatusResponse("stable")
|
||||
statusResponse := agentapisdk.StatusStable
|
||||
|
||||
// Start a fake AgentAPI that accepts GET /status and POST /message.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/status" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := agentapisdk.GetStatusResponse{
|
||||
Status: statusResponse,
|
||||
}
|
||||
respBytes, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, statusResponse)
|
||||
w.Write(respBytes)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && r.URL.Path == "/message" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
assert.Equal(t, `{"content":"Hello, Agent!","type":"user"}`, string(b), "expected message content")
|
||||
expectedReq := agentapisdk.PostMessageParams{
|
||||
Content: "Hello, Agent!",
|
||||
Type: agentapisdk.MessageTypeUser,
|
||||
}
|
||||
expectedBytes, _ := json.Marshal(expectedReq)
|
||||
assert.Equal(t, string(expectedBytes), string(b), "expected message content")
|
||||
|
||||
resp := agentapisdk.PostMessageResponse{Ok: true}
|
||||
respBytes, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, `{"ok": true}`)
|
||||
w.Write(respBytes)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -511,103 +509,105 @@ func TestTasks(t *testing.T) {
|
||||
defer srv.Close()
|
||||
|
||||
// Create an AI-capable template whose sidebar app points to our fake AgentAPI.
|
||||
authToken := uuid.NewString()
|
||||
template := createAITemplate(t, client, owner, withSidebarURL(srv.URL), withAgentToken(authToken))
|
||||
var (
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
owner = coderdtest.CreateFirstUser(t, client)
|
||||
userClient, _ = coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
agentAuthToken = uuid.NewString()
|
||||
template = createAITemplate(t, client, owner, withAgentToken(agentAuthToken), withSidebarURL(srv.URL))
|
||||
exp = codersdk.NewExperimentalClient(userClient)
|
||||
)
|
||||
|
||||
// Create a workspace (task) from the AI-capable template.
|
||||
ws := coderdtest.CreateWorkspace(t, userClient, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||
{Name: codersdk.AITaskPromptParameterName, Value: "send a message"},
|
||||
}
|
||||
task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Input: "send me food",
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, task.WorkspaceID.Valid)
|
||||
|
||||
// Get the workspace and wait for it to be ready.
|
||||
ws, err := userClient.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, ws.LatestBuild.ID)
|
||||
|
||||
// Fetch the task by ID via experimental API and verify fields.
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, task.WorkspaceBuildNumber)
|
||||
require.True(t, task.WorkspaceAgentID.Valid)
|
||||
require.True(t, task.WorkspaceAppID.Valid)
|
||||
|
||||
// Insert an app status for the workspace
|
||||
_, err = db.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: task.WorkspaceID.UUID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
AgentID: task.WorkspaceAgentID.UUID,
|
||||
AppID: task.WorkspaceAppID.UUID,
|
||||
State: database.WorkspaceAppStatusStateComplete,
|
||||
Message: "all done",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start a fake agent so the workspace agent is connected before sending the message.
|
||||
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(authToken))
|
||||
_ = agenttest.New(t, client.URL, authToken, func(o *agent.Options) {
|
||||
agentClient := agentsdk.New(userClient.URL, agentsdk.WithFixedToken(agentAuthToken))
|
||||
_ = agenttest.New(t, userClient.URL, agentAuthToken, func(o *agent.Options) {
|
||||
o.Client = agentClient
|
||||
})
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, userClient, ws.ID).WaitFor(coderdtest.AgentsReady)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, client, ws.ID).WithContext(ctx).WaitFor(coderdtest.AgentsReady)
|
||||
|
||||
// Lookup the sidebar app ID.
|
||||
w, err := client.Workspace(ctx, ws.ID)
|
||||
// Fetch the task by ID via experimental API and verify fields.
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
var sidebarAppID uuid.UUID
|
||||
for _, res := range w.LatestBuild.Resources {
|
||||
for _, ag := range res.Agents {
|
||||
for _, app := range ag.Apps {
|
||||
if app.Slug == "task-sidebar" {
|
||||
sidebarAppID = app.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotEqual(t, uuid.Nil, sidebarAppID)
|
||||
|
||||
// Make the sidebar app unhealthy initially.
|
||||
err = api.Database.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{
|
||||
ID: sidebarAppID,
|
||||
err = db.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{
|
||||
ID: task.WorkspaceAppID.UUID,
|
||||
Health: database.WorkspaceAppHealthUnhealthy,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
exp := codersdk.NewExperimentalClient(userClient)
|
||||
err = exp.TaskSend(ctx, "me", ws.ID, codersdk.TaskSendRequest{
|
||||
err = exp.TaskSend(ctx, "me", task.ID, codersdk.TaskSendRequest{
|
||||
Input: "Hello, Agent!",
|
||||
})
|
||||
require.Error(t, err, "wanted error due to unhealthy sidebar app")
|
||||
|
||||
// Make the sidebar app healthy.
|
||||
err = api.Database.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{
|
||||
ID: sidebarAppID,
|
||||
err = db.UpdateWorkspaceAppHealthByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAppHealthByIDParams{
|
||||
ID: task.WorkspaceAppID.UUID,
|
||||
Health: database.WorkspaceAppHealthHealthy,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
statusResponse = createStatusResponse("bad")
|
||||
statusResponse = agentapisdk.AgentStatus("bad")
|
||||
|
||||
err = exp.TaskSend(ctx, "me", ws.ID, codersdk.TaskSendRequest{
|
||||
err = exp.TaskSend(ctx, "me", task.ID, codersdk.TaskSendRequest{
|
||||
Input: "Hello, Agent!",
|
||||
})
|
||||
require.Error(t, err, "wanted error due to bad status")
|
||||
|
||||
statusResponse = createStatusResponse("stable")
|
||||
statusResponse = agentapisdk.StatusStable
|
||||
|
||||
// Send task input to the tasks sidebar app and expect 204.e
|
||||
err = exp.TaskSend(ctx, "me", ws.ID, codersdk.TaskSendRequest{
|
||||
Input: "Hello, Agent!",
|
||||
})
|
||||
require.NoError(t, err, "wanted no error due to healthy sidebar app and stable status")
|
||||
})
|
||||
|
||||
t.Run("MissingContent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
template := createAITemplate(t, client, user)
|
||||
|
||||
// Create a workspace (task).
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||
{Name: codersdk.AITaskPromptParameterName, Value: "do work"},
|
||||
}
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
err := exp.TaskSend(ctx, "me", ws.ID, codersdk.TaskSendRequest{
|
||||
Input: "",
|
||||
//nolint:tparallel // Not intended to run in parallel.
|
||||
t.Run("SendOK", func(t *testing.T) {
|
||||
err = exp.TaskSend(ctx, "me", task.ID, codersdk.TaskSendRequest{
|
||||
Input: "Hello, Agent!",
|
||||
})
|
||||
require.NoError(t, err, "wanted no error due to healthy sidebar app and stable status")
|
||||
})
|
||||
|
||||
var sdkErr *codersdk.Error
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
//nolint:tparallel // Not intended to run in parallel.
|
||||
t.Run("MissingContent", func(t *testing.T) {
|
||||
err = exp.TaskSend(ctx, "me", task.ID, codersdk.TaskSendRequest{
|
||||
Input: "",
|
||||
})
|
||||
require.Error(t, err, "wanted error due to missing content")
|
||||
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("TaskNotFound", func(t *testing.T) {
|
||||
@@ -627,108 +627,112 @@ func TestTasks(t *testing.T) {
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NotATask", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Create a template without AI tasks.
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
err := exp.TaskSend(ctx, "me", ws.ID, codersdk.TaskSendRequest{
|
||||
Input: "hello",
|
||||
})
|
||||
|
||||
var sdkErr *codersdk.Error
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Logs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Skip("TODO(mafredri): Remove, fixed down-stack!")
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
messageResponse := `
|
||||
messageResponseData := agentapisdk.GetMessagesResponse{
|
||||
Messages: []agentapisdk.Message{
|
||||
{
|
||||
"$schema": "http://localhost:3284/schemas/MessagesResponseBody.json",
|
||||
"messages": [
|
||||
{
|
||||
"id": 0,
|
||||
"content": "Welcome, user!",
|
||||
"role": "agent",
|
||||
"time": "2025-09-25T10:42:48.751774125Z"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"content": "Hello, agent!",
|
||||
"role": "user",
|
||||
"time": "2025-09-25T10:46:42.880996296Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"content": "What would you like to work on today?",
|
||||
"role": "agent",
|
||||
"time": "2025-09-25T10:46:50.747761102Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
Id: 0,
|
||||
Content: "Welcome, user!",
|
||||
Role: agentapisdk.RoleAgent,
|
||||
Time: time.Date(2025, 9, 25, 10, 42, 48, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Id: 1,
|
||||
Content: "Hello, agent!",
|
||||
Role: agentapisdk.RoleUser,
|
||||
Time: time.Date(2025, 9, 25, 10, 46, 42, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Content: "What would you like to work on today?",
|
||||
Role: agentapisdk.RoleAgent,
|
||||
Time: time.Date(2025, 9, 25, 10, 46, 50, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
}
|
||||
messageResponseBytes, err := json.Marshal(messageResponseData)
|
||||
require.NoError(t, err)
|
||||
messageResponse := string(messageResponseBytes)
|
||||
|
||||
// Fake AgentAPI that returns a couple of messages.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/messages" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, messageResponse)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
var shouldReturnError bool
|
||||
|
||||
// Template pointing sidebar app to our fake AgentAPI.
|
||||
authToken := uuid.NewString()
|
||||
template := createAITemplate(t, client, owner, withSidebarURL(srv.URL), withAgentToken(authToken))
|
||||
// Fake AgentAPI that returns a couple of messages or an error.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if shouldReturnError {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = io.WriteString(w, "boom")
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/messages" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, messageResponse)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Create task workspace.
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||
{Name: codersdk.AITaskPromptParameterName, Value: "show logs"},
|
||||
}
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
// Create an AI-capable template whose sidebar app points to our fake AgentAPI.
|
||||
var (
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
owner = coderdtest.CreateFirstUser(t, client)
|
||||
agentAuthToken = uuid.NewString()
|
||||
template = createAITemplate(t, client, owner, withAgentToken(agentAuthToken), withSidebarURL(srv.URL))
|
||||
exp = codersdk.NewExperimentalClient(client)
|
||||
)
|
||||
|
||||
// Start a fake agent.
|
||||
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(authToken))
|
||||
_ = agenttest.New(t, client.URL, authToken, func(o *agent.Options) {
|
||||
o.Client = agentClient
|
||||
})
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, client, ws.ID).WithContext(ctx).WaitFor(coderdtest.AgentsReady)
|
||||
task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Input: "show logs",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, task.WorkspaceID.Valid)
|
||||
|
||||
// Omit sidebar app health as undefined is OK.
|
||||
// Get the workspace and wait for it to be ready.
|
||||
ws, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Fetch the task by ID via experimental API and verify fields.
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, task.WorkspaceBuildNumber)
|
||||
require.True(t, task.WorkspaceAgentID.Valid)
|
||||
require.True(t, task.WorkspaceAppID.Valid)
|
||||
|
||||
// Insert an app status for the workspace
|
||||
_, err = db.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: task.WorkspaceID.UUID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
AgentID: task.WorkspaceAgentID.UUID,
|
||||
AppID: task.WorkspaceAppID.UUID,
|
||||
State: database.WorkspaceAppStatusStateComplete,
|
||||
Message: "all done",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start a fake agent so the workspace agent is connected before fetching logs.
|
||||
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(agentAuthToken))
|
||||
_ = agenttest.New(t, client.URL, agentAuthToken, func(o *agent.Options) {
|
||||
o.Client = agentClient
|
||||
})
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, client, ws.ID).WaitFor(coderdtest.AgentsReady)
|
||||
|
||||
// Fetch the task by ID via experimental API and verify fields.
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:tparallel // Not intended to run in parallel.
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
// Fetch logs.
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
resp, err := exp.TaskLogs(ctx, "me", ws.ID)
|
||||
resp, err := exp.TaskLogs(ctx, "me", task.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Logs, 3)
|
||||
assert.Equal(t, 0, resp.Logs[0].ID)
|
||||
@@ -744,38 +748,11 @@ func TestTasks(t *testing.T) {
|
||||
assert.Equal(t, "What would you like to work on today?", resp.Logs[2].Content)
|
||||
})
|
||||
|
||||
//nolint:tparallel // Not intended to run in parallel.
|
||||
t.Run("UpstreamError", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Fake AgentAPI that returns 500 for messages.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = io.WriteString(w, "boom")
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
authToken := uuid.NewString()
|
||||
template := createAITemplate(t, client, owner, withSidebarURL(srv.URL), withAgentToken(authToken))
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||||
{Name: codersdk.AITaskPromptParameterName, Value: "show logs"},
|
||||
}
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Start fake agent.
|
||||
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(authToken))
|
||||
_ = agenttest.New(t, client.URL, authToken, func(o *agent.Options) {
|
||||
o.Client = agentClient
|
||||
})
|
||||
coderdtest.NewWorkspaceAgentWaiter(t, client, ws.ID).WithContext(ctx).WaitFor(coderdtest.AgentsReady)
|
||||
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
_, err := exp.TaskLogs(ctx, "me", ws.ID)
|
||||
shouldReturnError = true
|
||||
t.Cleanup(func() { shouldReturnError = false })
|
||||
_, err := exp.TaskLogs(ctx, "me", task.ID)
|
||||
|
||||
var sdkErr *codersdk.Error
|
||||
require.Error(t, err)
|
||||
|
||||
Reference in New Issue
Block a user