@@ -392,6 +392,135 @@ func mustCreateAgentLogs(ctx context.Context, t *testing.T, db database.Store, a
392392require .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