@@ -30,6 +30,7 @@ import (
3030 "github.com/coder/coder/v2/coderd/httpapi"
3131 "github.com/coder/coder/v2/coderd/notifications"
3232 "github.com/coder/coder/v2/coderd/notifications/notificationstest"
33+ "github.com/coder/coder/v2/coderd/rbac"
3334 "github.com/coder/coder/v2/coderd/util/slice"
3435 "github.com/coder/coder/v2/codersdk"
3536 "github.com/coder/coder/v2/codersdk/agentsdk"
@@ -39,6 +40,66 @@ import (
3940 "github.com/coder/quartz"
4041)
4142
43+ // createTaskInState is a helper to create a task in the desired state.
44+ // It returns a function that takes context, test, and status, and returns the task ID.
45+ // The caller is responsible for setting up the database, owner, and user.
46+ func createTaskInState (db database.Store , ownerSubject rbac.Subject , ownerOrgID , userID uuid.UUID ) func (context.Context , * testing.T , database.TaskStatus ) uuid.UUID {
47+ return func (ctx context.Context , t * testing.T , status database.TaskStatus ) uuid.UUID {
48+ ctx = dbauthz .As (ctx , ownerSubject )
49+
50+ builder := dbfake .WorkspaceBuild (t , db , database.WorkspaceTable {
51+ OrganizationID : ownerOrgID ,
52+ OwnerID : userID ,
53+ }).
54+ WithTask (database.TaskTable {
55+ OrganizationID : ownerOrgID ,
56+ OwnerID : userID ,
57+ }, nil )
58+
59+ switch status {
60+ case database .TaskStatusPending :
61+ builder = builder .Pending ()
62+ case database .TaskStatusInitializing :
63+ builder = builder .Starting ()
64+ case database .TaskStatusPaused :
65+ builder = builder .Seed (database.WorkspaceBuild {
66+ Transition : database .WorkspaceTransitionStop ,
67+ })
68+ case database .TaskStatusError :
69+ // For error state, create a completed build then manipulate app health.
70+ default :
71+ require .Fail (t , "unsupported task status in test helper" , "status: %s" , status )
72+ }
73+
74+ resp := builder .Do ()
75+ taskID := resp .Task .ID
76+
77+ // Post-process by manipulating agent and app state.
78+ if status == database .TaskStatusError {
79+ // First, set agent to ready state so agent_status returns 'active'.
80+ // This ensures the cascade reaches app_status.
81+ err := db .UpdateWorkspaceAgentLifecycleStateByID (ctx , database.UpdateWorkspaceAgentLifecycleStateByIDParams {
82+ ID : resp .Agents [0 ].ID ,
83+ LifecycleState : database .WorkspaceAgentLifecycleStateReady ,
84+ })
85+ require .NoError (t , err )
86+
87+ // Then set workspace app health to unhealthy to trigger error state.
88+ apps , err := db .GetWorkspaceAppsByAgentID (ctx , resp .Agents [0 ].ID )
89+ require .NoError (t , err )
90+ require .Len (t , apps , 1 , "expected exactly one app for task" )
91+
92+ err = db .UpdateWorkspaceAppHealthByID (ctx , database.UpdateWorkspaceAppHealthByIDParams {
93+ ID : apps [0 ].ID ,
94+ Health : database .WorkspaceAppHealthUnhealthy ,
95+ })
96+ require .NoError (t , err )
97+ }
98+
99+ return taskID
100+ }
101+ }
102+
42103func TestTasks (t * testing.T ) {
43104 t .Parallel ()
44105
@@ -591,6 +652,94 @@ func TestTasks(t *testing.T) {
591652 require .ErrorAs (t , err , & sdkErr )
592653 require .Equal (t , http .StatusNotFound , sdkErr .StatusCode ())
593654 })
655+
656+ t .Run ("SendToNonActiveStates" , func (t * testing.T ) {
657+ t .Parallel ()
658+
659+ client , db := coderdtest .NewWithDatabase (t , & coderdtest.Options {IncludeProvisionerDaemon : true })
660+ owner := coderdtest .CreateFirstUser (t , client )
661+ ctx := testutil .Context (t , testutil .WaitMedium )
662+
663+ ownerUser , err := client .User (ctx , owner .UserID .String ())
664+ require .NoError (t , err )
665+ ownerSubject := coderdtest .AuthzUserSubject (ownerUser )
666+
667+ // Create a regular user for task ownership.
668+ _ , user := coderdtest .CreateAnotherUser (t , client , owner .OrganizationID )
669+
670+ createTask := createTaskInState (db , ownerSubject , owner .OrganizationID , user .ID )
671+
672+ t .Run ("Paused" , func (t * testing.T ) {
673+ t .Parallel ()
674+
675+ ctx := testutil .Context (t , testutil .WaitMedium )
676+ taskID := createTask (ctx , t , database .TaskStatusPaused )
677+
678+ err := client .TaskSend (ctx , "me" , taskID , codersdk.TaskSendRequest {
679+ Input : "Hello" ,
680+ })
681+
682+ var sdkErr * codersdk.Error
683+ require .Error (t , err )
684+ require .ErrorAs (t , err , & sdkErr )
685+ require .Equal (t , http .StatusConflict , sdkErr .StatusCode ())
686+ require .Contains (t , sdkErr .Message , "paused" )
687+ require .Contains (t , sdkErr .Detail , "Resume" )
688+ })
689+
690+ t .Run ("Initializing" , func (t * testing.T ) {
691+ t .Parallel ()
692+
693+ ctx := testutil .Context (t , testutil .WaitMedium )
694+ taskID := createTask (ctx , t , database .TaskStatusInitializing )
695+
696+ err := client .TaskSend (ctx , "me" , taskID , codersdk.TaskSendRequest {
697+ Input : "Hello" ,
698+ })
699+
700+ var sdkErr * codersdk.Error
701+ require .Error (t , err )
702+ require .ErrorAs (t , err , & sdkErr )
703+ require .Equal (t , http .StatusConflict , sdkErr .StatusCode ())
704+ require .Contains (t , sdkErr .Message , "initializing" )
705+ require .Contains (t , sdkErr .Detail , "resuming" )
706+ })
707+
708+ t .Run ("Pending" , func (t * testing.T ) {
709+ t .Parallel ()
710+
711+ ctx := testutil .Context (t , testutil .WaitMedium )
712+ taskID := createTask (ctx , t , database .TaskStatusPending )
713+
714+ err := client .TaskSend (ctx , "me" , taskID , codersdk.TaskSendRequest {
715+ Input : "Hello" ,
716+ })
717+
718+ var sdkErr * codersdk.Error
719+ require .Error (t , err )
720+ require .ErrorAs (t , err , & sdkErr )
721+ require .Equal (t , http .StatusConflict , sdkErr .StatusCode ())
722+ require .Contains (t , sdkErr .Message , "pending" )
723+ require .Contains (t , sdkErr .Detail , "resuming" )
724+ })
725+
726+ t .Run ("Error" , func (t * testing.T ) {
727+ t .Parallel ()
728+
729+ ctx := testutil .Context (t , testutil .WaitMedium )
730+ taskID := createTask (ctx , t , database .TaskStatusError )
731+
732+ err := client .TaskSend (ctx , "me" , taskID , codersdk.TaskSendRequest {
733+ Input : "Hello" ,
734+ })
735+
736+ var sdkErr * codersdk.Error
737+ require .Error (t , err )
738+ require .ErrorAs (t , err , & sdkErr )
739+ require .Equal (t , http .StatusBadRequest , sdkErr .StatusCode ())
740+ require .Contains (t , sdkErr .Message , "must be active" )
741+ })
742+ })
594743 })
595744
596745 t .Run ("Logs" , func (t * testing.T ) {
@@ -737,61 +886,7 @@ func TestTasks(t *testing.T) {
737886 // Create a regular user to test snapshot access.
738887 client , user := coderdtest .CreateAnotherUser (t , ownerClient , owner .OrganizationID )
739888
740- // Helper to create a task in the desired state.
741- createTaskInState := func (ctx context.Context , t * testing.T , status database.TaskStatus ) uuid.UUID {
742- ctx = dbauthz .As (ctx , ownerSubject )
743-
744- builder := dbfake .WorkspaceBuild (t , db , database.WorkspaceTable {
745- OrganizationID : owner .OrganizationID ,
746- OwnerID : user .ID ,
747- }).
748- WithTask (database.TaskTable {
749- OrganizationID : owner .OrganizationID ,
750- OwnerID : user .ID ,
751- }, nil )
752-
753- switch status {
754- case database .TaskStatusPending :
755- builder = builder .Pending ()
756- case database .TaskStatusInitializing :
757- builder = builder .Starting ()
758- case database .TaskStatusPaused :
759- builder = builder .Seed (database.WorkspaceBuild {
760- Transition : database .WorkspaceTransitionStop ,
761- })
762- case database .TaskStatusError :
763- // For error state, create a completed build then manipulate app health.
764- default :
765- require .Fail (t , "unsupported task status in test helper" , "status: %s" , status )
766- }
767-
768- resp := builder .Do ()
769- taskID := resp .Task .ID
770-
771- // Post-process by manipulating agent and app state.
772- if status == database .TaskStatusError {
773- // First, set agent to ready state so agent_status returns 'active'.
774- // This ensures the cascade reaches app_status.
775- err := db .UpdateWorkspaceAgentLifecycleStateByID (ctx , database.UpdateWorkspaceAgentLifecycleStateByIDParams {
776- ID : resp .Agents [0 ].ID ,
777- LifecycleState : database .WorkspaceAgentLifecycleStateReady ,
778- })
779- require .NoError (t , err )
780-
781- // Then set workspace app health to unhealthy to trigger error state.
782- apps , err := db .GetWorkspaceAppsByAgentID (ctx , resp .Agents [0 ].ID )
783- require .NoError (t , err )
784- require .Len (t , apps , 1 , "expected exactly one app for task" )
785-
786- err = db .UpdateWorkspaceAppHealthByID (ctx , database.UpdateWorkspaceAppHealthByIDParams {
787- ID : apps [0 ].ID ,
788- Health : database .WorkspaceAppHealthUnhealthy ,
789- })
790- require .NoError (t , err )
791- }
792-
793- return taskID
794- }
889+ createTask := createTaskInState (db , ownerSubject , owner .OrganizationID , user .ID )
795890
796891 // Prepare snapshot data used across tests.
797892 snapshotMessages := []agentapisdk.Message {
@@ -853,7 +948,7 @@ func TestTasks(t *testing.T) {
853948 t .Parallel ()
854949
855950 ctx := testutil .Context (t , testutil .WaitMedium )
856- taskID := createTaskInState (ctx , t , database .TaskStatusPending )
951+ taskID := createTask (ctx , t , database .TaskStatusPending )
857952
858953 err := db .UpsertTaskSnapshot (dbauthz .As (ctx , ownerSubject ), database.UpsertTaskSnapshotParams {
859954 TaskID : taskID ,
@@ -871,7 +966,7 @@ func TestTasks(t *testing.T) {
871966 t .Parallel ()
872967
873968 ctx := testutil .Context (t , testutil .WaitMedium )
874- taskID := createTaskInState (ctx , t , database .TaskStatusInitializing )
969+ taskID := createTask (ctx , t , database .TaskStatusInitializing )
875970
876971 err := db .UpsertTaskSnapshot (dbauthz .As (ctx , ownerSubject ), database.UpsertTaskSnapshotParams {
877972 TaskID : taskID ,
@@ -889,7 +984,7 @@ func TestTasks(t *testing.T) {
889984 t .Parallel ()
890985
891986 ctx := testutil .Context (t , testutil .WaitMedium )
892- taskID := createTaskInState (ctx , t , database .TaskStatusPaused )
987+ taskID := createTask (ctx , t , database .TaskStatusPaused )
893988
894989 err := db .UpsertTaskSnapshot (dbauthz .As (ctx , ownerSubject ), database.UpsertTaskSnapshotParams {
895990 TaskID : taskID ,
@@ -907,7 +1002,7 @@ func TestTasks(t *testing.T) {
9071002 t .Parallel ()
9081003
9091004 ctx := testutil .Context (t , testutil .WaitMedium )
910- taskID := createTaskInState (ctx , t , database .TaskStatusPending )
1005+ taskID := createTask (ctx , t , database .TaskStatusPending )
9111006
9121007 logsResp , err := client .TaskLogs (ctx , "me" , taskID )
9131008 require .NoError (t , err )
@@ -921,7 +1016,7 @@ func TestTasks(t *testing.T) {
9211016 t .Parallel ()
9221017
9231018 ctx := testutil .Context (t , testutil .WaitMedium )
924- taskID := createTaskInState (ctx , t , database .TaskStatusPending )
1019+ taskID := createTask (ctx , t , database .TaskStatusPending )
9251020
9261021 invalidEnvelope := coderd.TaskLogSnapshotEnvelope {
9271022 Format : "unknown-format" ,
@@ -950,7 +1045,7 @@ func TestTasks(t *testing.T) {
9501045 t .Parallel ()
9511046
9521047 ctx := testutil .Context (t , testutil .WaitMedium )
953- taskID := createTaskInState (ctx , t , database .TaskStatusPending )
1048+ taskID := createTask (ctx , t , database .TaskStatusPending )
9541049
9551050 err := db .UpsertTaskSnapshot (dbauthz .As (ctx , ownerSubject ), database.UpsertTaskSnapshotParams {
9561051 TaskID : taskID ,
@@ -971,7 +1066,7 @@ func TestTasks(t *testing.T) {
9711066 t .Parallel ()
9721067
9731068 ctx := testutil .Context (t , testutil .WaitMedium )
974- taskID := createTaskInState (ctx , t , database .TaskStatusError )
1069+ taskID := createTask (ctx , t , database .TaskStatusError )
9751070
9761071 _ , err := client .TaskLogs (ctx , "me" , taskID )
9771072 require .Error (t , err )
0 commit comments