fix(coderd)!: only show task status for current build (#19966)

This changes the task get endpoint to omit app statuses for previous
'lifetimes' of a workspace.

It also introduces a [breaking
change](https://github.com/coder/coder/blob/release/2.26/codersdk/aitasks.go#L83)
to bring `TaskStateComplete` in line with
`WorkspaceAppStatusStateComplete`. I can alternatively revert this
change and add a conversion function between the two SDK types.
This commit is contained in:
Cian Johnston
2025-09-26 10:14:37 +01:00
committed by GitHub
parent 930bbaf035
commit eb55f0ab19
5 changed files with 71 additions and 27 deletions
+6 -6
View File
@@ -96,9 +96,9 @@ func Test_TaskStatus(t *testing.T) {
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!`,
2s ago running complete Splines reticulated successfully!
2s ago stopping complete Splines reticulated successfully!
2s ago stopped complete 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) {
@@ -143,7 +143,7 @@ STATE CHANGED STATUS STATE MESSAGE
CreatedAt: now.Add(-5 * time.Second),
UpdatedAt: now.Add(-4 * time.Second),
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateCompleted,
State: codersdk.TaskStateComplete,
Timestamp: now.Add(-2 * time.Second),
Message: "Splines reticulated successfully!",
},
@@ -155,7 +155,7 @@ STATE CHANGED STATUS STATE MESSAGE
CreatedAt: now.Add(-5 * time.Second),
UpdatedAt: now.Add(-1 * time.Second),
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateCompleted,
State: codersdk.TaskStateComplete,
Timestamp: now.Add(-2 * time.Second),
Message: "Splines reticulated successfully!",
},
@@ -167,7 +167,7 @@ STATE CHANGED STATUS STATE MESSAGE
CreatedAt: now.Add(-5 * time.Second),
UpdatedAt: now,
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateCompleted,
State: codersdk.TaskStateComplete,
Timestamp: now.Add(-2 * time.Second),
Message: "Splines reticulated successfully!",
},
+10 -5
View File
@@ -245,13 +245,18 @@ func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Tas
}
}
// Ignore 'latest app status' if it is older than the latest build and the latest build is a 'start' transition.
// This ensures that you don't show a stale app status from a previous build.
// For stop transitions, there is still value in showing the latest app status.
var currentState *codersdk.TaskStateEntry
if ws.LatestAppStatus != nil {
currentState = &codersdk.TaskStateEntry{
Timestamp: ws.LatestAppStatus.CreatedAt,
State: codersdk.TaskState(ws.LatestAppStatus.State),
Message: ws.LatestAppStatus.Message,
URI: ws.LatestAppStatus.URI,
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart || ws.LatestAppStatus.CreatedAt.After(ws.LatestBuild.CreatedAt) {
currentState = &codersdk.TaskStateEntry{
Timestamp: ws.LatestAppStatus.CreatedAt,
State: codersdk.TaskState(ws.LatestAppStatus.State),
Message: ws.LatestAppStatus.Message,
URI: ws.LatestAppStatus.URI,
}
}
}
+49 -10
View File
@@ -18,6 +18,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
@@ -269,19 +270,39 @@ func TestTasks(t *testing.T) {
t.Run("Get", 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)
var (
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
ctx = testutil.Context(t, testutil.WaitLong)
user = coderdtest.CreateFirstUser(t, client)
template = createAITemplate(t, client, user)
// Create a workspace (task) with a specific prompt.
wantPrompt = "review my code"
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: codersdk.AITaskPromptParameterName, Value: wantPrompt},
}
})
)
template := createAITemplate(t, client, user)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
// Assert invariant: the workspace has exactly one resource with one agent with one app.
require.Len(t, ws.LatestBuild.Resources, 1)
require.Len(t, ws.LatestBuild.Resources[0].Agents, 1)
agentID := ws.LatestBuild.Resources[0].Agents[0].ID
taskAppID := ws.LatestBuild.Resources[0].Agents[0].Apps[0].ID
// Create a workspace (task) with a specific prompt.
wantPrompt := "review my code"
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: codersdk.AITaskPromptParameterName, Value: wantPrompt},
}
// Insert an app status for the workspace
_, err := db.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{
ID: uuid.New(),
WorkspaceID: workspace.ID,
CreatedAt: dbtime.Now(),
AgentID: agentID,
AppID: taskAppID,
State: database.WorkspaceAppStatusStateComplete,
Message: "all done",
})
require.NoError(t, err)
// Fetch the task by ID via experimental API and verify fields.
exp := codersdk.NewExperimentalClient(client)
@@ -293,6 +314,24 @@ func TestTasks(t *testing.T) {
assert.Equal(t, wantPrompt, task.InitialPrompt, "task prompt should match the AI Prompt parameter")
assert.Equal(t, workspace.ID, task.WorkspaceID.UUID, "workspace id should match")
assert.NotEmpty(t, task.Status, "task status should not be empty")
// Stop the workspace
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Verify that the previous status still remains
updated, err := exp.TaskByID(ctx, workspace.ID)
require.NoError(t, err)
assert.NotNil(t, updated.CurrentState, "current state should not be nil")
assert.Equal(t, "all done", updated.CurrentState.Message)
assert.Equal(t, codersdk.TaskStateComplete, updated.CurrentState.State)
// Start the workspace again
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart)
// Verify that the status from the previous build is no longer present
updated, err = exp.TaskByID(ctx, workspace.ID)
require.NoError(t, err)
assert.Nil(t, updated.CurrentState, "current state should be nil")
})
t.Run("Delete", func(t *testing.T) {
+4 -4
View File
@@ -96,10 +96,10 @@ type TaskState string
// TaskState enums.
const (
TaskStateWorking TaskState = "working"
TaskStateIdle TaskState = "idle"
TaskStateCompleted TaskState = "completed"
TaskStateFailed TaskState = "failed"
TaskStateWorking TaskState = "working"
TaskStateIdle TaskState = "idle"
TaskStateComplete TaskState = "complete"
TaskStateFailed TaskState = "failed"
)
// Task represents a task.
+2 -2
View File
@@ -2907,7 +2907,7 @@ export interface TaskSendRequest {
}
// From codersdk/aitasks.go
export type TaskState = "completed" | "failed" | "idle" | "working";
export type TaskState = "complete" | "failed" | "idle" | "working";
// From codersdk/aitasks.go
export interface TaskStateEntry {
@@ -2918,7 +2918,7 @@ export interface TaskStateEntry {
}
export const TaskStates: TaskState[] = [
"completed",
"complete",
"failed",
"idle",
"working",