@@ -392,6 +392,135 @@ func mustCreateAgentLogs(ctx context.Context, t *testing.T, db database.Store, a
392392 require .NotEmpty (t , agentLogs , "agent logs must be present" )
393393}
394394
395+ func TestDeleteOldWorkspaceAgentLogsRetention (t * testing.T ) {
396+ t .Parallel ()
397+
398+ t .Run ("RetentionEnabled" , func (t * testing.T ) {
399+ t .Parallel ()
400+
401+ ctx := testutil .Context (t , testutil .WaitShort )
402+
403+ clk := quartz .NewMock (t )
404+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
405+ retentionPeriod := 7 * 24 * time .Hour
406+ oldTime := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // 8 days ago (should be deleted)
407+ clk .Set (now ).MustWait (ctx )
408+
409+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
410+ org := dbgen .Organization (t , db , database.Organization {})
411+ user := dbgen .User (t , db , database.User {})
412+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
413+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
414+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
415+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
416+
417+ ws := dbgen .Workspace (t , db , database.WorkspaceTable {Name : "test-ws" , OwnerID : user .ID , OrganizationID : org .ID , TemplateID : tmpl .ID })
418+ wb1 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 1 )
419+ wb2 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 2 )
420+ agent1 := mustCreateAgent (t , db , wb1 )
421+ agent2 := mustCreateAgent (t , db , wb2 )
422+ mustCreateAgentLogs (ctx , t , db , agent1 , & oldTime , "agent 1 logs" )
423+ mustCreateAgentLogs (ctx , t , db , agent2 , & oldTime , "agent 2 logs" )
424+
425+ done := awaitDoTick (ctx , t , clk )
426+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
427+ Retention : codersdk.RetentionConfig {
428+ WorkspaceAgentLogs : serpent .Duration (retentionPeriod ),
429+ },
430+ }, clk )
431+ defer closer .Close ()
432+ testutil .TryReceive (ctx , t , done )
433+
434+ // Non-latest build logs should be deleted.
435+ assertNoWorkspaceAgentLogs (ctx , t , db , agent1 .ID )
436+ // Latest build logs should be retained.
437+ assertWorkspaceAgentLogs (ctx , t , db , agent2 .ID , "agent 2 logs" )
438+ })
439+
440+ t .Run ("RetentionDisabled" , func (t * testing.T ) {
441+ t .Parallel ()
442+
443+ ctx := testutil .Context (t , testutil .WaitShort )
444+
445+ clk := quartz .NewMock (t )
446+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
447+ oldTime := now .Add (- 60 * 24 * time .Hour ) // 60 days ago
448+ clk .Set (now ).MustWait (ctx )
449+
450+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
451+ org := dbgen .Organization (t , db , database.Organization {})
452+ user := dbgen .User (t , db , database.User {})
453+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
454+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
455+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
456+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
457+
458+ ws := dbgen .Workspace (t , db , database.WorkspaceTable {Name : "test-ws" , OwnerID : user .ID , OrganizationID : org .ID , TemplateID : tmpl .ID })
459+ wb1 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 1 )
460+ wb2 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 2 )
461+ agent1 := mustCreateAgent (t , db , wb1 )
462+ agent2 := mustCreateAgent (t , db , wb2 )
463+ mustCreateAgentLogs (ctx , t , db , agent1 , & oldTime , "agent 1 logs" )
464+ mustCreateAgentLogs (ctx , t , db , agent2 , & oldTime , "agent 2 logs" )
465+
466+ done := awaitDoTick (ctx , t , clk )
467+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
468+ Retention : codersdk.RetentionConfig {
469+ WorkspaceAgentLogs : serpent .Duration (0 ), // disabled
470+ },
471+ }, clk )
472+ defer closer .Close ()
473+ testutil .TryReceive (ctx , t , done )
474+
475+ // All logs should be retained when retention is disabled.
476+ assertWorkspaceAgentLogs (ctx , t , db , agent1 .ID , "agent 1 logs" )
477+ assertWorkspaceAgentLogs (ctx , t , db , agent2 .ID , "agent 2 logs" )
478+ })
479+
480+ t .Run ("GlobalRetentionFallback" , func (t * testing.T ) {
481+ t .Parallel ()
482+
483+ ctx := testutil .Context (t , testutil .WaitShort )
484+
485+ clk := quartz .NewMock (t )
486+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
487+ retentionPeriod := 14 * 24 * time .Hour
488+ oldTime := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // 15 days ago
489+ clk .Set (now ).MustWait (ctx )
490+
491+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
492+ org := dbgen .Organization (t , db , database.Organization {})
493+ user := dbgen .User (t , db , database.User {})
494+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
495+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
496+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
497+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
498+
499+ ws := dbgen .Workspace (t , db , database.WorkspaceTable {Name : "test-ws" , OwnerID : user .ID , OrganizationID : org .ID , TemplateID : tmpl .ID })
500+ wb1 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 1 )
501+ wb2 := mustCreateWorkspaceBuild (t , db , org , tv , ws .ID , oldTime , 2 )
502+ agent1 := mustCreateAgent (t , db , wb1 )
503+ agent2 := mustCreateAgent (t , db , wb2 )
504+ mustCreateAgentLogs (ctx , t , db , agent1 , & oldTime , "agent 1 logs" )
505+ mustCreateAgentLogs (ctx , t , db , agent2 , & oldTime , "agent 2 logs" )
506+
507+ done := awaitDoTick (ctx , t , clk )
508+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
509+ Retention : codersdk.RetentionConfig {
510+ Global : serpent .Duration (retentionPeriod ), // Use global
511+ WorkspaceAgentLogs : serpent .Duration (0 ), // Not set, falls back to global
512+ },
513+ }, clk )
514+ defer closer .Close ()
515+ testutil .TryReceive (ctx , t , done )
516+
517+ // Non-latest build logs should be deleted via global retention.
518+ assertNoWorkspaceAgentLogs (ctx , t , db , agent1 .ID )
519+ // Latest build logs should be retained.
520+ assertWorkspaceAgentLogs (ctx , t , db , agent2 .ID , "agent 2 logs" )
521+ })
522+ }
523+
395524//nolint:paralleltest // It uses LockIDDBPurge.
396525func TestDeleteOldProvisionerDaemons (t * testing.T ) {
397526 // TODO: must refactor DeleteOldProvisionerDaemons to allow passing in cutoff
0 commit comments