@@ -759,6 +759,223 @@ func TestDeleteOldTelemetryHeartbeats(t *testing.T) {
759759 }, testutil .WaitShort , testutil .IntervalFast , "it should delete old telemetry heartbeats" )
760760}
761761
762+ func TestDeleteOldConnectionLogs (t * testing.T ) {
763+ t .Parallel ()
764+
765+ t .Run ("RetentionEnabled" , func (t * testing.T ) {
766+ t .Parallel ()
767+
768+ ctx := testutil .Context (t , testutil .WaitShort )
769+
770+ clk := quartz .NewMock (t )
771+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
772+ retentionPeriod := 30 * 24 * time .Hour // 30 days
773+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // 31 days ago (older than threshold)
774+ beforeThreshold := now .Add (- 15 * 24 * time .Hour ) // 15 days ago (newer than threshold)
775+ clk .Set (now ).MustWait (ctx )
776+
777+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
778+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
779+ user := dbgen .User (t , db , database.User {})
780+ org := dbgen .Organization (t , db , database.Organization {})
781+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
782+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
783+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
784+ workspace := dbgen .Workspace (t , db , database.WorkspaceTable {
785+ OwnerID : user .ID ,
786+ OrganizationID : org .ID ,
787+ TemplateID : tmpl .ID ,
788+ })
789+
790+ // Create old connection log (should be deleted)
791+ oldLog := dbgen .ConnectionLog (t , db , database.UpsertConnectionLogParams {
792+ ID : uuid .New (),
793+ Time : afterThreshold ,
794+ OrganizationID : org .ID ,
795+ WorkspaceOwnerID : user .ID ,
796+ WorkspaceID : workspace .ID ,
797+ WorkspaceName : workspace .Name ,
798+ AgentName : "agent1" ,
799+ Type : database .ConnectionTypeSsh ,
800+ ConnectionStatus : database .ConnectionStatusConnected ,
801+ })
802+
803+ // Create recent connection log (should be kept)
804+ recentLog := dbgen .ConnectionLog (t , db , database.UpsertConnectionLogParams {
805+ ID : uuid .New (),
806+ Time : beforeThreshold ,
807+ OrganizationID : org .ID ,
808+ WorkspaceOwnerID : user .ID ,
809+ WorkspaceID : workspace .ID ,
810+ WorkspaceName : workspace .Name ,
811+ AgentName : "agent2" ,
812+ Type : database .ConnectionTypeSsh ,
813+ ConnectionStatus : database .ConnectionStatusConnected ,
814+ })
815+
816+ // Run the purge with configured retention period
817+ done := awaitDoTick (ctx , t , clk )
818+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
819+ Retention : codersdk.RetentionConfig {
820+ ConnectionLogs : serpent .Duration (retentionPeriod ),
821+ },
822+ }, clk )
823+ defer closer .Close ()
824+ testutil .TryReceive (ctx , t , done )
825+
826+ // Verify results by querying all connection logs
827+ logs , err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
828+ LimitOpt : 100 ,
829+ })
830+ require .NoError (t , err )
831+
832+ logIDs := make ([]uuid.UUID , len (logs ))
833+ for i , log := range logs {
834+ logIDs [i ] = log .ConnectionLog .ID
835+ }
836+
837+ require .NotContains (t , logIDs , oldLog .ID , "old connection log should be deleted" )
838+ require .Contains (t , logIDs , recentLog .ID , "recent connection log should be kept" )
839+ })
840+
841+ t .Run ("RetentionDisabled" , func (t * testing.T ) {
842+ t .Parallel ()
843+
844+ ctx := testutil .Context (t , testutil .WaitShort )
845+
846+ clk := quartz .NewMock (t )
847+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
848+ oldTime := now .Add (- 365 * 24 * time .Hour ) // 1 year ago
849+ clk .Set (now ).MustWait (ctx )
850+
851+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
852+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
853+ user := dbgen .User (t , db , database.User {})
854+ org := dbgen .Organization (t , db , database.Organization {})
855+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
856+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
857+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
858+ workspace := dbgen .Workspace (t , db , database.WorkspaceTable {
859+ OwnerID : user .ID ,
860+ OrganizationID : org .ID ,
861+ TemplateID : tmpl .ID ,
862+ })
863+
864+ // Create old connection log (should NOT be deleted when retention is 0)
865+ oldLog := dbgen .ConnectionLog (t , db , database.UpsertConnectionLogParams {
866+ ID : uuid .New (),
867+ Time : oldTime ,
868+ OrganizationID : org .ID ,
869+ WorkspaceOwnerID : user .ID ,
870+ WorkspaceID : workspace .ID ,
871+ WorkspaceName : workspace .Name ,
872+ AgentName : "agent1" ,
873+ Type : database .ConnectionTypeSsh ,
874+ ConnectionStatus : database .ConnectionStatusConnected ,
875+ })
876+
877+ // Run the purge with retention disabled (0)
878+ done := awaitDoTick (ctx , t , clk )
879+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
880+ Retention : codersdk.RetentionConfig {
881+ ConnectionLogs : serpent .Duration (0 ), // disabled
882+ },
883+ }, clk )
884+ defer closer .Close ()
885+ testutil .TryReceive (ctx , t , done )
886+
887+ // Verify old log is still present
888+ logs , err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
889+ LimitOpt : 100 ,
890+ })
891+ require .NoError (t , err )
892+
893+ logIDs := make ([]uuid.UUID , len (logs ))
894+ for i , log := range logs {
895+ logIDs [i ] = log .ConnectionLog .ID
896+ }
897+
898+ require .Contains (t , logIDs , oldLog .ID , "old connection log should NOT be deleted when retention is disabled" )
899+ })
900+
901+ t .Run ("GlobalRetentionFallback" , func (t * testing.T ) {
902+ t .Parallel ()
903+
904+ ctx := testutil .Context (t , testutil .WaitShort )
905+
906+ clk := quartz .NewMock (t )
907+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
908+ retentionPeriod := 30 * 24 * time .Hour // 30 days
909+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // 31 days ago (older than threshold)
910+ beforeThreshold := now .Add (- 15 * 24 * time .Hour ) // 15 days ago (newer than threshold)
911+ clk .Set (now ).MustWait (ctx )
912+
913+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
914+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
915+ user := dbgen .User (t , db , database.User {})
916+ org := dbgen .Organization (t , db , database.Organization {})
917+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {UserID : user .ID , OrganizationID : org .ID })
918+ tv := dbgen .TemplateVersion (t , db , database.TemplateVersion {OrganizationID : org .ID , CreatedBy : user .ID })
919+ tmpl := dbgen .Template (t , db , database.Template {OrganizationID : org .ID , ActiveVersionID : tv .ID , CreatedBy : user .ID })
920+ workspace := dbgen .Workspace (t , db , database.WorkspaceTable {
921+ OwnerID : user .ID ,
922+ OrganizationID : org .ID ,
923+ TemplateID : tmpl .ID ,
924+ })
925+
926+ // Create old connection log (should be deleted)
927+ oldLog := dbgen .ConnectionLog (t , db , database.UpsertConnectionLogParams {
928+ ID : uuid .New (),
929+ Time : afterThreshold ,
930+ OrganizationID : org .ID ,
931+ WorkspaceOwnerID : user .ID ,
932+ WorkspaceID : workspace .ID ,
933+ WorkspaceName : workspace .Name ,
934+ AgentName : "agent1" ,
935+ Type : database .ConnectionTypeSsh ,
936+ ConnectionStatus : database .ConnectionStatusConnected ,
937+ })
938+
939+ // Create recent connection log (should be kept)
940+ recentLog := dbgen .ConnectionLog (t , db , database.UpsertConnectionLogParams {
941+ ID : uuid .New (),
942+ Time : beforeThreshold ,
943+ OrganizationID : org .ID ,
944+ WorkspaceOwnerID : user .ID ,
945+ WorkspaceID : workspace .ID ,
946+ WorkspaceName : workspace .Name ,
947+ AgentName : "agent2" ,
948+ Type : database .ConnectionTypeSsh ,
949+ ConnectionStatus : database .ConnectionStatusConnected ,
950+ })
951+
952+ // Run the purge with global retention (connection logs retention is 0, so it falls back)
953+ done := awaitDoTick (ctx , t , clk )
954+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
955+ Retention : codersdk.RetentionConfig {
956+ Global : serpent .Duration (retentionPeriod ), // Use global
957+ ConnectionLogs : serpent .Duration (0 ), // Not set, should fall back to global
958+ },
959+ }, clk )
960+ defer closer .Close ()
961+ testutil .TryReceive (ctx , t , done )
962+
963+ // Verify results
964+ logs , err := db .GetConnectionLogsOffset (ctx , database.GetConnectionLogsOffsetParams {
965+ LimitOpt : 100 ,
966+ })
967+ require .NoError (t , err )
968+
969+ logIDs := make ([]uuid.UUID , len (logs ))
970+ for i , log := range logs {
971+ logIDs [i ] = log .ConnectionLog .ID
972+ }
973+
974+ require .NotContains (t , logIDs , oldLog .ID , "old connection log should be deleted via global retention" )
975+ require .Contains (t , logIDs , recentLog .ID , "recent connection log should be kept" )
976+ })
977+ }
978+
762979func TestDeleteOldAIBridgeRecords (t * testing.T ) {
763980 t .Parallel ()
764981
0 commit comments