@@ -573,7 +573,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
573
573
build , err = client .WorkspaceBuild (ctx , workspace .LatestBuild .ID )
574
574
return assert .NoError (t , err ) && build .Job .Status == codersdk .ProvisionerJobRunning
575
575
}, testutil .WaitShort , testutil .IntervalFast )
576
- err := client .CancelWorkspaceBuild (ctx , build .ID )
576
+ err := client .CancelWorkspaceBuild (ctx , build .ID , codersdk. CancelWorkspaceBuildRequest {} )
577
577
require .NoError (t , err )
578
578
require .Eventually (t , func () bool {
579
579
var err error
@@ -618,11 +618,161 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
618
618
build , err = userClient .WorkspaceBuild (ctx , workspace .LatestBuild .ID )
619
619
return assert .NoError (t , err ) && build .Job .Status == codersdk .ProvisionerJobRunning
620
620
}, testutil .WaitShort , testutil .IntervalFast )
621
- err := userClient .CancelWorkspaceBuild (ctx , build .ID )
621
+ err := userClient .CancelWorkspaceBuild (ctx , build .ID , codersdk. CancelWorkspaceBuildRequest {} )
622
622
var apiErr * codersdk.Error
623
623
require .ErrorAs (t , err , & apiErr )
624
624
require .Equal (t , http .StatusForbidden , apiErr .StatusCode ())
625
625
})
626
+
627
+ t .Run ("Cancel with expect_state=pending" , func (t * testing.T ) {
628
+ t .Parallel ()
629
+ if ! dbtestutil .WillUsePostgres () {
630
+ t .Skip ("this test requires postgres" )
631
+ }
632
+ // Given: a coderd instance with a provisioner daemon
633
+ store , ps , db := dbtestutil .NewDBWithSQLDB (t )
634
+ client , closeDaemon := coderdtest .NewWithProvisionerCloser (t , & coderdtest.Options {
635
+ Database : store ,
636
+ Pubsub : ps ,
637
+ IncludeProvisionerDaemon : true ,
638
+ })
639
+ defer closeDaemon .Close ()
640
+ // Given: a user, template, and workspace
641
+ user := coderdtest .CreateFirstUser (t , client )
642
+ version := coderdtest .CreateTemplateVersion (t , client , user .OrganizationID , nil )
643
+ coderdtest .AwaitTemplateVersionJobCompleted (t , client , version .ID )
644
+ template := coderdtest .CreateTemplate (t , client , user .OrganizationID , version .ID )
645
+ workspace := coderdtest .CreateWorkspace (t , client , template .ID )
646
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , workspace .LatestBuild .ID )
647
+
648
+ // Stop the provisioner daemon.
649
+ require .NoError (t , closeDaemon .Close ())
650
+ ctx := testutil .Context (t , testutil .WaitLong )
651
+ // Given: no provisioner daemons exist.
652
+ _ , err := db .ExecContext (ctx , `DELETE FROM provisioner_daemons;` )
653
+ require .NoError (t , err )
654
+
655
+ // When: a new workspace build is created
656
+ build , err := client .CreateWorkspaceBuild (ctx , workspace .ID , codersdk.CreateWorkspaceBuildRequest {
657
+ TemplateVersionID : template .ActiveVersionID ,
658
+ Transition : codersdk .WorkspaceTransitionStart ,
659
+ })
660
+ // Then: the request should succeed.
661
+ require .NoError (t , err )
662
+ // Then: the provisioner job should remain pending.
663
+ require .Equal (t , codersdk .ProvisionerJobPending , build .Job .Status )
664
+
665
+ // Then: the response should indicate no provisioners are available.
666
+ if assert .NotNil (t , build .MatchedProvisioners ) {
667
+ assert .Zero (t , build .MatchedProvisioners .Count )
668
+ assert .Zero (t , build .MatchedProvisioners .Available )
669
+ assert .Zero (t , build .MatchedProvisioners .MostRecentlySeen .Time )
670
+ assert .False (t , build .MatchedProvisioners .MostRecentlySeen .Valid )
671
+ }
672
+
673
+ // When: the workspace build is canceled
674
+ err = client .CancelWorkspaceBuild (ctx , build .ID , codersdk.CancelWorkspaceBuildRequest {
675
+ ExpectState : codersdk .ProvisionerJobPending ,
676
+ })
677
+ require .NoError (t , err )
678
+
679
+ // Then: the workspace build should be canceled.
680
+ build , err = client .WorkspaceBuild (ctx , build .ID )
681
+ require .NoError (t , err )
682
+ require .Equal (t , codersdk .ProvisionerJobCanceled , build .Job .Status )
683
+ })
684
+
685
+ t .Run ("Cancel with expect_state=pending - should fail with 412" , func (t * testing.T ) {
686
+ t .Parallel ()
687
+ if ! dbtestutil .WillUsePostgres () {
688
+ t .Skip ("this test requires postgres" )
689
+ }
690
+
691
+ // Given: a coderd instance with a provisioner daemon
692
+ store , ps , db := dbtestutil .NewDBWithSQLDB (t )
693
+ client , closeDaemon , api := coderdtest .NewWithAPI (t , & coderdtest.Options {
694
+ Database : store ,
695
+ Pubsub : ps ,
696
+ IncludeProvisionerDaemon : true ,
697
+ })
698
+ defer closeDaemon .Close ()
699
+
700
+ // Given: a user, template, and workspace
701
+ user := coderdtest .CreateFirstUser (t , client )
702
+ version := coderdtest .CreateTemplateVersion (t , client , user .OrganizationID , nil )
703
+ coderdtest .AwaitTemplateVersionJobCompleted (t , client , version .ID )
704
+ template := coderdtest .CreateTemplate (t , client , user .OrganizationID , version .ID )
705
+ workspace := coderdtest .CreateWorkspace (t , client , template .ID )
706
+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , workspace .LatestBuild .ID )
707
+
708
+ // Stop the provisioner daemon.
709
+ require .NoError (t , closeDaemon .Close ())
710
+ ctx := testutil .Context (t , testutil .WaitLong )
711
+
712
+ // Given: no provisioner daemons exist.
713
+ _ , err := db .ExecContext (ctx , `DELETE FROM provisioner_daemons;` )
714
+ require .NoError (t , err )
715
+
716
+ // When: a new workspace build is created
717
+ build , err := client .CreateWorkspaceBuild (ctx , workspace .ID , codersdk.CreateWorkspaceBuildRequest {
718
+ TemplateVersionID : template .ActiveVersionID ,
719
+ Transition : codersdk .WorkspaceTransitionStart ,
720
+ })
721
+ // Then: the request should succeed.
722
+ require .NoError (t , err )
723
+ // Then: the provisioner job should remain pending.
724
+ require .Equal (t , codersdk .ProvisionerJobPending , build .Job .Status )
725
+
726
+ // When: a provisioner daemon is started
727
+ daemon := coderdtest .NewProvisionerDaemon (t , api )
728
+ defer daemon .Close ()
729
+ // Then: the job should be acquired (status changes from pending)
730
+ require .Eventually (t , func () bool {
731
+ build , err = client .WorkspaceBuild (ctx , build .ID )
732
+ return err == nil && build .Job .Status != codersdk .ProvisionerJobPending
733
+ }, testutil .WaitShort , testutil .IntervalFast )
734
+
735
+ // When: a cancel request is made with expect_state=pending
736
+ err = client .CancelWorkspaceBuild (ctx , build .ID , codersdk.CancelWorkspaceBuildRequest {
737
+ ExpectState : codersdk .ProvisionerJobPending ,
738
+ })
739
+ // Then: the request should fail with 412.
740
+ require .Error (t , err )
741
+ require .Equal (t , http .StatusPreconditionFailed , err .(* codersdk.Error ).StatusCode ())
742
+ })
743
+
744
+ t .Run ("Cancel with expect_state - invalid status" , func (t * testing.T ) {
745
+ t .Parallel ()
746
+
747
+ // Given: a coderd instance with a provisioner daemon
748
+ client := coderdtest .New (t , & coderdtest.Options {IncludeProvisionerDaemon : true })
749
+ user := coderdtest .CreateFirstUser (t , client )
750
+ version := coderdtest .CreateTemplateVersion (t , client , user .OrganizationID , & echo.Responses {
751
+ Parse : echo .ParseComplete ,
752
+ ProvisionApply : []* proto.Response {{
753
+ Type : & proto.Response_Log {
754
+ Log : & proto.Log {},
755
+ },
756
+ }},
757
+ ProvisionPlan : echo .PlanComplete ,
758
+ })
759
+ coderdtest .AwaitTemplateVersionJobCompleted (t , client , version .ID )
760
+ template := coderdtest .CreateTemplate (t , client , user .OrganizationID , version .ID )
761
+ workspace := coderdtest .CreateWorkspace (t , client , template .ID )
762
+
763
+ ctx , cancel := context .WithTimeout (context .Background (), testutil .WaitLong )
764
+ defer cancel ()
765
+
766
+ // When: a cancel request is made with invalid expect_state
767
+ err := client .CancelWorkspaceBuild (ctx , workspace .LatestBuild .ID , codersdk.CancelWorkspaceBuildRequest {
768
+ ExpectState : "invalid_status" ,
769
+ })
770
+ // Then: the request should fail with 400.
771
+ var apiErr * codersdk.Error
772
+ require .ErrorAs (t , err , & apiErr )
773
+ require .Equal (t , http .StatusBadRequest , apiErr .StatusCode ())
774
+ require .Contains (t , apiErr .Message , "Invalid expect_state" )
775
+ })
626
776
}
627
777
628
778
func TestWorkspaceBuildResources (t * testing.T ) {
@@ -968,7 +1118,7 @@ func TestWorkspaceBuildStatus(t *testing.T) {
968
1118
_ = closeDaemon .Close ()
969
1119
// after successful cancel is "canceled"
970
1120
build = coderdtest .CreateWorkspaceBuild (t , client , workspace , database .WorkspaceTransitionStart )
971
- err = client .CancelWorkspaceBuild (ctx , build .ID )
1121
+ err = client .CancelWorkspaceBuild (ctx , build .ID , codersdk. CancelWorkspaceBuildRequest {} )
972
1122
require .NoError (t , err )
973
1123
974
1124
workspace , err = client .Workspace (ctx , workspace .ID )
0 commit comments