@@ -1764,3 +1764,177 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) {
17641764
17651765assert .Len (t ,stats .Transitions ,1 ,"should create builds when provisioners are available" )
17661766}
1767+
1768+ func TestExecutorTaskWorkspace (t * testing.T ) {
1769+ t .Parallel ()
1770+
1771+ createTaskTemplate := func (t * testing.T ,client * codersdk.Client ,orgID uuid.UUID ,ctx context.Context ,defaultTTL time.Duration ) codersdk.Template {
1772+ t .Helper ()
1773+
1774+ taskAppID := uuid .New ()
1775+ version := coderdtest .CreateTemplateVersion (t ,client ,orgID ,& echo.Responses {
1776+ Parse :echo .ParseComplete ,
1777+ ProvisionPlan : []* proto.Response {
1778+ {
1779+ Type :& proto.Response_Plan {
1780+ Plan :& proto.PlanComplete {HasAiTasks :true },
1781+ },
1782+ },
1783+ },
1784+ ProvisionApply : []* proto.Response {
1785+ {
1786+ Type :& proto.Response_Apply {
1787+ Apply :& proto.ApplyComplete {
1788+ Resources : []* proto.Resource {
1789+ {
1790+ Agents : []* proto.Agent {
1791+ {
1792+ Id :uuid .NewString (),
1793+ Name :"dev" ,
1794+ Auth :& proto.Agent_Token {
1795+ Token :uuid .NewString (),
1796+ },
1797+ Apps : []* proto.App {
1798+ {
1799+ Id :taskAppID .String (),
1800+ Slug :"task-app" ,
1801+ },
1802+ },
1803+ },
1804+ },
1805+ },
1806+ },
1807+ AiTasks : []* proto.AITask {
1808+ {
1809+ AppId :taskAppID .String (),
1810+ },
1811+ },
1812+ },
1813+ },
1814+ },
1815+ },
1816+ })
1817+ coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
1818+ template := coderdtest .CreateTemplate (t ,client ,orgID ,version .ID )
1819+
1820+ if defaultTTL > 0 {
1821+ _ ,err := client .UpdateTemplateMeta (ctx ,template .ID , codersdk.UpdateTemplateMeta {
1822+ DefaultTTLMillis :defaultTTL .Milliseconds (),
1823+ })
1824+ require .NoError (t ,err )
1825+ }
1826+
1827+ return template
1828+ }
1829+
1830+ createTaskWorkspace := func (t * testing.T ,client * codersdk.Client ,template codersdk.Template ,ctx context.Context ,input string ) codersdk.Workspace {
1831+ t .Helper ()
1832+
1833+ exp := codersdk .NewExperimentalClient (client )
1834+ task ,err := exp .CreateTask (ctx ,"me" , codersdk.CreateTaskRequest {
1835+ TemplateVersionID :template .ActiveVersionID ,
1836+ Input :input ,
1837+ })
1838+ require .NoError (t ,err )
1839+ require .True (t ,task .WorkspaceID .Valid ,"task should have a workspace" )
1840+
1841+ workspace ,err := client .Workspace (ctx ,task .WorkspaceID .UUID )
1842+ require .NoError (t ,err )
1843+ coderdtest .AwaitWorkspaceBuildJobCompleted (t ,client ,workspace .LatestBuild .ID )
1844+
1845+ return workspace
1846+ }
1847+
1848+ t .Run ("Autostart" ,func (t * testing.T ) {
1849+ t .Parallel ()
1850+
1851+ var (
1852+ sched = mustSchedule (t ,"CRON_TZ=UTC 0 * * * *" )
1853+ tickCh = make (chan time.Time )
1854+ statsCh = make (chan autobuild.Stats )
1855+ client ,db = coderdtest .NewWithDatabase (t ,& coderdtest.Options {
1856+ AutobuildTicker :tickCh ,
1857+ IncludeProvisionerDaemon :true ,
1858+ AutobuildStats :statsCh ,
1859+ })
1860+ )
1861+
1862+ admin := coderdtest .CreateFirstUser (t ,client )
1863+ ctx := testutil .Context (t ,testutil .WaitLong )
1864+
1865+ // Given: A task workspace
1866+ template := createTaskTemplate (t ,client ,admin .OrganizationID ,ctx ,0 )
1867+ workspace := createTaskWorkspace (t ,client ,template ,ctx ,"test task for autostart" )
1868+
1869+ // Given: The task workspace has an autostart schedule
1870+ err := client .UpdateWorkspaceAutostart (ctx ,workspace .ID , codersdk.UpdateWorkspaceAutostartRequest {
1871+ Schedule :ptr .Ref (sched .String ()),
1872+ })
1873+ require .NoError (t ,err )
1874+
1875+ // Given: That the workspace is in a stopped state.
1876+ workspace = coderdtest .MustTransitionWorkspace (t ,client ,workspace .ID ,codersdk .WorkspaceTransitionStart ,codersdk .WorkspaceTransitionStop )
1877+
1878+ p ,err := coderdtest .GetProvisionerForTags (db ,time .Now (),workspace .OrganizationID ,map [string ]string {})
1879+ require .NoError (t ,err )
1880+
1881+ // When: the autobuild executor ticks after the scheduled time
1882+ go func () {
1883+ tickTime := sched .Next (workspace .LatestBuild .CreatedAt )
1884+ coderdtest .UpdateProvisionerLastSeenAt (t ,db ,p .ID ,tickTime )
1885+ tickCh <- tickTime
1886+ close (tickCh )
1887+ }()
1888+
1889+ // Then: We expect to see a start transition
1890+ stats := <- statsCh
1891+ require .Len (t ,stats .Transitions ,1 ,"lifecycle executor should transition the task workspace" )
1892+ assert .Contains (t ,stats .Transitions ,workspace .ID ,"task workspace should be in transitions" )
1893+ assert .Equal (t ,database .WorkspaceTransitionStart ,stats .Transitions [workspace .ID ],"should autostart the workspace" )
1894+ require .Empty (t ,stats .Errors ,"should have no errors when managing task workspaces" )
1895+ })
1896+
1897+ t .Run ("Autostop" ,func (t * testing.T ) {
1898+ t .Parallel ()
1899+
1900+ var (
1901+ tickCh = make (chan time.Time )
1902+ statsCh = make (chan autobuild.Stats )
1903+ client ,db = coderdtest .NewWithDatabase (t ,& coderdtest.Options {
1904+ AutobuildTicker :tickCh ,
1905+ IncludeProvisionerDaemon :true ,
1906+ AutobuildStats :statsCh ,
1907+ })
1908+ )
1909+
1910+ admin := coderdtest .CreateFirstUser (t ,client )
1911+ ctx := testutil .Context (t ,testutil .WaitLong )
1912+
1913+ // Given: A task workspace with an 8 hour deadline
1914+ template := createTaskTemplate (t ,client ,admin .OrganizationID ,ctx ,8 * time .Hour )
1915+ workspace := createTaskWorkspace (t ,client ,template ,ctx ,"test task for autostop" )
1916+
1917+ // Given: The workspace is currently running
1918+ workspace = coderdtest .MustWorkspace (t ,client ,workspace .ID )
1919+ require .Equal (t ,codersdk .WorkspaceTransitionStart ,workspace .LatestBuild .Transition )
1920+ require .NotZero (t ,workspace .LatestBuild .Deadline ,"workspace should have a deadline for autostop" )
1921+
1922+ p ,err := coderdtest .GetProvisionerForTags (db ,time .Now (),workspace .OrganizationID ,map [string ]string {})
1923+ require .NoError (t ,err )
1924+
1925+ // When: the autobuild executor ticks after the deadline
1926+ go func () {
1927+ tickTime := workspace .LatestBuild .Deadline .Time .Add (time .Minute )
1928+ coderdtest .UpdateProvisionerLastSeenAt (t ,db ,p .ID ,tickTime )
1929+ tickCh <- tickTime
1930+ close (tickCh )
1931+ }()
1932+
1933+ // Then: We expect to see a stop transition
1934+ stats := <- statsCh
1935+ require .Len (t ,stats .Transitions ,1 ,"lifecycle executor should transition the task workspace" )
1936+ assert .Contains (t ,stats .Transitions ,workspace .ID ,"task workspace should be in transitions" )
1937+ assert .Equal (t ,database .WorkspaceTransitionStop ,stats .Transitions [workspace .ID ],"should autostop the workspace" )
1938+ require .Empty (t ,stats .Errors ,"should have no errors when managing task workspaces" )
1939+ })
1940+ }