11#!/usr/bin/env python3
22import shutil
33import subprocess
4+ import tempfile
45import unittest
56from pathlib import Path
67
@@ -371,6 +372,17 @@ def test_make_run_review_uses_skill_bundle_context(self) -> None:
371372 self .assertIn ('review_pipeline_context "$repo"' , proc .stdout )
372373 self .assertIn ('skill_prompt_with_context review-pipeline' , proc .stdout )
373374
375+ def test_make_run_review_watch_mode_emits_outcome_markers (self ) -> None :
376+ proc = subprocess .run (
377+ ["make" , "-n" , "run-review" , "WATCH_MODE=1" , "N=570" ],
378+ cwd = REPO_ROOT ,
379+ capture_output = True ,
380+ text = True ,
381+ )
382+ self .assertEqual (proc .returncode , 0 , proc .stderr )
383+ self .assertIn ("watch_emit_outcome gone" , proc .stdout )
384+ self .assertIn ('run_agent_with_watch_outcome "review-output.log" "$PROMPT"' , proc .stdout )
385+
374386 def test_make_run_pipeline_uses_scripted_board_selection (self ) -> None :
375387 proc = subprocess .run (
376388 ["make" , "-n" , "run-pipeline" ],
@@ -382,6 +394,17 @@ def test_make_run_pipeline_uses_scripted_board_selection(self) -> None:
382394 self .assertIn ('board_next_json ready "" "" "$tmp_state"' , proc .stdout )
383395 self .assertIn ('skill_prompt_with_context project-pipeline' , proc .stdout )
384396
397+ def test_make_run_pipeline_watch_mode_emits_outcome_markers (self ) -> None :
398+ proc = subprocess .run (
399+ ["make" , "-n" , "run-pipeline" , "WATCH_MODE=1" , "N=42" ],
400+ cwd = REPO_ROOT ,
401+ capture_output = True ,
402+ text = True ,
403+ )
404+ self .assertEqual (proc .returncode , 0 , proc .stderr )
405+ self .assertIn ("watch_emit_outcome gone" , proc .stdout )
406+ self .assertIn ('run_agent_with_watch_outcome "pipeline-output.log" "$PROMPT"' , proc .stdout )
407+
385408 def test_watch_and_dispatch_uses_persistent_default_state_file (self ) -> None :
386409 if shutil .which ("dash" ) is None :
387410 self .skipTest ("dash is not installed" )
@@ -406,7 +429,7 @@ def test_watch_and_dispatch_uses_persistent_default_state_file(self) -> None:
406429 proc .stderr ,
407430 )
408431
409- def test_watch_and_dispatch_sleeps_after_successful_dispatch (self ) -> None :
432+ def test_watch_and_dispatch_rechecks_immediately_after_successful_dispatch (self ) -> None :
410433 if shutil .which ("dash" ) is None :
411434 self .skipTest ("dash is not installed" )
412435
@@ -437,7 +460,92 @@ def test_watch_and_dispatch_sleeps_after_successful_dispatch(self) -> None:
437460 self .assertEqual (proc .returncode , 2 , proc .stderr )
438461 self .assertIn ("make:run-pipeline N=42" , proc .stdout )
439462 self .assertIn ("ack:PVTI_1" , proc .stdout )
440- self .assertIn ("sleep:600" , proc .stdout )
463+ self .assertNotIn ("sleep:600" , proc .stdout )
464+
465+ def test_watch_and_dispatch_drains_ready_items_before_sleeping (self ) -> None :
466+ if shutil .which ("dash" ) is None :
467+ self .skipTest ("dash is not installed" )
468+
469+ proc = subprocess .run (
470+ [
471+ "dash" ,
472+ "-c" ,
473+ (
474+ ". scripts/make_helpers.sh; "
475+ "counter_file=/tmp/test-watch-drain-$$.count; "
476+ "rm -f \" $counter_file\" ; "
477+ "date() { printf '2026-03-16 00:00:00'; }; "
478+ "poll_project_items() { "
479+ " count=$(cat \" $counter_file\" 2>/dev/null || printf '0'); "
480+ " count=$((count + 1)); "
481+ " printf '%s' \" $count\" > \" $counter_file\" ; "
482+ " case \" $count\" in "
483+ " 1) printf 'PVTI_1\\ t42\\ n'; return 0 ;; "
484+ " 2) printf 'PVTI_2\\ t43\\ n'; return 0 ;; "
485+ " 3) return 1 ;; "
486+ " *) return 2 ;; "
487+ " esac; "
488+ "}; "
489+ "make() { printf 'make:%s %s\\ n' \" $1\" \" $2\" ; return 0; }; "
490+ "ack_polled_item() { printf 'ack:%s\\ n' \" $2\" ; }; "
491+ "sleep() { printf 'sleep:%s\\ n' \" $1\" ; return 0; }; "
492+ "MAKE=make POLL_INTERVAL=600 "
493+ "watch_and_dispatch ready run-pipeline 'Ready issues'"
494+ ),
495+ ],
496+ cwd = REPO_ROOT ,
497+ capture_output = True ,
498+ text = True ,
499+ )
500+ self .assertEqual (proc .returncode , 2 , proc .stderr )
501+ self .assertIn ("make:run-pipeline N=42" , proc .stdout )
502+ self .assertIn ("ack:PVTI_1" , proc .stdout )
503+ self .assertIn ("make:run-pipeline N=43" , proc .stdout )
504+ self .assertIn ("ack:PVTI_2" , proc .stdout )
505+ self .assertEqual (proc .stdout .count ("sleep:600" ), 1 )
506+ self .assertLess (
507+ proc .stdout .index ("make:run-pipeline N=43" ),
508+ proc .stdout .index ("sleep:600" ),
509+ )
510+
511+ def test_watch_and_dispatch_skips_retry_state_for_explicit_gone_outcome (self ) -> None :
512+ if shutil .which ("dash" ) is None :
513+ self .skipTest ("dash is not installed" )
514+
515+ with tempfile .TemporaryDirectory () as tmpdir :
516+ state_file = Path (tmpdir ) / "gone-state.json"
517+ counter_file = Path (tmpdir ) / "gone-counter.txt"
518+ proc = subprocess .run (
519+ [
520+ "dash" ,
521+ "-c" ,
522+ (
523+ ". scripts/make_helpers.sh; "
524+ f"state_file={ state_file } ; "
525+ f"counter_file={ counter_file } ; "
526+ "rm -f \" $state_file\" \" $counter_file\" ; "
527+ "poll_project_items() { "
528+ " count=$(cat \" $counter_file\" 2>/dev/null || printf '0'); "
529+ " count=$((count + 1)); "
530+ " printf '%s' \" $count\" > \" $counter_file\" ; "
531+ " if [ \" $count\" -eq 1 ]; then printf 'PVTI_1\\ t42\\ n'; return 0; fi; "
532+ " return 2; "
533+ "}; "
534+ "make() { printf '__WATCH_OUTCOME__=gone\\ n'; return 3; }; "
535+ "move_board_item() { printf 'move:%s %s\\ n' \" $1\" \" $2\" ; }; "
536+ "sleep() { printf 'sleep:%s\\ n' \" $1\" ; return 0; }; "
537+ "STATE_FILE=\" $state_file\" MAKE=make POLL_INTERVAL=600 "
538+ "watch_and_dispatch ready run-pipeline 'Ready issues'"
539+ ),
540+ ],
541+ cwd = REPO_ROOT ,
542+ capture_output = True ,
543+ text = True ,
544+ )
545+ state_file_exists = state_file .exists ()
546+ self .assertEqual (proc .returncode , 2 , proc .stderr )
547+ self .assertNotIn ("move:PVTI_1 on-hold" , proc .stdout )
548+ self .assertFalse (state_file_exists )
441549
442550 def test_watch_and_dispatch_calls_poll_with_correct_args (self ) -> None :
443551 if shutil .which ("dash" ) is None :
0 commit comments