mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
Closes https://github.com/coder/internal/issues/949 Adds the following fields to `codersdk.Task` - OwnerName - TemplateName - TemplateDisplayName - TemplateIcon - WorkspaceAgentID - WorkspaceAgentLifecycle - WorkspaceAgentHealth The implementation is unfortunately not compatible with multiple agents as we have no reliable way to tell which agent has the AI task running in it. For now we just pick the first agent found, but in the future this will need to be changed.
278 lines
9.5 KiB
Go
278 lines
9.5 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func Test_TaskStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
args []string
|
|
expectOutput string
|
|
expectError string
|
|
hf func(context.Context, time.Time) func(http.ResponseWriter, *http.Request)
|
|
}{
|
|
{
|
|
args: []string{"doesnotexist"},
|
|
expectError: httpapi.ResourceNotFoundResponse.Message,
|
|
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) {
|
|
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{"err-fetching-workspace"},
|
|
expectError: assert.AnError.Error(),
|
|
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/users/me/workspace/err-fetching-workspace":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
})
|
|
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
|
|
httpapi.InternalServerError(w, assert.AnError)
|
|
default:
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
args: []string{"exists"},
|
|
expectOutput: `STATE CHANGED STATUS STATE MESSAGE
|
|
0s ago running working Thinking furiously...`,
|
|
hf: func(ctx context.Context, now time.Time) func(w http.ResponseWriter, r *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/users/me/workspace/exists":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
})
|
|
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusRunning,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateWorking,
|
|
Timestamp: now,
|
|
Message: "Thinking furiously...",
|
|
},
|
|
})
|
|
default:
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
args: []string{"exists", "--watch"},
|
|
expectOutput: `
|
|
STATE CHANGED STATUS STATE MESSAGE
|
|
4s ago running
|
|
3s ago running working Reticulating splines...
|
|
2s ago running completed Splines reticulated successfully!
|
|
2s ago stopping completed Splines reticulated successfully!
|
|
2s ago stopped completed Splines reticulated successfully!`,
|
|
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) {
|
|
var calls atomic.Int64
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
defer calls.Add(1)
|
|
switch r.URL.Path {
|
|
case "/api/v2/users/me/workspace/exists":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
})
|
|
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
|
|
switch calls.Load() {
|
|
case 0:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusPending,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now.Add(-5 * time.Second),
|
|
})
|
|
case 1:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusRunning,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now.Add(-4 * time.Second),
|
|
})
|
|
case 2:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusRunning,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now.Add(-4 * time.Second),
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateWorking,
|
|
Timestamp: now.Add(-3 * time.Second),
|
|
Message: "Reticulating splines...",
|
|
},
|
|
})
|
|
case 3:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusRunning,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now.Add(-4 * time.Second),
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateCompleted,
|
|
Timestamp: now.Add(-2 * time.Second),
|
|
Message: "Splines reticulated successfully!",
|
|
},
|
|
})
|
|
case 4:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusStopping,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now.Add(-1 * time.Second),
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateCompleted,
|
|
Timestamp: now.Add(-2 * time.Second),
|
|
Message: "Splines reticulated successfully!",
|
|
},
|
|
})
|
|
case 5:
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusStopped,
|
|
CreatedAt: now.Add(-5 * time.Second),
|
|
UpdatedAt: now,
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateCompleted,
|
|
Timestamp: now.Add(-2 * time.Second),
|
|
Message: "Splines reticulated successfully!",
|
|
},
|
|
})
|
|
default:
|
|
httpapi.InternalServerError(w, xerrors.New("too many calls!"))
|
|
return
|
|
}
|
|
default:
|
|
httpapi.InternalServerError(w, xerrors.Errorf("unexpected path: %q", r.URL.Path))
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
args: []string{"exists", "--output", "json"},
|
|
expectOutput: `{
|
|
"id": "11111111-1111-1111-1111-111111111111",
|
|
"organization_id": "00000000-0000-0000-0000-000000000000",
|
|
"owner_id": "00000000-0000-0000-0000-000000000000",
|
|
"owner_name": "",
|
|
"name": "",
|
|
"template_id": "00000000-0000-0000-0000-000000000000",
|
|
"template_name": "",
|
|
"template_display_name": "",
|
|
"template_icon": "",
|
|
"workspace_id": null,
|
|
"workspace_agent_id": null,
|
|
"workspace_agent_lifecycle": null,
|
|
"workspace_agent_health": null,
|
|
"initial_prompt": "",
|
|
"status": "running",
|
|
"current_state": {
|
|
"timestamp": "2025-08-26T12:34:57Z",
|
|
"state": "working",
|
|
"message": "Thinking furiously...",
|
|
"uri": ""
|
|
},
|
|
"created_at": "2025-08-26T12:34:56Z",
|
|
"updated_at": "2025-08-26T12:34:56Z"
|
|
}`,
|
|
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) {
|
|
ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v2/users/me/workspace/exists":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Workspace{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
})
|
|
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
|
|
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
|
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
|
Status: codersdk.WorkspaceStatusRunning,
|
|
CreatedAt: ts,
|
|
UpdatedAt: ts,
|
|
CurrentState: &codersdk.TaskStateEntry{
|
|
State: codersdk.TaskStateWorking,
|
|
Timestamp: ts.Add(time.Second),
|
|
Message: "Thinking furiously...",
|
|
},
|
|
})
|
|
default:
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
} {
|
|
t.Run(strings.Join(tc.args, ","), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = testutil.Context(t, testutil.WaitShort)
|
|
now = time.Now().UTC() // TODO: replace with quartz
|
|
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, now)))
|
|
client = new(codersdk.Client)
|
|
sb = strings.Builder{}
|
|
args = []string{"exp", "task", "status", "--watch-interval", testutil.IntervalFast.String()}
|
|
)
|
|
|
|
t.Cleanup(srv.Close)
|
|
client.URL = testutil.MustURL(t, srv.URL)
|
|
args = append(args, tc.args...)
|
|
inv, root := clitest.New(t, args...)
|
|
inv.Stdout = &sb
|
|
inv.Stderr = &sb
|
|
clitest.SetupConfig(t, client, root)
|
|
err := inv.WithContext(ctx).Run()
|
|
if tc.expectError == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.expectError)
|
|
}
|
|
if diff := tableDiff(tc.expectOutput, sb.String()); diff != "" {
|
|
t.Errorf("unexpected output diff (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func tableDiff(want, got string) string {
|
|
var gotTrimmed strings.Builder
|
|
for _, line := range strings.Split(got, "\n") {
|
|
_, _ = gotTrimmed.WriteString(strings.TrimRight(line, " ") + "\n")
|
|
}
|
|
return cmp.Diff(strings.TrimSpace(want), strings.TrimSpace(gotTrimmed.String()))
|
|
}
|