@@ -3,6 +3,7 @@ package coderd_test
33import (
44 "bytes"
55 "context"
6+ "database/sql"
67 "encoding/json"
78 "fmt"
89 "io"
@@ -21,6 +22,7 @@ import (
2122 "github.com/stretchr/testify/assert"
2223 "github.com/stretchr/testify/require"
2324 "go.uber.org/goleak"
25+ "go.uber.org/mock/gomock"
2426
2527 "cdr.dev/slog"
2628 "cdr.dev/slog/sloggers/slogtest"
@@ -39,13 +41,16 @@ import (
3941 "github.com/coder/retry"
4042 "github.com/coder/serpent"
4143
44+ agplcoderd "github.com/coder/coder/v2/coderd"
4245 agplaudit "github.com/coder/coder/v2/coderd/audit"
4346 "github.com/coder/coder/v2/coderd/coderdtest"
4447 "github.com/coder/coder/v2/coderd/database"
4548 "github.com/coder/coder/v2/coderd/database/dbauthz"
4649 "github.com/coder/coder/v2/coderd/database/dbfake"
50+ "github.com/coder/coder/v2/coderd/database/dbmock"
4751 "github.com/coder/coder/v2/coderd/database/dbtestutil"
4852 "github.com/coder/coder/v2/coderd/database/dbtime"
53+ "github.com/coder/coder/v2/coderd/entitlements"
4954 "github.com/coder/coder/v2/coderd/rbac"
5055 "github.com/coder/coder/v2/codersdk"
5156 "github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -635,18 +640,18 @@ func TestManagedAgentLimit(t *testing.T) {
635640 })
636641
637642 // Get entitlements to check that the license is a-ok.
638- entitlements , err := cli .Entitlements (ctx ) //nolint:gocritic // we're not testing authz on the entitlements endpoint, so using owner is fine
643+ sdkEntitlements , err := cli .Entitlements (ctx ) //nolint:gocritic // we're not testing authz on the entitlements endpoint, so using owner is fine
639644 require .NoError (t , err )
640- require .True (t , entitlements .HasLicense )
641- agentLimit := entitlements .Features [codersdk .FeatureManagedAgentLimit ]
645+ require .True (t , sdkEntitlements .HasLicense )
646+ agentLimit := sdkEntitlements .Features [codersdk .FeatureManagedAgentLimit ]
642647 require .True (t , agentLimit .Enabled )
643648 require .NotNil (t , agentLimit .Limit )
644649 require .EqualValues (t , 1 , * agentLimit .Limit )
645650 require .NotNil (t , agentLimit .SoftLimit )
646651 require .EqualValues (t , 1 , * agentLimit .SoftLimit )
647- require .Empty (t , entitlements .Errors )
652+ require .Empty (t , sdkEntitlements .Errors )
648653 // There should be a warning since we're really close to our agent limit.
649- require .Equal (t , entitlements .Warnings [0 ], "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information." )
654+ require .Equal (t , sdkEntitlements .Warnings [0 ], "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information." )
650655
651656 // Create a fake provision response that claims there are agents in the
652657 // template and every built workspace.
@@ -723,6 +728,69 @@ func TestManagedAgentLimit(t *testing.T) {
723728 coderdtest .AwaitWorkspaceBuildJobCompleted (t , cli , workspace .LatestBuild .ID )
724729}
725730
731+ func TestCheckBuildUsage_SkipsAIForNonStartTransitions (t * testing.T ) {
732+ t .Parallel ()
733+ ctrl := gomock .NewController (t )
734+ defer ctrl .Finish ()
735+
736+ // Prepare entitlements with a managed agent limit to enforce.
737+ entSet := entitlements .New ()
738+ entSet .Modify (func (e * codersdk.Entitlements ) {
739+ e .HasLicense = true
740+ limit := int64 (1 )
741+ issuedAt := time .Now ().Add (- 2 * time .Hour )
742+ start := time .Now ().Add (- time .Hour )
743+ end := time .Now ().Add (time .Hour )
744+ e .Features [codersdk .FeatureManagedAgentLimit ] = codersdk.Feature {
745+ Enabled : true ,
746+ Limit : & limit ,
747+ UsagePeriod : & codersdk.UsagePeriod {IssuedAt : issuedAt , Start : start , End : end },
748+ }
749+ })
750+
751+ // Enterprise API instance with entitlements injected.
752+ agpl := & agplcoderd.API {
753+ Options : & agplcoderd.Options {
754+ Entitlements : entSet ,
755+ },
756+ }
757+ eapi := & coderd.API {
758+ AGPL : agpl ,
759+ Options : & coderd.Options {Options : agpl .Options },
760+ }
761+
762+ // Template version that has an AI task.
763+ tv := & database.TemplateVersion {
764+ HasAITask : sql.NullBool {Valid : true , Bool : true },
765+ HasExternalAgent : sql.NullBool {Valid : true , Bool : false },
766+ }
767+
768+ // Mock DB: expect exactly one count call for the "start" transition.
769+ mDB := dbmock .NewMockStore (ctrl )
770+ mDB .EXPECT ().
771+ GetTotalUsageDCManagedAgentsV1 (gomock .Any (), gomock .Any ()).
772+ Times (1 ).
773+ Return (int64 (1 ), nil ) // equal to limit -> should breach
774+
775+ ctx := context .Background ()
776+
777+ // Start transition: should be not permitted due to limit breach.
778+ startResp , err := eapi .CheckBuildUsage (ctx , mDB , tv , database .WorkspaceTransitionStart )
779+ require .NoError (t , err )
780+ require .False (t , startResp .Permitted )
781+ require .Contains (t , startResp .Message , "breached the managed agent limit" )
782+
783+ // Stop transition: should be permitted and must not trigger additional DB calls.
784+ stopResp , err := eapi .CheckBuildUsage (ctx , mDB , tv , database .WorkspaceTransitionStop )
785+ require .NoError (t , err )
786+ require .True (t , stopResp .Permitted )
787+
788+ // Delete transition: should be permitted and must not trigger additional DB calls.
789+ deleteResp , err := eapi .CheckBuildUsage (ctx , mDB , tv , database .WorkspaceTransitionDelete )
790+ require .NoError (t , err )
791+ require .True (t , deleteResp .Permitted )
792+ }
793+
726794// testDBAuthzRole returns a context with a subject that has a role
727795// with permissions required for test setup.
728796func testDBAuthzRole (ctx context.Context ) context.Context {
0 commit comments