@@ -2246,9 +2246,9 @@ func TestSpawnerReconcilerTaskSpawnerPredicate(t *testing.T) {
22462246 }
22472247}
22482248
2249- func TestSpawnerReconcilerTaskPredicate (t * testing.T ) {
2249+ func TestTaskActivityReconcilerTaskPredicate (t * testing.T ) {
22502250 key := types.NamespacedName {Name : "spawner" , Namespace : "default" }
2251- r := & spawnerReconciler {Key : key }
2251+ r := & taskActivityReconciler {Key : key }
22522252 p := r .taskPredicate ()
22532253
22542254 base := newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhasePending )
@@ -2279,9 +2279,9 @@ func TestSpawnerReconcilerTaskPredicate(t *testing.T) {
22792279 }
22802280}
22812281
2282- func TestSpawnerReconcilerRequestsForTask (t * testing.T ) {
2282+ func TestTaskActivityReconcilerRequestsForTask (t * testing.T ) {
22832283 key := types.NamespacedName {Name : "spawner" , Namespace : "default" }
2284- r := & spawnerReconciler {Key : key }
2284+ r := & taskActivityReconciler {Key : key }
22852285
22862286 task := newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhasePending )
22872287 requests := r .requestsForTask (context .Background (), task .DeepCopy ())
@@ -2293,9 +2293,9 @@ func TestSpawnerReconcilerRequestsForTask(t *testing.T) {
22932293 }
22942294
22952295 other := newTask ("other-1" , "default" , "other" , kelosv1alpha1 .TaskPhasePending )
2296- requests = r .requestsForTask (context .Background (), other .DeepCopy ())
2297- if len (requests ) != 0 {
2298- t .Fatalf ("Expected no requests for non-matching task, got %d" , len (requests ))
2296+ otherRequests : = r .requestsForTask (context .Background (), other .DeepCopy ())
2297+ if len (otherRequests ) != 0 {
2298+ t .Fatalf ("Expected no requests for non-matching task, got %d" , len (otherRequests ))
22992299 }
23002300}
23012301
@@ -2415,3 +2415,207 @@ func TestRunOnce_ReturnsSourcePollInterval(t *testing.T) {
24152415 t .Fatalf ("Interval = %v, want %v" , interval , 15 * time .Second )
24162416 }
24172417}
2418+
2419+ func TestHandleTaskActivity_UpdatesActiveTasksCount (t * testing.T ) {
2420+ ts := newTaskSpawner ("spawner" , "default" , nil )
2421+ ts .Status .ActiveTasks = 3 // stale value
2422+
2423+ existingTasks := []kelosv1alpha1.Task {
2424+ newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseRunning ),
2425+ newTask ("spawner-2" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseSucceeded ),
2426+ newTask ("spawner-3" , "default" , "spawner" , kelosv1alpha1 .TaskPhasePending ),
2427+ }
2428+ cl , key := setupTest (t , ts , existingTasks ... )
2429+
2430+ if err := handleTaskActivity (context .Background (), cl , key , spawnerRuntimeConfig {}); err != nil {
2431+ t .Fatalf ("Unexpected error: %v" , err )
2432+ }
2433+
2434+ var updated kelosv1alpha1.TaskSpawner
2435+ if err := cl .Get (context .Background (), key , & updated ); err != nil {
2436+ t .Fatalf ("Getting TaskSpawner: %v" , err )
2437+ }
2438+ // 1 running + 1 pending = 2 active (succeeded is excluded)
2439+ if updated .Status .ActiveTasks != 2 {
2440+ t .Errorf ("Expected activeTasks=2, got %d" , updated .Status .ActiveTasks )
2441+ }
2442+ }
2443+
2444+ func TestHandleTaskActivity_NoUpdateWhenCountUnchanged (t * testing.T ) {
2445+ ts := newTaskSpawner ("spawner" , "default" , nil )
2446+ ts .Status .ActiveTasks = 1
2447+
2448+ task1 := newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseRunning )
2449+ task2 := newTask ("spawner-2" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseSucceeded )
2450+
2451+ // Track status updates via interceptor
2452+ updateCalled := false
2453+ cl := fake .NewClientBuilder ().
2454+ WithScheme (newTestScheme ()).
2455+ WithObjects (ts , & task1 , & task2 ).
2456+ WithStatusSubresource (ts ).
2457+ WithInterceptorFuncs (interceptor.Funcs {
2458+ SubResourceUpdate : func (ctx context.Context , c client.Client , subResourceName string , obj client.Object , opts ... client.SubResourceUpdateOption ) error {
2459+ if subResourceName == "status" {
2460+ if _ , ok := obj .(* kelosv1alpha1.TaskSpawner ); ok {
2461+ updateCalled = true
2462+ }
2463+ }
2464+ return nil
2465+ },
2466+ }).
2467+ Build ()
2468+ key := types.NamespacedName {Name : ts .Name , Namespace : ts .Namespace }
2469+
2470+ if err := handleTaskActivity (context .Background (), cl , key , spawnerRuntimeConfig {}); err != nil {
2471+ t .Fatalf ("Unexpected error: %v" , err )
2472+ }
2473+
2474+ if updateCalled {
2475+ t .Error ("Expected no status update when activeTasks count is unchanged" )
2476+ }
2477+ }
2478+
2479+ func TestHandleTaskActivity_RunsReportingWhenEnabled (t * testing.T ) {
2480+ ts := newTaskSpawner ("spawner" , "default" , nil )
2481+ ts .Spec .When .GitHubIssues .Reporting = & kelosv1alpha1.GitHubReporting {Enabled : true }
2482+
2483+ task := kelosv1alpha1.Task {
2484+ ObjectMeta : metav1.ObjectMeta {
2485+ Name : "spawner-1" ,
2486+ Namespace : "default" ,
2487+ Labels : map [string ]string {
2488+ "kelos.dev/taskspawner" : "spawner" ,
2489+ },
2490+ Annotations : map [string ]string {
2491+ reporting .AnnotationGitHubReporting : "enabled" ,
2492+ reporting .AnnotationSourceNumber : "42" ,
2493+ reporting .AnnotationSourceKind : "issue" ,
2494+ },
2495+ },
2496+ Spec : kelosv1alpha1.TaskSpec {
2497+ Type : "claude-code" ,
2498+ Prompt : "test" ,
2499+ Credentials : kelosv1alpha1.Credentials {
2500+ Type : kelosv1alpha1 .CredentialTypeOAuth ,
2501+ SecretRef : & kelosv1alpha1.SecretReference {Name : "creds" },
2502+ },
2503+ },
2504+ Status : kelosv1alpha1.TaskStatus {
2505+ Phase : kelosv1alpha1 .TaskPhasePending ,
2506+ },
2507+ }
2508+
2509+ cl , key := setupTest (t , ts , task )
2510+
2511+ apiCalled := false
2512+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
2513+ apiCalled = true
2514+ w .WriteHeader (http .StatusCreated )
2515+ json .NewEncoder (w ).Encode (map [string ]int64 {"id" : 999 })
2516+ }))
2517+ defer server .Close ()
2518+
2519+ cfg := spawnerRuntimeConfig {
2520+ GitHubOwner : "owner" ,
2521+ GitHubRepo : "repo" ,
2522+ GitHubAPIBaseURL : server .URL ,
2523+ }
2524+
2525+ if err := handleTaskActivity (context .Background (), cl , key , cfg ); err != nil {
2526+ t .Fatalf ("Unexpected error: %v" , err )
2527+ }
2528+
2529+ if ! apiCalled {
2530+ t .Error ("Expected GitHub API to be called for reporting" )
2531+ }
2532+
2533+ // Verify annotations were updated on the task
2534+ var updated kelosv1alpha1.Task
2535+ if err := cl .Get (context .Background (), client .ObjectKeyFromObject (& task ), & updated ); err != nil {
2536+ t .Fatalf ("Getting updated task: %v" , err )
2537+ }
2538+ if updated .Annotations [reporting .AnnotationGitHubReportPhase ] != "accepted" {
2539+ t .Errorf ("Expected report phase 'accepted', got %q" , updated .Annotations [reporting .AnnotationGitHubReportPhase ])
2540+ }
2541+ }
2542+
2543+ func TestHandleTaskActivity_SkipsReportingWhenDisabled (t * testing.T ) {
2544+ ts := newTaskSpawner ("spawner" , "default" , nil )
2545+ // Reporting not enabled (default)
2546+
2547+ task := newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseRunning )
2548+ cl , key := setupTest (t , ts , task )
2549+
2550+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
2551+ t .Error ("GitHub API should not be called when reporting is disabled" )
2552+ w .WriteHeader (http .StatusOK )
2553+ }))
2554+ defer server .Close ()
2555+
2556+ cfg := spawnerRuntimeConfig {
2557+ GitHubOwner : "owner" ,
2558+ GitHubRepo : "repo" ,
2559+ GitHubAPIBaseURL : server .URL ,
2560+ }
2561+
2562+ if err := handleTaskActivity (context .Background (), cl , key , cfg ); err != nil {
2563+ t .Fatalf ("Unexpected error: %v" , err )
2564+ }
2565+ }
2566+
2567+ func TestHandleTaskActivity_DoesNotTriggerDiscovery (t * testing.T ) {
2568+ ts := newTaskSpawner ("spawner" , "default" , nil )
2569+ existingTasks := []kelosv1alpha1.Task {
2570+ newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseSucceeded ),
2571+ }
2572+ cl , key := setupTest (t , ts , existingTasks ... )
2573+
2574+ // Record discovery metric before
2575+ beforeDiscovery := testutil .ToFloat64 (discoveryTotal )
2576+
2577+ if err := handleTaskActivity (context .Background (), cl , key , spawnerRuntimeConfig {}); err != nil {
2578+ t .Fatalf ("Unexpected error: %v" , err )
2579+ }
2580+
2581+ // Discovery metric should not have changed
2582+ afterDiscovery := testutil .ToFloat64 (discoveryTotal )
2583+ if afterDiscovery != beforeDiscovery {
2584+ t .Errorf ("Expected no discovery cycles to run, but discoveryTotal changed from %v to %v" , beforeDiscovery , afterDiscovery )
2585+ }
2586+ }
2587+
2588+ func TestHandleTaskActivity_TaskSpawnerNotFound (t * testing.T ) {
2589+ // TaskSpawner does not exist - should return nil (no error)
2590+ cl := fake .NewClientBuilder ().
2591+ WithScheme (newTestScheme ()).
2592+ Build ()
2593+ key := types.NamespacedName {Name : "nonexistent" , Namespace : "default" }
2594+
2595+ if err := handleTaskActivity (context .Background (), cl , key , spawnerRuntimeConfig {}); err != nil {
2596+ t .Fatalf ("Expected no error for missing TaskSpawner, got: %v" , err )
2597+ }
2598+ }
2599+
2600+ func TestHandleTaskActivity_AllTasksTerminal (t * testing.T ) {
2601+ ts := newTaskSpawner ("spawner" , "default" , nil )
2602+ ts .Status .ActiveTasks = 2 // stale value
2603+
2604+ existingTasks := []kelosv1alpha1.Task {
2605+ newTask ("spawner-1" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseSucceeded ),
2606+ newTask ("spawner-2" , "default" , "spawner" , kelosv1alpha1 .TaskPhaseFailed ),
2607+ }
2608+ cl , key := setupTest (t , ts , existingTasks ... )
2609+
2610+ if err := handleTaskActivity (context .Background (), cl , key , spawnerRuntimeConfig {}); err != nil {
2611+ t .Fatalf ("Unexpected error: %v" , err )
2612+ }
2613+
2614+ var updated kelosv1alpha1.TaskSpawner
2615+ if err := cl .Get (context .Background (), key , & updated ); err != nil {
2616+ t .Fatalf ("Getting TaskSpawner: %v" , err )
2617+ }
2618+ if updated .Status .ActiveTasks != 0 {
2619+ t .Errorf ("Expected activeTasks=0 when all tasks are terminal, got %d" , updated .Status .ActiveTasks )
2620+ }
2621+ }
0 commit comments