@@ -534,6 +534,92 @@ async def mock_caller(server, tool, args):
534534 assert "result 3" in result
535535
536536
537+ @pytest .mark .asyncio
538+ class TestPreviewStage :
539+ """Test preview stage functionality."""
540+
541+ async def test_preview_stage_basic (self ):
542+ """Test that preview stage summarizes JSON data."""
543+ large_data = json .dumps ({"items" : [{"id" : i , "name" : f"Item { i } " } for i in range (100 )]})
544+ mock_caller = AsyncMock (return_value = MockToolResult (large_data ))
545+ engine = ShellEngine (tool_caller = mock_caller )
546+
547+ pipeline = [
548+ {"type" : "tool" , "name" : "get_data" , "server" : "test" , "args" : {}},
549+ {"type" : "preview" , "chars" : 500 },
550+ ]
551+
552+ result = await engine .execute_pipeline (pipeline )
553+
554+ # Should contain preview markers
555+ assert "=== PREVIEW" in result
556+ assert "not valid JSON" in result
557+ assert "=== END PREVIEW ===" in result
558+ # Should show structure but be truncated
559+ assert "items" in result
560+ # The output should be smaller than the input
561+ assert len (result ) < len (large_data )
562+
563+ async def test_preview_stage_shows_omission_markers (self ):
564+ """Test that preview shows /* N more */ markers for truncated data."""
565+ large_array = json .dumps (list (range (1000 )))
566+ mock_caller = AsyncMock (return_value = MockToolResult (large_array ))
567+ engine = ShellEngine (tool_caller = mock_caller )
568+
569+ pipeline = [
570+ {"type" : "tool" , "name" : "get_data" , "server" : "test" , "args" : {}},
571+ {"type" : "preview" , "chars" : 200 },
572+ ]
573+
574+ result = await engine .execute_pipeline (pipeline )
575+
576+ # detailed style should show omission counts
577+ assert "more" in result .lower ()
578+
579+ async def test_preview_stage_default_chars (self ):
580+ """Test that preview stage uses default 3000 chars when not specified."""
581+ mock_caller = AsyncMock (return_value = MockToolResult ('{"test": "data"}' ))
582+ engine = ShellEngine (tool_caller = mock_caller )
583+
584+ pipeline = [
585+ {"type" : "tool" , "name" : "get_data" , "server" : "test" , "args" : {}},
586+ {"type" : "preview" }, # No chars specified
587+ ]
588+
589+ # Should not raise, uses default
590+ result = await engine .execute_pipeline (pipeline )
591+ assert "=== PREVIEW" in result
592+
593+ async def test_preview_stage_invalid_chars (self ):
594+ """Test that preview stage rejects invalid chars parameter."""
595+ mock_caller = AsyncMock (return_value = MockToolResult ('{"test": "data"}' ))
596+ engine = ShellEngine (tool_caller = mock_caller )
597+
598+ pipeline = [
599+ {"type" : "tool" , "name" : "get_data" , "server" : "test" , "args" : {}},
600+ {"type" : "preview" , "chars" : - 100 },
601+ ]
602+
603+ with pytest .raises (RuntimeError , match = "chars.*must be a positive integer" ):
604+ await engine .execute_pipeline (pipeline )
605+
606+ async def test_preview_stage_in_middle_of_pipeline (self ):
607+ """Test that preview can be used mid-pipeline (though output won't be valid JSON)."""
608+ mock_caller = AsyncMock (return_value = MockToolResult ('{"value": 42}' ))
609+ engine = ShellEngine (tool_caller = mock_caller )
610+
611+ # Preview in the middle - subsequent stages see preview output, not original data
612+ pipeline = [
613+ {"type" : "tool" , "name" : "get_data" , "server" : "test" , "args" : {}},
614+ {"type" : "preview" , "chars" : 500 },
615+ {"type" : "command" , "command" : "wc" , "args" : ["-l" ]}, # Count lines
616+ ]
617+
618+ result = await engine .execute_pipeline (pipeline )
619+ # wc -l should return a number (the line count of the preview)
620+ assert result .strip ().isdigit () or result .strip ().split ()[0 ].isdigit ()
621+
622+
537623@pytest .mark .asyncio
538624class TestErrorHandling :
539625 """Test error handling and edge cases."""
0 commit comments