@@ -1282,6 +1282,17 @@ func TestWakeLoopUpdatesNudgeTime(t *testing.T) {
12821282 t .Fatalf ("Failed to add agent: %v" , err )
12831283 }
12841284
1285+ // Add a worker so supervisor has work to do (selective wakeup requires it)
1286+ worker := state.Agent {
1287+ Type : state .AgentTypeWorker ,
1288+ TmuxWindow : "worker" ,
1289+ Task : "test task" ,
1290+ CreatedAt : time .Now (),
1291+ }
1292+ if err := d .state .AddAgent ("test-repo" , "worker-1" , worker ); err != nil {
1293+ t .Fatalf ("Failed to add worker: %v" , err )
1294+ }
1295+
12851296 // Trigger wake
12861297 beforeWake := time .Now ()
12871298 d .TriggerWake ()
@@ -2828,3 +2839,269 @@ func TestHandleClearCurrentRepoWhenNone(t *testing.T) {
28282839 t .Errorf ("clear_current_repo should succeed even when no repo set: %s" , resp .Error )
28292840 }
28302841}
2842+
2843+ func TestAgentHasWorkSupervisorWithActiveWorkers (t * testing.T ) {
2844+ d , cleanup := setupTestDaemon (t )
2845+ defer cleanup ()
2846+
2847+ repo := & state.Repository {
2848+ GithubURL : "https://github.com/test/repo" ,
2849+ TmuxSession : "mc-test" ,
2850+ Agents : make (map [string ]state.Agent ),
2851+ }
2852+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2853+ t .Fatalf ("Failed to add repo: %v" , err )
2854+ }
2855+
2856+ // Add supervisor and an active worker
2857+ supervisor := state.Agent {Type : state .AgentTypeSupervisor , TmuxWindow : "supervisor" , CreatedAt : time .Now ()}
2858+ worker := state.Agent {Type : state .AgentTypeWorker , TmuxWindow : "worker" , Task : "do stuff" , CreatedAt : time .Now ()}
2859+ d .state .AddAgent ("test-repo" , "supervisor" , supervisor )
2860+ d .state .AddAgent ("test-repo" , "worker-1" , worker )
2861+
2862+ msgMgr := d .getMessageManager ()
2863+
2864+ // Re-fetch repo snapshot to include the worker
2865+ repos := d .state .GetAllRepos ()
2866+ repoSnap := repos ["test-repo" ]
2867+
2868+ reason := d .agentHasWork ("test-repo" , "supervisor" , supervisor , repoSnap , msgMgr )
2869+ if reason == "" {
2870+ t .Error ("Supervisor should have work when active workers exist" )
2871+ }
2872+ if reason != "active workers" {
2873+ t .Errorf ("Expected reason 'active workers', got %q" , reason )
2874+ }
2875+ }
2876+
2877+ func TestAgentHasWorkSupervisorNoWorkers (t * testing.T ) {
2878+ d , cleanup := setupTestDaemon (t )
2879+ defer cleanup ()
2880+
2881+ repo := & state.Repository {
2882+ GithubURL : "https://github.com/test/repo" ,
2883+ TmuxSession : "mc-test" ,
2884+ Agents : make (map [string ]state.Agent ),
2885+ }
2886+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2887+ t .Fatalf ("Failed to add repo: %v" , err )
2888+ }
2889+
2890+ supervisor := state.Agent {Type : state .AgentTypeSupervisor , TmuxWindow : "supervisor" , CreatedAt : time .Now ()}
2891+ d .state .AddAgent ("test-repo" , "supervisor" , supervisor )
2892+
2893+ msgMgr := d .getMessageManager ()
2894+ repos := d .state .GetAllRepos ()
2895+ repoSnap := repos ["test-repo" ]
2896+
2897+ reason := d .agentHasWork ("test-repo" , "supervisor" , supervisor , repoSnap , msgMgr )
2898+ if reason != "" {
2899+ t .Errorf ("Supervisor should have no work when no workers exist, got reason %q" , reason )
2900+ }
2901+ }
2902+
2903+ func TestAgentHasWorkMergeQueueWithOpenPRs (t * testing.T ) {
2904+ d , cleanup := setupTestDaemon (t )
2905+ defer cleanup ()
2906+
2907+ repo := & state.Repository {
2908+ GithubURL : "https://github.com/test/repo" ,
2909+ TmuxSession : "mc-test" ,
2910+ Agents : make (map [string ]state.Agent ),
2911+ }
2912+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2913+ t .Fatalf ("Failed to add repo: %v" , err )
2914+ }
2915+
2916+ mq := state.Agent {Type : state .AgentTypeMergeQueue , TmuxWindow : "merge-queue" , CreatedAt : time .Now ()}
2917+ worker := state.Agent {Type : state .AgentTypeWorker , TmuxWindow : "worker" , PRURL : "https://github.com/test/repo/pull/1" , CreatedAt : time .Now ()}
2918+ d .state .AddAgent ("test-repo" , "merge-queue" , mq )
2919+ d .state .AddAgent ("test-repo" , "worker-1" , worker )
2920+
2921+ msgMgr := d .getMessageManager ()
2922+ repos := d .state .GetAllRepos ()
2923+ repoSnap := repos ["test-repo" ]
2924+
2925+ reason := d .agentHasWork ("test-repo" , "merge-queue" , mq , repoSnap , msgMgr )
2926+ if reason == "" {
2927+ t .Error ("Merge queue should have work when workers have open PRs" )
2928+ }
2929+ }
2930+
2931+ func TestAgentHasWorkMergeQueueNoPRs (t * testing.T ) {
2932+ d , cleanup := setupTestDaemon (t )
2933+ defer cleanup ()
2934+
2935+ repo := & state.Repository {
2936+ GithubURL : "https://github.com/test/repo" ,
2937+ TmuxSession : "mc-test" ,
2938+ Agents : make (map [string ]state.Agent ),
2939+ }
2940+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2941+ t .Fatalf ("Failed to add repo: %v" , err )
2942+ }
2943+
2944+ mq := state.Agent {Type : state .AgentTypeMergeQueue , TmuxWindow : "merge-queue" , CreatedAt : time .Now ()}
2945+ d .state .AddAgent ("test-repo" , "merge-queue" , mq )
2946+
2947+ msgMgr := d .getMessageManager ()
2948+ repos := d .state .GetAllRepos ()
2949+ repoSnap := repos ["test-repo" ]
2950+
2951+ reason := d .agentHasWork ("test-repo" , "merge-queue" , mq , repoSnap , msgMgr )
2952+ if reason != "" {
2953+ t .Errorf ("Merge queue should have no work without PRs, got reason %q" , reason )
2954+ }
2955+ }
2956+
2957+ func TestAgentHasWorkWorkerNoMessages (t * testing.T ) {
2958+ d , cleanup := setupTestDaemon (t )
2959+ defer cleanup ()
2960+
2961+ repo := & state.Repository {
2962+ GithubURL : "https://github.com/test/repo" ,
2963+ TmuxSession : "mc-test" ,
2964+ Agents : make (map [string ]state.Agent ),
2965+ }
2966+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2967+ t .Fatalf ("Failed to add repo: %v" , err )
2968+ }
2969+
2970+ worker := state.Agent {Type : state .AgentTypeWorker , TmuxWindow : "worker" , Task : "do stuff" , CreatedAt : time .Now ()}
2971+ d .state .AddAgent ("test-repo" , "worker-1" , worker )
2972+
2973+ msgMgr := d .getMessageManager ()
2974+ repos := d .state .GetAllRepos ()
2975+ repoSnap := repos ["test-repo" ]
2976+
2977+ reason := d .agentHasWork ("test-repo" , "worker-1" , worker , repoSnap , msgMgr )
2978+ if reason != "" {
2979+ t .Errorf ("Worker should have no work without messages, got reason %q" , reason )
2980+ }
2981+ }
2982+
2983+ func TestAgentHasWorkWithPendingMessages (t * testing.T ) {
2984+ d , cleanup := setupTestDaemon (t )
2985+ defer cleanup ()
2986+
2987+ repo := & state.Repository {
2988+ GithubURL : "https://github.com/test/repo" ,
2989+ TmuxSession : "mc-test" ,
2990+ Agents : make (map [string ]state.Agent ),
2991+ }
2992+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
2993+ t .Fatalf ("Failed to add repo: %v" , err )
2994+ }
2995+
2996+ worker := state.Agent {Type : state .AgentTypeWorker , TmuxWindow : "worker" , Task : "do stuff" , CreatedAt : time .Now ()}
2997+ d .state .AddAgent ("test-repo" , "worker-1" , worker )
2998+
2999+ // Send a message to the worker
3000+ msgMgr := d .getMessageManager ()
3001+ _ , err := msgMgr .Send ("test-repo" , "supervisor" , "worker-1" , "You have a task" )
3002+ if err != nil {
3003+ t .Fatalf ("Failed to send message: %v" , err )
3004+ }
3005+
3006+ repos := d .state .GetAllRepos ()
3007+ repoSnap := repos ["test-repo" ]
3008+
3009+ reason := d .agentHasWork ("test-repo" , "worker-1" , worker , repoSnap , msgMgr )
3010+ if reason != "pending messages" {
3011+ t .Errorf ("Worker with pending messages should have work, got reason %q" , reason )
3012+ }
3013+ }
3014+
3015+ func TestAgentHasWorkMergeQueueWithHistoryPRs (t * testing.T ) {
3016+ d , cleanup := setupTestDaemon (t )
3017+ defer cleanup ()
3018+
3019+ repo := & state.Repository {
3020+ GithubURL : "https://github.com/test/repo" ,
3021+ TmuxSession : "mc-test" ,
3022+ Agents : make (map [string ]state.Agent ),
3023+ }
3024+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
3025+ t .Fatalf ("Failed to add repo: %v" , err )
3026+ }
3027+
3028+ mq := state.Agent {Type : state .AgentTypeMergeQueue , TmuxWindow : "merge-queue" , CreatedAt : time .Now ()}
3029+ d .state .AddAgent ("test-repo" , "merge-queue" , mq )
3030+
3031+ // Add a task history entry with an open PR
3032+ entry := state.TaskHistoryEntry {
3033+ Name : "old-worker" ,
3034+ Task : "some task" ,
3035+ Branch : "work/old-worker" ,
3036+ PRURL : "https://github.com/test/repo/pull/5" ,
3037+ PRNumber : 5 ,
3038+ Status : state .TaskStatusOpen ,
3039+ CreatedAt : time .Now (),
3040+ CompletedAt : time .Now (),
3041+ }
3042+ d .state .AddTaskHistory ("test-repo" , entry )
3043+
3044+ msgMgr := d .getMessageManager ()
3045+ repos := d .state .GetAllRepos ()
3046+ repoSnap := repos ["test-repo" ]
3047+
3048+ reason := d .agentHasWork ("test-repo" , "merge-queue" , mq , repoSnap , msgMgr )
3049+ if reason == "" {
3050+ t .Error ("Merge queue should have work when task history has open PRs" )
3051+ }
3052+ if reason != "open PRs in history" {
3053+ t .Errorf ("Expected reason 'open PRs in history', got %q" , reason )
3054+ }
3055+ }
3056+
3057+ func TestSelectiveWakeSkipsWorkersWithoutWork (t * testing.T ) {
3058+ tmuxClient := tmux .NewClient ()
3059+ if ! tmuxClient .IsTmuxAvailable () {
3060+ t .Skip ("tmux not available" )
3061+ }
3062+
3063+ d , cleanup := setupTestDaemon (t )
3064+ defer cleanup ()
3065+
3066+ // Create a real tmux session
3067+ sessionName := "mc-test-selective-wake"
3068+ if err := tmuxClient .CreateSession (context .Background (), sessionName , true ); err != nil {
3069+ t .Fatalf ("Failed to create tmux session: %v" , err )
3070+ }
3071+ defer tmuxClient .KillSession (context .Background (), sessionName )
3072+
3073+ // Create windows for agents
3074+ if err := tmuxClient .CreateWindow (context .Background (), sessionName , "worker" ); err != nil {
3075+ t .Fatalf ("Failed to create worker window: %v" , err )
3076+ }
3077+
3078+ // Add repo and a worker with no messages (should NOT be woken)
3079+ repo := & state.Repository {
3080+ GithubURL : "https://github.com/test/repo" ,
3081+ TmuxSession : sessionName ,
3082+ Agents : make (map [string ]state.Agent ),
3083+ }
3084+ if err := d .state .AddRepo ("test-repo" , repo ); err != nil {
3085+ t .Fatalf ("Failed to add repo: %v" , err )
3086+ }
3087+
3088+ worker := state.Agent {
3089+ Type : state .AgentTypeWorker ,
3090+ TmuxWindow : "worker" ,
3091+ Task : "Test task" ,
3092+ CreatedAt : time .Now (),
3093+ LastNudge : time.Time {}, // Never nudged
3094+ }
3095+ if err := d .state .AddAgent ("test-repo" , "worker-1" , worker ); err != nil {
3096+ t .Fatalf ("Failed to add agent: %v" , err )
3097+ }
3098+
3099+ // Trigger wake - worker has no messages, should NOT be woken
3100+ d .TriggerWake ()
3101+
3102+ // Verify LastNudge was NOT updated (worker skipped due to no work)
3103+ updatedAgent , _ := d .state .GetAgent ("test-repo" , "worker-1" )
3104+ if ! updatedAgent .LastNudge .IsZero () {
3105+ t .Error ("Worker without work should NOT be woken - LastNudge should remain zero" )
3106+ }
3107+ }
0 commit comments