@@ -676,6 +676,8 @@ func TestExecuteAutostopSuspendedUser(t *testing.T) {
676676)
677677
678678admin := coderdtest .CreateFirstUser (t ,client )
679+ // Wait for provisioner to be available
680+ mustWaitForProvisionersWithClient (t ,client )
679681version := coderdtest .CreateTemplateVersion (t ,client ,admin .OrganizationID ,nil )
680682coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
681683template := coderdtest .CreateTemplate (t ,client ,admin .OrganizationID ,version .ID )
@@ -976,6 +978,8 @@ func TestExecutorRequireActiveVersion(t *testing.T) {
976978TemplateScheduleStore :schedule .NewAGPLTemplateScheduleStore (),
977979})
978980)
981+ // Wait for provisioner to be available
982+ mustWaitForProvisionersWithClient (t ,ownerClient )
979983ctx := testutil .Context (t ,testutil .WaitShort )
980984owner := coderdtest .CreateFirstUser (t ,ownerClient )
981985me ,err := ownerClient .User (ctx ,codersdk .Me )
@@ -1543,6 +1547,25 @@ func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*
15431547return coderdtest .MustWorkspace (t ,client ,ws .ID )
15441548}
15451549
1550+ // mustProvisionWorkspaceWithProvisionerTags creates a workspace with a template version that has specific provisioner tags
1551+ func mustProvisionWorkspaceWithProvisionerTags (t * testing.T ,client * codersdk.Client ,provisionerTags map [string ]string ,mut ... func (* codersdk.CreateWorkspaceRequest )) codersdk.Workspace {
1552+ t .Helper ()
1553+ user := coderdtest .CreateFirstUser (t ,client )
1554+
1555+ // Create template version with specific provisioner tags
1556+ version := coderdtest .CreateTemplateVersion (t ,client ,user .OrganizationID ,nil ,func (request * codersdk.CreateTemplateVersionRequest ) {
1557+ request .ProvisionerTags = provisionerTags
1558+ })
1559+ coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
1560+ t .Logf ("template version %s job has completed with provisioner tags %v" ,version .ID ,provisionerTags )
1561+
1562+ template := coderdtest .CreateTemplate (t ,client ,user .OrganizationID ,version .ID )
1563+
1564+ ws := coderdtest .CreateWorkspace (t ,client ,template .ID ,mut ... )
1565+ coderdtest .AwaitWorkspaceBuildJobCompleted (t ,client ,ws .LatestBuild .ID )
1566+ return coderdtest .MustWorkspace (t ,client ,ws .ID )
1567+ }
1568+
15461569func mustProvisionWorkspaceWithParameters (t * testing.T ,client * codersdk.Client ,richParameters []* proto.RichParameter ,mut ... func (* codersdk.CreateWorkspaceRequest )) codersdk.Workspace {
15471570t .Helper ()
15481571user := coderdtest .CreateFirstUser (t ,client )
@@ -1580,6 +1603,71 @@ func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID
15801603require .NotEmpty (t ,buildParameters )
15811604}
15821605
1606+ func mustWaitForProvisioners (t * testing.T ,db database.Store ) {
1607+ t .Helper ()
1608+ ctx := testutil .Context (t ,testutil .WaitShort )
1609+ require .Eventually (t ,func ()bool {
1610+ daemons ,err := db .GetProvisionerDaemons (ctx )
1611+ return err == nil && len (daemons )> 0
1612+ },testutil .WaitShort ,testutil .IntervalFast )
1613+ }
1614+
1615+ func mustWaitForProvisionersWithClient (t * testing.T ,client * codersdk.Client ) {
1616+ t .Helper ()
1617+ ctx := testutil .Context (t ,testutil .WaitShort )
1618+ require .Eventually (t ,func ()bool {
1619+ daemons ,err := client .ProvisionerDaemons (ctx )
1620+ return err == nil && len (daemons )> 0
1621+ },testutil .WaitShort ,testutil .IntervalFast )
1622+ }
1623+
1624+ // mustWaitForProvisionersAvailable waits for provisioners to be available for a specific workspace
1625+ func mustWaitForProvisionersAvailable (t * testing.T ,db database.Store ,workspace codersdk.Workspace ) {
1626+ t .Helper ()
1627+ ctx := testutil .Context (t ,testutil .WaitShort )
1628+
1629+ // Get the workspace from the database
1630+ require .Eventually (t ,func ()bool {
1631+ ws ,err := db .GetWorkspaceByID (ctx ,workspace .ID )
1632+ if err != nil {
1633+ return false
1634+ }
1635+
1636+ // Get the latest build
1637+ latestBuild ,err := db .GetWorkspaceBuildByID (ctx ,workspace .LatestBuild .ID )
1638+ if err != nil {
1639+ return false
1640+ }
1641+
1642+ // Get the template version job
1643+ templateVersionJob ,err := db .GetProvisionerJobByID (ctx ,latestBuild .JobID )
1644+ if err != nil {
1645+ return false
1646+ }
1647+
1648+ // Check if provisioners are available using the same logic as hasAvailableProvisioners
1649+ provisionerDaemons ,err := db .GetProvisionerDaemonsByOrganization (ctx , database.GetProvisionerDaemonsByOrganizationParams {
1650+ OrganizationID :ws .OrganizationID ,
1651+ WantTags :templateVersionJob .Tags ,
1652+ })
1653+ if err != nil {
1654+ return false
1655+ }
1656+
1657+ // Check if any provisioners are active (not stale)
1658+ now := time .Now ()
1659+ for _ ,pd := range provisionerDaemons {
1660+ if pd .LastSeenAt .Valid {
1661+ age := now .Sub (pd .LastSeenAt .Time )
1662+ if age <= autobuild .TestingStaleInterval {
1663+ return true // Found an active provisioner
1664+ }
1665+ }
1666+ }
1667+ return false // No active provisioners found
1668+ },testutil .WaitShort ,testutil .IntervalFast )
1669+ }
1670+
15831671func TestMain (m * testing.M ) {
15841672goleak .VerifyTestMain (m ,testutil .GoleakOptions ... )
15851673}
@@ -1595,17 +1683,20 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
15951683)
15961684
15971685// Create client with provisioner closer
1686+ provisionerDaemonTags := map [string ]string {"owner" :"testowner" ,"scope" :"organization" }
1687+ t .Logf ("Setting provisioner daemon tags: %v" ,provisionerDaemonTags )
15981688client ,provisionerCloser := coderdtest .NewWithProvisionerCloser (t ,& coderdtest.Options {
15991689AutobuildTicker :tickCh ,
16001690IncludeProvisionerDaemon :true ,
16011691AutobuildStats :statsCh ,
16021692Database :db ,
16031693Pubsub :ps ,
1604- SkipProvisionerCheck :ptr . Ref ( false ) ,
1694+ ProvisionerDaemonTags :provisionerDaemonTags ,
16051695})
16061696
1607- // Create workspace with autostart enabled
1608- workspace := mustProvisionWorkspace (t ,client ,func (cwr * codersdk.CreateWorkspaceRequest ) {
1697+ // Create workspace with autostart enabled and matching provisioner tags
1698+ provisionerTags := map [string ]string {"owner" :"testowner" ,"scope" :"organization" }
1699+ workspace := mustProvisionWorkspaceWithProvisionerTags (t ,client ,provisionerTags ,func (cwr * codersdk.CreateWorkspaceRequest ) {
16091700cwr .AutostartSchedule = ptr .Ref (sched .String ())
16101701})
16111702
@@ -1614,16 +1705,20 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
16141705codersdk .WorkspaceTransitionStart ,codersdk .WorkspaceTransitionStop )
16151706
16161707// Wait for provisioner to be registered
1617- ctx := testutil .Context (t ,testutil .WaitShort )
1618- require .Eventually (t ,func ()bool {
1619- daemons ,err := db .GetProvisionerDaemons (ctx )
1620- return err == nil && len (daemons )> 0
1621- },testutil .WaitShort ,testutil .IntervalFast )
1708+ mustWaitForProvisioners (t ,db )
1709+
1710+ // Wait for provisioner to be available for this specific workspace
1711+ mustWaitForProvisionersAvailable (t ,db ,workspace )
16221712
16231713// Now shut down the provisioner daemon
1714+ ctx := testutil .Context (t ,testutil .WaitShort )
16241715err := provisionerCloser .Close ()
16251716require .NoError (t ,err )
16261717
1718+ // Wait for the provisioner to become stale (heartbeat stops)
1719+ // The stale interval is 5 seconds, so we need to wait a bit longer
1720+ time .Sleep (6 * time .Second )
1721+
16271722// Debug: check what's in the database
16281723daemons ,err := db .GetProvisionerDaemons (ctx )
16291724require .NoError (t ,err )
@@ -1632,24 +1727,105 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
16321727t .Logf ("Daemon %d: ID=%s, Name=%s, LastSeen=%v" ,i ,daemon .ID ,daemon .Name ,daemon .LastSeenAt )
16331728}
16341729
1635- // Wait for provisioner to become stale (LastSeenAt > StaleInterval ago)
1636- require .Eventually (t ,func ()bool {
1637- daemons ,err := db .GetProvisionerDaemons (ctx )
1638- if err != nil || len (daemons )== 0 {
1639- return false
1730+ // Since we commented out the close call, the provisioner should NOT become stale
1731+ // Let's wait a bit but NOT longer than the stale interval
1732+ time .Sleep (2 * time .Second )// Wait less than the 5-second stale interval
1733+
1734+ daemons ,err = db .GetProvisionerDaemons (ctx )
1735+ require .NoError (t ,err )
1736+ require .NotEmpty (t ,daemons ,"should have provisioner daemons" )
1737+
1738+ now := time .Now ()
1739+ for _ ,daemon := range daemons {
1740+ if daemon .LastSeenAt .Valid {
1741+ age := now .Sub (daemon .LastSeenAt .Time )
1742+ t .Logf ("Daemon %s: age=%v, staleInterval=%v, isStale=%v" ,daemon .Name ,age ,autobuild .TestingStaleInterval ,age > autobuild .TestingStaleInterval )
1743+ // Since we closed the provisioner, it should be stale
1744+ require .True (t ,age > autobuild .TestingStaleInterval ,"provisioner should be stale since we closed it" )
16401745}
1746+ }
16411747
1642- now := time .Now ()
1643- for _ ,daemon := range daemons {
1644- if daemon .LastSeenAt .Valid {
1645- age := now .Sub (daemon .LastSeenAt .Time )
1646- if age > autobuild .TestingStaleInterval {
1647- return true // Provisioner is now stale
1648- }
1649- }
1748+ // Debug: Check the template version job tags and organization
1749+ templateVersion ,err := client .TemplateVersion (context .Background (),workspace .LatestBuild .TemplateVersionID )
1750+ require .NoError (t ,err )
1751+ t .Logf ("Template version job ID: %s" ,templateVersion .Job .ID )
1752+ t .Logf ("Template version job tags: %v" ,templateVersion .Job .Tags )
1753+ t .Logf ("Workspace organization ID: %s" ,workspace .OrganizationID )
1754+ t .Logf ("Template version organization ID: %s" ,templateVersion .OrganizationID )
1755+ t .Logf ("Workspace LatestBuild.TemplateVersionID: %s" ,workspace .LatestBuild .TemplateVersionID )
1756+
1757+ // Debug: Get the template version job directly from database to compare
1758+ templateVersionJobFromDB ,err := db .GetProvisionerJobByID (context .Background (),templateVersion .Job .ID )
1759+ require .NoError (t ,err )
1760+ t .Logf ("Template version job from DB - ID: %s, Tags: %v" ,templateVersionJobFromDB .ID ,templateVersionJobFromDB .Tags )
1761+
1762+ // Debug: Query database directly to see what provisioner daemons exist
1763+ allDaemons ,err := db .GetProvisionerDaemons (context .Background ())
1764+ require .NoError (t ,err )
1765+ t .Logf ("Total provisioner daemons in database: %d" ,len (allDaemons ))
1766+ for i ,daemon := range allDaemons {
1767+ t .Logf ("Daemon %d: ID=%s, Name=%s, OrgID=%s, Tags=%v" ,i ,daemon .ID ,daemon .Name ,daemon .OrganizationID ,daemon .Tags )
1768+ }
1769+
1770+ // Debug: Test if we can query using the ACTUAL provisioner daemon tags
1771+ if len (allDaemons )> 0 {
1772+ actualDaemonTags := allDaemons [0 ].Tags
1773+ t .Logf ("Testing query with ACTUAL daemon tags: %v" ,actualDaemonTags )
1774+ queryWithActualTags := database.GetProvisionerDaemonsByOrganizationParams {
1775+ OrganizationID :workspace .OrganizationID ,
1776+ WantTags :actualDaemonTags ,
1777+ }
1778+ matchingWithActualTags ,err := db .GetProvisionerDaemonsByOrganization (context .Background (),queryWithActualTags )
1779+ require .NoError (t ,err )
1780+ t .Logf ("Query with actual daemon tags returns: %d daemons" ,len (matchingWithActualTags ))
1781+ }
1782+
1783+ // Debug: Test the exact query that hasAvailableProvisioners uses
1784+ queryParams := database.GetProvisionerDaemonsByOrganizationParams {
1785+ OrganizationID :workspace .OrganizationID ,
1786+ WantTags :templateVersion .Job .Tags ,
1787+ }
1788+ t .Logf ("Query params: OrgID=%s, WantTags=%v" ,queryParams .OrganizationID ,queryParams .WantTags )
1789+ t .Logf ("Test query detailed params: org_id_type=%T, org_id_value=%s, want_tags_type=%T, want_tags_value=%v" ,
1790+ queryParams .OrganizationID ,queryParams .OrganizationID ,queryParams .WantTags ,queryParams .WantTags )
1791+
1792+ // Test 1: Transaction isolation - try with different transaction contexts
1793+ t .Logf ("=== Testing transaction isolation ===" )
1794+
1795+ // Query with context.Background() (what we used above)
1796+ matchingDaemons1 ,err := db .GetProvisionerDaemonsByOrganization (context .Background (),queryParams )
1797+ require .NoError (t ,err )
1798+ t .Logf ("With context.Background(): %d daemons" ,len (matchingDaemons1 ))
1799+
1800+ // Query with a fresh context
1801+ ctx2 := context .Background ()
1802+ matchingDaemons2 ,err2 := db .GetProvisionerDaemonsByOrganization (ctx2 ,queryParams )
1803+ require .NoError (t ,err2 )
1804+ t .Logf ("With fresh context: %d daemons" ,len (matchingDaemons2 ))
1805+
1806+ // Query within a transaction (like hasAvailableProvisioners might use)
1807+ err = db .InTx (func (tx database.Store )error {
1808+ matchingDaemons3 ,err := tx .GetProvisionerDaemonsByOrganization (ctx2 ,queryParams )
1809+ if err != nil {
1810+ return err
16501811}
1651- return false
1652- },testutil .WaitLong ,testutil .IntervalMedium )// Use longer timeout since we need to wait for staleness
1812+ t .Logf ("Within transaction: %d daemons" ,len (matchingDaemons3 ))
1813+ return nil
1814+ },nil )
1815+ require .NoError (t ,err )
1816+
1817+ // Test 2: Timing issue - query right before and after hasAvailableProvisioners
1818+ t .Logf ("=== Testing timing issue ===" )
1819+
1820+ // Query right before the autostart trigger
1821+ beforeDaemons ,err := db .GetProvisionerDaemonsByOrganization (context .Background (),queryParams )
1822+ require .NoError (t ,err )
1823+ t .Logf ("Right before autostart: %d daemons" ,len (beforeDaemons ))
1824+
1825+ // Since we commented out the close call, the provisioner is still active
1826+ // This means autobuild should proceed (not be skipped) and create transitions
1827+ // The test expects 0 transitions (skipped autostart), but with an active provisioner,
1828+ // it should get >0 transitions, causing the test to FAIL as intended
16531829
16541830// Trigger autobuild
16551831go func () {
@@ -1658,5 +1834,14 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
16581834}()
16591835
16601836stats := <- statsCh
1837+
1838+ // Query right after hasAvailableProvisioners was called
1839+ afterDaemons ,err := db .GetProvisionerDaemonsByOrganization (context .Background (),queryParams )
1840+ require .NoError (t ,err )
1841+ t .Logf ("Right after autostart: %d daemons" ,len (afterDaemons ))
1842+
1843+ // This assertion should FAIL when provisioner is available (demonstrating the fix works)
1844+ // When provisioner close is commented out: provisioner available → autostart proceeds → transitions > 0 → test fails ✓
1845+ // When provisioner close is active: provisioner unavailable → autostart skipped → transitions = 0 → test passes ✓
16611846assert .Len (t ,stats .Transitions ,0 ,"should not create builds when no provisioners available" )
16621847}