@@ -822,6 +822,115 @@ def test_validate_invalid_on_reject(self):
822822 })
823823 assert any ("on_reject" in e for e in errors )
824824
825+ def test_interactive_prompt_renders_show_file (self , tmp_path , monkeypatch , capsys ):
826+ from specify_cli .workflows .steps .gate import GateStep
827+ from specify_cli .workflows .base import StepContext , StepStatus
828+
829+ review = tmp_path / "spec.md"
830+ review .write_text ("LINE-ONE\n LINE-TWO\n " , encoding = "utf-8" )
831+
832+ monkeypatch .setattr ("sys.stdin.isatty" , lambda : True )
833+ monkeypatch .setattr ("builtins.input" , lambda _prompt = "" : "1" )
834+
835+ step = GateStep ()
836+ config = {
837+ "id" : "review" ,
838+ "message" : "Review the spec." ,
839+ "show_file" : str (review ),
840+ "options" : ["approve" , "reject" ],
841+ }
842+ result = step .execute (config , StepContext ())
843+ out = capsys .readouterr ().out
844+
845+ assert "LINE-ONE" in out and "LINE-TWO" in out
846+ assert str (review ) in out
847+ assert result .status == StepStatus .COMPLETED
848+ assert result .output ["choice" ] == "approve"
849+
850+ def test_interactive_prompt_missing_show_file_does_not_crash (
851+ self , tmp_path , monkeypatch , capsys
852+ ):
853+ from specify_cli .workflows .steps .gate import GateStep
854+ from specify_cli .workflows .base import StepContext , StepStatus
855+
856+ missing = tmp_path / "does-not-exist.md"
857+
858+ monkeypatch .setattr ("sys.stdin.isatty" , lambda : True )
859+ monkeypatch .setattr ("builtins.input" , lambda _prompt = "" : "1" )
860+
861+ step = GateStep ()
862+ config = {
863+ "id" : "review" ,
864+ "message" : "Review." ,
865+ "show_file" : str (missing ),
866+ "options" : ["approve" , "reject" ],
867+ }
868+ result = step .execute (config , StepContext ())
869+ out = capsys .readouterr ().out
870+
871+ assert "could not read file" in out
872+ assert result .status == StepStatus .COMPLETED
873+
874+ def test_non_interactive_show_file_still_pauses_without_reading (
875+ self , tmp_path , monkeypatch
876+ ):
877+ from specify_cli .workflows .steps .gate import GateStep
878+ from specify_cli .workflows .base import StepContext , StepStatus
879+
880+ review = tmp_path / "spec.md"
881+ review .write_text ("CONTENT\n " , encoding = "utf-8" )
882+
883+ monkeypatch .setattr ("sys.stdin.isatty" , lambda : False )
884+
885+ step = GateStep ()
886+ config = {
887+ "id" : "review" ,
888+ "message" : "Review." ,
889+ "show_file" : str (review ),
890+ "options" : ["approve" , "reject" ],
891+ }
892+ result = step .execute (config , StepContext ())
893+ assert result .status == StepStatus .PAUSED
894+ assert result .output ["show_file" ] == str (review )
895+
896+ def test_read_show_file_empty (self , tmp_path ):
897+ from specify_cli .workflows .steps .gate import GateStep
898+
899+ empty = tmp_path / "empty.md"
900+ empty .write_text ("" , encoding = "utf-8" )
901+ assert GateStep ._read_show_file (str (empty )) == ["(file is empty)" ]
902+
903+ def test_read_show_file_truncates_large_file (self , tmp_path ):
904+ from specify_cli .workflows .steps .gate import GateStep
905+
906+ big = tmp_path / "big.md"
907+ big .write_text (
908+ "\n " .join (f"line{ i } " for i in range (GateStep .MAX_SHOW_FILE_LINES + 50 )),
909+ encoding = "utf-8" ,
910+ )
911+ rendered = GateStep ._read_show_file (str (big ))
912+ # MAX_SHOW_FILE_LINES content lines + one truncation notice line.
913+ assert len (rendered ) == GateStep .MAX_SHOW_FILE_LINES + 1
914+ assert "truncated" in rendered [- 1 ]
915+
916+ def test_templated_show_file_resolving_to_non_string_is_coerced (self ):
917+ from specify_cli .workflows .steps .gate import GateStep
918+ from specify_cli .workflows .base import StepContext , StepStatus
919+
920+ # A single-expression template can resolve to a non-string (e.g. a
921+ # number from a prior step); it must be coerced to str, not skipped.
922+ step = GateStep ()
923+ ctx = StepContext (steps = {"prev" : {"output" : {"ref" : 123 }}})
924+ config = {
925+ "id" : "review" ,
926+ "message" : "Review." ,
927+ "show_file" : "{{ steps.prev.output.ref }}" ,
928+ "options" : ["approve" , "reject" ],
929+ }
930+ result = step .execute (config , ctx ) # non-interactive -> PAUSED
931+ assert result .status == StepStatus .PAUSED
932+ assert result .output ["show_file" ] == "123"
933+
825934
826935class TestIfThenStep :
827936 """Test the if/then/else step type."""
0 commit comments