@@ -392,6 +392,103 @@ func mustCreateAgentLogs(ctx context.Context, t *testing.T, db database.Store, a
392392require .NotEmpty (t ,agentLogs ,"agent logs must be present" )
393393}
394394
395+ //nolint:paralleltest // It uses LockIDDBPurge.
396+ func TestDeleteOldWorkspaceAgentLogsWithCustomRetention (t * testing.T ) {
397+ t .Run ("CustomRetention30Days" ,func (t * testing.T ) {
398+ ctx := testutil .Context (t ,testutil .WaitShort )
399+ clk := quartz .NewMock (t )
400+ now := dbtime .Now ()
401+ retentionPeriod := 30 * 24 * time .Hour
402+ threshold := now .Add (- retentionPeriod )
403+ beforeThreshold := threshold .Add (- 24 * time .Hour )// 31 days ago
404+ afterThreshold := threshold .Add (24 * time .Hour )// 29 days ago
405+ clk .Set (now ).MustWait (ctx )
406+
407+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
408+ org := dbgen .Organization (t ,db , database.Organization {})
409+ user := dbgen .User (t ,db , database.User {})
410+ _ = dbgen .OrganizationMember (t ,db , database.OrganizationMember {UserID :user .ID ,OrganizationID :org .ID })
411+ tv := dbgen .TemplateVersion (t ,db , database.TemplateVersion {OrganizationID :org .ID ,CreatedBy :user .ID })
412+ tmpl := dbgen .Template (t ,db , database.Template {OrganizationID :org .ID ,ActiveVersionID :tv .ID ,CreatedBy :user .ID })
413+
414+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
415+
416+ // Workspace with two builds, both before the 30-day threshold.
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 ,beforeThreshold ,1 )
419+ wb2 := mustCreateWorkspaceBuild (t ,db ,org ,tv ,ws .ID ,beforeThreshold ,2 )
420+ agent1 := mustCreateAgent (t ,db ,wb1 )
421+ agent2 := mustCreateAgent (t ,db ,wb2 )
422+ mustCreateAgentLogs (ctx ,t ,db ,agent1 ,& beforeThreshold ,"agent 1 logs should be deleted" )
423+ mustCreateAgentLogs (ctx ,t ,db ,agent2 ,& beforeThreshold ,"agent 2 logs should be retained" )
424+
425+ // Workspace with build after the 30-day threshold.
426+ wsRecent := dbgen .Workspace (t ,db , database.WorkspaceTable {Name :"recent-ws" ,OwnerID :user .ID ,OrganizationID :org .ID ,TemplateID :tmpl .ID })
427+ wbRecent := mustCreateWorkspaceBuild (t ,db ,org ,tv ,wsRecent .ID ,afterThreshold ,1 )
428+ agentRecent := mustCreateAgent (t ,db ,wbRecent )
429+ mustCreateAgentLogs (ctx ,t ,db ,agentRecent ,& afterThreshold ,"recent agent logs should be retained" )
430+
431+ done := awaitDoTick (ctx ,t ,clk )
432+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
433+ Retention : codersdk.RetentionConfig {
434+ WorkspaceAgentLogs :serpent .Duration (retentionPeriod ),
435+ },
436+ },clk )
437+ defer closer .Close ()
438+ testutil .TryReceive (ctx ,t ,done )
439+
440+ // Agent 1 logs should be deleted (non-latest build, older than 30 days).
441+ assertNoWorkspaceAgentLogs (ctx ,t ,db ,agent1 .ID )
442+ // Agent 2 logs should be retained (latest build).
443+ assertWorkspaceAgentLogs (ctx ,t ,db ,agent2 .ID ,"agent 2 logs should be retained" )
444+ // Recent agent logs should be retained (within 30-day threshold).
445+ assertWorkspaceAgentLogs (ctx ,t ,db ,agentRecent .ID ,"recent agent logs should be retained" )
446+ })
447+
448+ t .Run ("RetentionDisabled" ,func (t * testing.T ) {
449+ ctx := testutil .Context (t ,testutil .WaitShort )
450+ clk := quartz .NewMock (t )
451+ now := dbtime .Now ()
452+ // Very old logs (60 days ago).
453+ veryOld := now .Add (- 60 * 24 * time .Hour )
454+ clk .Set (now ).MustWait (ctx )
455+
456+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
457+ org := dbgen .Organization (t ,db , database.Organization {})
458+ user := dbgen .User (t ,db , database.User {})
459+ _ = dbgen .OrganizationMember (t ,db , database.OrganizationMember {UserID :user .ID ,OrganizationID :org .ID })
460+ tv := dbgen .TemplateVersion (t ,db , database.TemplateVersion {OrganizationID :org .ID ,CreatedBy :user .ID })
461+ tmpl := dbgen .Template (t ,db , database.Template {OrganizationID :org .ID ,ActiveVersionID :tv .ID ,CreatedBy :user .ID })
462+
463+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
464+
465+ // Workspace with old builds.
466+ ws := dbgen .Workspace (t ,db , database.WorkspaceTable {Name :"test-ws" ,OwnerID :user .ID ,OrganizationID :org .ID ,TemplateID :tmpl .ID })
467+ wb1 := mustCreateWorkspaceBuild (t ,db ,org ,tv ,ws .ID ,veryOld ,1 )
468+ wb2 := mustCreateWorkspaceBuild (t ,db ,org ,tv ,ws .ID ,veryOld ,2 )
469+ agent1 := mustCreateAgent (t ,db ,wb1 )
470+ agent2 := mustCreateAgent (t ,db ,wb2 )
471+ mustCreateAgentLogs (ctx ,t ,db ,agent1 ,& veryOld ,"agent 1 logs should be deleted with default retention" )
472+ mustCreateAgentLogs (ctx ,t ,db ,agent2 ,& veryOld ,"agent 2 logs should be retained" )
473+
474+ // Note: When retention is set to 0, we fall back to the default 7-day retention.
475+ // The logs are 60 days old, so non-latest build logs will still be deleted.
476+ done := awaitDoTick (ctx ,t ,clk )
477+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
478+ Retention : codersdk.RetentionConfig {
479+ WorkspaceAgentLogs :serpent .Duration (0 ),// Falls back to default 7 days
480+ },
481+ },clk )
482+ defer closer .Close ()
483+ testutil .TryReceive (ctx ,t ,done )
484+
485+ // Agent 1 logs should be deleted (non-latest build, older than default 7 days).
486+ assertNoWorkspaceAgentLogs (ctx ,t ,db ,agent1 .ID )
487+ // Agent 2 logs should be retained (latest build).
488+ assertWorkspaceAgentLogs (ctx ,t ,db ,agent2 .ID ,"agent 2 logs should be retained" )
489+ })
490+ }
491+
395492//nolint:paralleltest // It uses LockIDDBPurge.
396493func TestDeleteOldProvisionerDaemons (t * testing.T ) {
397494// TODO: must refactor DeleteOldProvisionerDaemons to allow passing in cutoff