@@ -14,10 +14,12 @@ import (
1414 "github.com/redis/go-redis/v9"
1515
1616 "github.com/chaitin/MonkeyCode/backend/config"
17+ "github.com/chaitin/MonkeyCode/backend/consts"
1718 "github.com/chaitin/MonkeyCode/backend/db"
1819 "github.com/chaitin/MonkeyCode/backend/db/enttest"
1920 "github.com/chaitin/MonkeyCode/backend/db/virtualmachine"
2021 "github.com/chaitin/MonkeyCode/backend/domain"
22+ "github.com/chaitin/MonkeyCode/backend/pkg/delayqueue"
2123 "github.com/chaitin/MonkeyCode/backend/pkg/taskflow"
2224)
2325
@@ -107,6 +109,104 @@ func TestRefreshCachesNotFoundVM(t *testing.T) {
107109 }
108110}
109111
112+ func TestShouldSkipRecycleForRecentTaskLastActiveAtSkipsClickHouse (t * testing.T ) {
113+ ctx := context .Background ()
114+ now := time .Date (2026 , 7 , 7 , 10 , 34 , 38 , 0 , time .UTC )
115+ taskID := uuid .New ()
116+ vm := & db.VirtualMachine {ID : "vm-recent-pg-activity" , UserID : uuid .New ()}
117+ vm .Edges .Tasks = []* db.Task {{
118+ ID : taskID ,
119+ Status : consts .TaskStatusProcessing ,
120+ LastActiveAt : now .Add (- time .Minute ),
121+ }}
122+ redisClient := newTestRedis (t )
123+ logger := slog .Default ()
124+ repo := & refreshHostRepoStub {vm : vm }
125+ activity := & taskLogActivityStub {err : errors .New ("unexpected clickhouse query" )}
126+ r := & vmIdleRefresher {
127+ cfg : & config.Config {VMIdle : config.VMIdle {SleepSeconds : 600 , RecycleSeconds : 3600 }},
128+ redis : redisClient ,
129+ logger : logger ,
130+ hostRepo : repo ,
131+ taskLogActivity : activity ,
132+ sleepQueue : delayqueue .NewVMSleepQueue (redisClient , logger ),
133+ notifyQueue : delayqueue .NewVMNotifyQueue (redisClient , logger ),
134+ recycleQueue : delayqueue .NewVMRecycleQueue (redisClient , logger ),
135+ }
136+
137+ skip , err := r .shouldSkipRecycleForRecentActivity (ctx , vm , now )
138+ if err != nil {
139+ t .Fatal (err )
140+ }
141+ if ! skip {
142+ t .Fatal ("expected recent task last_active_at to skip recycle" )
143+ }
144+ if activity .calls != 0 {
145+ t .Fatalf ("clickhouse calls = %d, want 0" , activity .calls )
146+ }
147+ if repo .getVirtualMachineCalls != 1 {
148+ t .Fatalf ("GetVirtualMachine calls = %d, want 1" , repo .getVirtualMachineCalls )
149+ }
150+ }
151+
152+ func TestShouldSkipRecycleForRecentTaskLogRefreshesVM (t * testing.T ) {
153+ ctx := context .Background ()
154+ now := time .Date (2026 , 7 , 7 , 10 , 34 , 38 , 0 , time .UTC )
155+ taskID := uuid .New ()
156+ vm := & db.VirtualMachine {ID : "vm-recent-log" , UserID : uuid .New ()}
157+ vm .Edges .Tasks = []* db.Task {{ID : taskID , Status : consts .TaskStatusProcessing }}
158+ redisClient := newTestRedis (t )
159+ logger := slog .Default ()
160+ repo := & refreshHostRepoStub {vm : vm }
161+ r := & vmIdleRefresher {
162+ cfg : & config.Config {VMIdle : config.VMIdle {SleepSeconds : 600 , RecycleSeconds : 3600 }},
163+ redis : redisClient ,
164+ logger : logger ,
165+ hostRepo : repo ,
166+ taskLogActivity : & taskLogActivityStub {latest : map [uuid.UUID ]time.Time {taskID : now .Add (- time .Minute )}},
167+ sleepQueue : delayqueue .NewVMSleepQueue (redisClient , logger ),
168+ notifyQueue : delayqueue .NewVMNotifyQueue (redisClient , logger ),
169+ recycleQueue : delayqueue .NewVMRecycleQueue (redisClient , logger ),
170+ }
171+
172+ skip , err := r .shouldSkipRecycleForRecentActivity (ctx , vm , now )
173+ if err != nil {
174+ t .Fatal (err )
175+ }
176+ if ! skip {
177+ t .Fatal ("expected recent task log to skip recycle" )
178+ }
179+ if repo .getVirtualMachineCalls != 1 {
180+ t .Fatalf ("GetVirtualMachine calls = %d, want 1" , repo .getVirtualMachineCalls )
181+ }
182+ }
183+
184+ func TestShouldSkipRecycleForRecentTaskLogAllowsStaleVM (t * testing.T ) {
185+ ctx := context .Background ()
186+ now := time .Date (2026 , 7 , 7 , 10 , 34 , 38 , 0 , time .UTC )
187+ taskID := uuid .New ()
188+ vm := & db.VirtualMachine {ID : "vm-stale-log" , UserID : uuid .New ()}
189+ vm .Edges .Tasks = []* db.Task {{ID : taskID , Status : consts .TaskStatusProcessing }}
190+ repo := & refreshHostRepoStub {vm : vm }
191+ r := & vmIdleRefresher {
192+ cfg : & config.Config {VMIdle : config.VMIdle {SleepSeconds : 600 , RecycleSeconds : 3600 }},
193+ logger : slog .Default (),
194+ hostRepo : repo ,
195+ taskLogActivity : & taskLogActivityStub {latest : map [uuid.UUID ]time.Time {taskID : now .Add (- 2 * time .Hour )}},
196+ }
197+
198+ skip , err := r .shouldSkipRecycleForRecentActivity (ctx , vm , now )
199+ if err != nil {
200+ t .Fatal (err )
201+ }
202+ if skip {
203+ t .Fatal ("expected stale task log to allow recycle" )
204+ }
205+ if repo .getVirtualMachineCalls != 0 {
206+ t .Fatalf ("GetVirtualMachine calls = %d, want 0" , repo .getVirtualMachineCalls )
207+ }
208+ }
209+
110210func newTestRedis (t * testing.T ) * redis.Client {
111211 t .Helper ()
112212 srv := miniredis .RunT (t )
@@ -139,6 +239,21 @@ type refreshHostRepoStub struct {
139239 getVirtualMachineCalls int
140240}
141241
242+ type taskLogActivityStub struct {
243+ latest map [uuid.UUID ]time.Time
244+ err error
245+ calls int
246+ }
247+
248+ func (s * taskLogActivityStub ) LatestTaskLogTime (_ context.Context , taskID uuid.UUID ) (time.Time , bool , error ) {
249+ s .calls ++
250+ if s .err != nil {
251+ return time.Time {}, false , s .err
252+ }
253+ latest , ok := s .latest [taskID ]
254+ return latest , ok , nil
255+ }
256+
142257func (s * refreshHostRepoStub ) List (context.Context , uuid.UUID ) ([]* db.Host , error ) {
143258 return nil , errors .New ("not implemented" )
144259}
0 commit comments