Feature: Output Rendering Framework The output rendering framework decouples command output data from visual presentation through a session/handle/materialiser pipeline. # --- OutputSession lifecycle --- Scenario: OutputSession opens and closes cleanly Given an OutputSession with format "plain" When the session is closed Then the session state is "closed" Scenario: OutputSession works as a context manager When I use an OutputSession context manager with format "plain" Then the session was closed automatically Scenario: OutputSession context manager sets exit_code on exception When I use an OutputSession context manager that raises an exception Then the session exit code is 1 # --- PanelHandle --- Scenario: PanelHandle set_entry adds key-value pairs Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I set entry "Name" to "test-project" And I set entry "Status" to "active" And I close the panel handle Then the panel has 2 entries And the panel entry "Name" has value "test-project" Scenario: PanelHandle set_entry updates existing key Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I set entry "Name" to "old-value" And I set entry "Name" to "new-value" Then the panel entry "Name" has value "new-value" And the panel has 1 entries Scenario: PanelHandle set_entries batch update Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I batch set entries with keys "A,B,C" and values "1,2,3" Then the panel has 3 entries Scenario: PanelHandle remove_entry removes a key Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I set entry "Name" to "value" And I set entry "Extra" to "remove-me" And I remove entry "Extra" Then the panel has 1 entries Scenario: PanelHandle add_line convenience alias Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I add line "Key" with value "Val" Then the panel entry "Key" has value "Val" Scenario: Writing to a closed PanelHandle raises ElementClosedError Given an OutputSession with format "plain" When I create a panel handle with title "Details" And I close the panel handle Then setting entry "X" to "Y" raises ElementClosedError # --- TableHandle --- Scenario: TableHandle add_row with dict Given an OutputSession with format "plain" When I create a table handle with columns "Name,Status" And I add a dict row with Name "foo" and Status "active" Then the table has 1 rows Scenario: TableHandle add_row with list Given an OutputSession with format "plain" When I create a table handle with columns "Name,Status" And I add a list row with values "bar,inactive" Then the table has 1 rows Scenario: TableHandle add_rows batch Given an OutputSession with format "plain" When I create a table handle with columns "Name,Status" And I add 3 batch rows Then the table has 3 rows Scenario: TableHandle set_summary Given an OutputSession with format "plain" When I create a table handle with columns "Name,Count" And I add a dict row with Name "a" and Status "1" And I set summary with total "1" Then the table summary is set Scenario: TableHandle set_sort_key Given an OutputSession with format "plain" When I create a table handle with columns "Name,Status" And I set sort key to "Name" Then the table sort key is "Name" Scenario: Writing to a closed TableHandle raises ElementClosedError Given an OutputSession with format "plain" When I create a table handle with columns "Name,Status" And I close the table handle Then adding a row raises ElementClosedError # --- StatusHandle --- Scenario: StatusHandle creation and update Given an OutputSession with format "plain" When I create a status handle with message "Working..." And I update the status message to "Done" And I update the status level to "ok" And I set the status detail to "All tasks complete" Then the status message is "Done" And the status level is "ok" And the status detail is "All tasks complete" Scenario: StatusHandle rejects invalid level Given an OutputSession with format "plain" When I create a status handle with message "Test" Then setting status level to "invalid" raises ValueError # --- ProgressHandle --- Scenario: ProgressHandle set_progress updates current and total Given an OutputSession with format "plain" When I create a progress handle labelled "Uploading" And I set progress to 50 of 100 Then the progress current is 50 And the progress total is 100 Scenario: ProgressHandle tick increments by 1 Given an OutputSession with format "plain" When I create a progress handle labelled "Processing" And I tick the progress 3 times Then the progress current is 3 Scenario: ProgressHandle increment by delta Given an OutputSession with format "plain" When I create a progress handle labelled "Processing" And I increment progress by 5 Then the progress current is 5 Scenario: ProgressHandle set_label changes label Given an OutputSession with format "plain" When I create a progress handle labelled "Old Label" And I set progress label to "New Label" Then the progress label is "New Label" Scenario: ProgressHandle set_indeterminate enables spinner mode Given an OutputSession with format "plain" When I create a progress handle labelled "Loading" And I set progress to indeterminate mode Then the progress is indeterminate Scenario: ProgressHandle set_step_status tracks steps Given an OutputSession with format "plain" When I create a progress handle labelled "Pipeline" with steps "A,B,C" And I set step "A" status to "done" And I set step "B" status to "active" Then step "A" has status "done" And step "B" has status "active" And step "C" has status "pending" Scenario: ProgressHandle rejects negative current Given an OutputSession with format "plain" When I create a progress handle labelled "Test" Then setting progress to -1 raises ValueError Scenario: ProgressHandle rejects invalid step status Given an OutputSession with format "plain" When I create a progress handle labelled "Test" Then setting step "X" to status "bogus" raises ValueError # --- PlainMaterializer --- Scenario: PlainMaterializer renders panel as plain ASCII Given an OutputSession with format "plain" using PlainMaterializer When I create a panel handle with title "Info" And I set entry "Name" to "test" And I set entry "Value" to "42" And I close the panel handle And the session is closed Then the plain output contains "Info" And the plain output contains "Name: test" And the plain output contains "Value: 42" And the plain output does not contain ANSI codes Scenario: PlainMaterializer renders table as plain ASCII Given an OutputSession with format "plain" using PlainMaterializer When I create a table handle with columns "Name,Status" And I add a dict row with Name "foo" and Status "active" And I close the table handle And the session is closed Then the plain output contains "Name" And the plain output contains "foo" And the plain output does not contain ANSI codes Scenario: PlainMaterializer renders status as plain ASCII Given an OutputSession with format "plain" using PlainMaterializer When I create a status handle with message "All good" And I update the status level to "ok" And I close the status handle And the session is closed Then the plain output contains "[OK]" And the plain output contains "All good" Scenario: PlainMaterializer renders progress as plain ASCII Given an OutputSession with format "plain" using PlainMaterializer When I create a progress handle labelled "Downloading" with total 100 And I set progress to 75 of 100 And I close the progress handle And the session is closed Then the plain output contains "Downloading" And the plain output contains "75/100" # --- ColorMaterializer --- Scenario: ColorMaterializer renders panel with ANSI codes Given an OutputSession with format "color" using ColorMaterializer When I create a panel handle with title "Coloured" And I set entry "Key" to "val" And I close the panel handle And the session is closed Then the color output contains ANSI codes And the color output contains "Coloured" Scenario: ColorMaterializer renders status with ANSI codes Given an OutputSession with format "color" using ColorMaterializer When I create a status handle with message "Warning text" And I update the status level to "warn" And I close the status handle And the session is closed Then the color output contains "[WARN]" And the color output contains ANSI codes # --- TableMaterializer --- Scenario: TableMaterializer renders table with box-drawing Given an OutputSession with format "table" using TableMaterializer When I create a table handle with columns "Name,Status" And I add a dict row with Name "svc" and Status "up" And I close the table handle And the session is closed Then the table output contains box-drawing characters And the table output contains "svc" # --- RichMaterializer --- Scenario: RichMaterializer renders output Given an OutputSession with format "rich" using RichMaterializer When I create a panel handle with title "Rich Panel" And I set entry "Hello" to "World" And I close the panel handle And the session is closed Then the rich output contains "Rich Panel" # --- JsonMaterializer --- Scenario: JsonMaterializer serialises all elements as JSON Given an OutputSession with format "json" using JsonMaterializer When I create a panel handle with title "JSON Panel" And I set entry "Key1" to "Val1" And I close the panel handle And the session is closed Then the json output is valid JSON And the json output contains element type "panel" And the json output contains "Key1" Scenario: JsonMaterializer includes tables in output Given an OutputSession with format "json" using JsonMaterializer When I create a table handle with columns "Col1,Col2" And I add a dict row with Name "Col1" and Status "Val1" And I close the table handle And the session is closed Then the json output is valid JSON And the json output contains element type "table" # --- YamlMaterializer --- Scenario: YamlMaterializer serialises all elements as YAML Given an OutputSession with format "yaml" using YamlMaterializer When I create a panel handle with title "YAML Panel" And I set entry "Alpha" to "Beta" And I close the panel handle And the session is closed Then the yaml output is valid YAML And the yaml output contains "Alpha" # --- Error envelope --- Scenario: JsonMaterializer error envelope Given a JsonMaterializer When I generate an error envelope with code "NOT_FOUND" and message "Resource not found" Then the json error output contains "NOT_FOUND" And the json error output contains "Resource not found" Scenario: YamlMaterializer error envelope Given a YamlMaterializer When I generate a yaml error envelope with code "FORBIDDEN" and message "Access denied" Then the yaml error output contains "FORBIDDEN" And the yaml error output contains "Access denied" # --- Concurrent producers --- Scenario: Concurrent producers do not interleave in plain mode Given an OutputSession with format "plain" using PlainMaterializer When I create two panels concurrently and close them out of order And the session is closed Then the plain output has panel A before panel B # --- Format fallback --- Scenario: Rich format falls back to plain when no TTY Given a non-TTY terminal environment When I select a materializer for format "rich" Then the selected materializer is PlainMaterializer Scenario: Color format falls back to plain when no ANSI support Given a terminal without ANSI support When I select a materializer for format "color" Then the selected materializer is PlainMaterializer Scenario: Plain format always works Given a non-TTY terminal environment When I select a materializer for format "plain" Then the selected materializer is PlainMaterializer Scenario: JSON format ignores terminal capabilities Given a non-TTY terminal environment When I select a materializer for format "json" Then the selected materializer is JsonMaterializer Scenario: Invalid format raises ValueError When I attempt to select a materializer for format "invalid" Then a ValueError is raised # --- Backward compatibility --- Scenario: format_output still works with plain format When I call format_output with a dict and format "plain" Then the output contains key-value pairs Scenario: format_output still works with json format When I call format_output with a dict and format "json" Then the output is valid JSON Scenario: format_output_session renders via OutputSession When I call format_output_session with a dict and format "plain" Then the session output contains the dict keys # --- Handle context manager --- Scenario: Element handle works as context manager Given an OutputSession with format "plain" When I use a panel handle as a context manager Then the handle is closed after the context exits # --- Snapshot --- Scenario: Session snapshot captures all elements Given an OutputSession with format "plain" When I create a panel handle with title "P1" And I set entry "K" to "V" And I create a table handle with columns "A,B" And I add a dict row with Name "A" and Status "B" Then the session snapshot has 2 elements # --- Materializer selection with explicit flag --- Scenario: Explicit format overrides fallback Given a non-TTY terminal environment When I select a materializer for format "rich" with explicit flag Then the selected materializer is RichMaterializer # ================================================================ # Coverage boost: handle validation guards # ================================================================ Scenario: PanelHandle rejects empty key in set_entry Given an OutputSession with format "plain" When I create a panel handle with title "V" Then setting entry with empty key raises ValueError Scenario: PanelHandle rejects None entries in set_entries Given an OutputSession with format "plain" When I create a panel handle with title "V" Then batch setting None entries raises ValueError Scenario: PanelHandle rejects empty key in remove_entry Given an OutputSession with format "plain" When I create a panel handle with title "V" Then removing entry with empty key raises ValueError Scenario: TableHandle rejects None row in add_row Given an OutputSession with format "plain" When I create a table handle with columns "A,B" Then adding a None row raises ValueError Scenario: TableHandle rejects None rows in add_rows Given an OutputSession with format "plain" When I create a table handle with columns "A,B" Then adding None rows raises ValueError Scenario: TableHandle rejects None summary Given an OutputSession with format "plain" When I create a table handle with columns "A,B" Then setting None summary raises ValueError Scenario: TableHandle rejects empty sort key Given an OutputSession with format "plain" When I create a table handle with columns "A,B" Then setting empty sort key raises ValueError Scenario: StatusHandle rejects empty message Given an OutputSession with format "plain" When I create a status handle with message "ok" Then setting empty status message raises ValueError Scenario: StatusHandle rejects empty level Given an OutputSession with format "plain" When I create a status handle with message "ok" Then setting empty status level raises ValueError Scenario: ProgressHandle rejects empty label Given an OutputSession with format "plain" When I create a progress handle labelled "Loading" Then setting empty progress label raises ValueError Scenario: ProgressHandle rejects negative delta Given an OutputSession with format "plain" When I create a progress handle labelled "Loading" with total 10 Then incrementing by negative delta raises ValueError Scenario: ProgressHandle rejects empty step label Given an OutputSession with format "plain" When I create a progress handle labelled "Loading" with steps "A,B" Then setting step status with empty label raises ValueError # ================================================================ # Coverage boost: handle data paths # ================================================================ Scenario: PanelHandle set_entries updates existing entry Given an OutputSession with format "plain" When I create a panel handle with title "UP" And I set entry "K" to "old" And I batch set entries with keys "K" and values "new" Then the panel entry "K" has value "new" Scenario: TableHandle add_rows with list-type rows Given an OutputSession with format "plain" When I create a table handle with columns "A,B" And I batch add list rows with values "x,y" and "m,n" Then the table has 2 rows Scenario: ProgressHandle increment adds to current Given an OutputSession with format "plain" When I create a progress handle labelled "Down" with total 100 And I set progress to 10 of 100 And I increment progress by 5 Then the progress current is 15 Scenario: ProgressHandle set_step_status creates steps list Given an OutputSession with format "plain" When I create a progress handle labelled "Steps" And I set step "Alpha" status to "done" Then step "Alpha" has status "done" # ================================================================ # Coverage boost: color materializer rendering # ================================================================ Scenario: Color materializer renders table with ANSI codes Given an OutputSession with format "color" using ColorMaterializer When I create a table handle with columns "Name,Status" And I add a dict row with Name "foo" and Status "ok" And I close the table handle And the session is closed Then the color output contains ANSI codes And the color output contains "foo" Scenario: Color materializer renders empty table Given an OutputSession with format "color" using ColorMaterializer When I create a table with no columns And I close the table handle And the session is closed Then the color output contains "empty table" Scenario: Color materializer renders status with detail Given an OutputSession with format "color" using ColorMaterializer When I create a status handle with message "Building" And I set the status detail to "step 3 of 10" And I close the status handle And the session is closed Then the color output contains ANSI codes And the color output contains "Building" Scenario: Color materializer renders progress indicator Given an OutputSession with format "color" using ColorMaterializer When I create a progress handle labelled "Downloading" with total 50 And I set progress to 25 of 50 And I close the progress handle And the session is closed Then the color output contains ANSI codes And the color output contains "Downloading" Scenario: Color materializer renders panel with style hints Given an OutputSession with format "color" using ColorMaterializer When I create a panel handle with title "Results" And I set entry with key "passed" value "yes" and style hint "success" And I set entry with key "warnings" value "3" and style hint "warning" And I set entry with key "errors" value "1" and style hint "error" And I close the panel handle And the session is closed Then the color output contains ANSI codes And the color output contains "passed" # ================================================================ # Coverage boost: plain materializer edge cases # ================================================================ Scenario: Plain materializer renders empty table Given an OutputSession with format "plain" using PlainMaterializer When I create a table with no columns And I close the table handle And the session is closed Then the plain output contains "empty table" Scenario: Plain materializer renders table with title and summary Given an OutputSession with format "plain" using PlainMaterializer When I create a table handle with columns "Name,Count" And I set table title to "Report" And I add a dict row with Name "A" and Status "10" And I set summary with total "10" And I close the table handle And the session is closed Then the plain output contains "Report" Scenario: Plain materializer renders status with detail Given an OutputSession with format "plain" using PlainMaterializer When I create a status handle with message "Processing" And I set the status detail to "file 3 of 10" And I close the status handle And the session is closed Then the plain output contains "Processing" And the plain output contains "file 3 of 10" Scenario: Plain materializer renders indeterminate progress Given an OutputSession with format "plain" using PlainMaterializer When I create a progress handle labelled "Scanning" And I set progress to indeterminate mode And I close the progress handle And the session is closed Then the plain output contains "Scanning" Scenario: Plain materializer renders current-only progress Given an OutputSession with format "plain" using PlainMaterializer When I create a progress handle labelled "Items" And I set progress current to 42 And I close the progress handle And the session is closed Then the plain output contains "Items" And the plain output contains "42" Scenario: Plain materializer renders progress with steps Given an OutputSession with format "plain" using PlainMaterializer When I create a progress handle labelled "Deploy" with steps "Build,Test,Ship" And I set step "Build" status to "done" And I set step "Test" status to "active" And I close the progress handle And the session is closed Then the plain output contains "Build" And the plain output contains "Test" # ================================================================ # Coverage boost: serialization helpers (JSON/YAML) # ================================================================ Scenario: JSON materializer serializes status element Given an OutputSession with format "json" using JsonMaterializer When I create a status handle with message "Done" And I set the status detail to "All passed" And I close the status handle And the session is closed Then the json output is valid JSON And the json output contains element type "status" And the json output contains "Done" Scenario: JSON materializer serializes progress with steps Given an OutputSession with format "json" using JsonMaterializer When I create a progress handle labelled "Pipeline" with steps "A,B" And I set step "A" status to "done" And I close the progress handle And the session is closed Then the json output is valid JSON And the json output contains element type "progress" And the json output contains "Pipeline" Scenario: JSON materializer serializes panel with style hint and icon Given an OutputSession with format "json" using JsonMaterializer When I create a panel handle with title "Info" And I set entry with key "status" value "ok" and style hint "success" And I set entry with icon key "icon_entry" value "check" and icon "check_mark" And I close the panel handle And the session is closed Then the json output is valid JSON And the json output contains "style_hint" And the json output contains "icon" Scenario: JSON error envelope includes details Given a JsonMaterializer When I generate an error envelope with code "ERR" message "fail" and details "traceback" Then the json error output contains "traceback" # ================================================================ # Coverage boost: box-drawing materializer edge cases # ================================================================ Scenario: Box-drawing materializer renders empty table Given an OutputSession with format "table" using TableMaterializer When I create a table with no columns And I close the table handle And the session is closed Then the strategy output contains "empty table" Scenario: Box-drawing materializer renders table with title Given an OutputSession with format "table" using TableMaterializer When I create a table handle with columns "X,Y" And I set table title to "Summary" And I add a dict row with Name "X" and Status "1" And I close the table handle And the session is closed Then the strategy output contains "Summary" # ================================================================ # Coverage boost: format selection paths # ================================================================ Scenario: Select yaml materializer When I select a materializer for format "yaml" Then the selected materializer is YamlMaterializer Scenario: Select materializer rejects empty format When I attempt to select a materializer with empty format Then a ValueError is raised Scenario: Rich format falls back to color when no cursor support Given a terminal with ANSI but no cursor support When I select a materializer for format "rich" Then the selected materializer is ColorMaterializer Scenario: Color format uses color materializer with ANSI support Given a terminal with ANSI support When I select a materializer for format "color" Then the selected materializer is ColorMaterializer Scenario: Table format uses table materializer on TTY Given a TTY terminal with ANSI support When I select a materializer for format "table" Then the selected materializer is TableMaterializer Scenario: Table format falls back to plain on non-TTY Given a non-TTY terminal environment When I select a materializer for format "table" Then the selected materializer is PlainMaterializer # ================================================================ # Coverage boost: session edge cases # ================================================================ Scenario: Session rejects creating panel with empty title Given an OutputSession with format "plain" Then creating a panel with empty title raises ValueError Scenario: Session rejects creating status with empty message Given an OutputSession with format "plain" Then creating a status with empty message raises ValueError Scenario: Session rejects creating progress with empty label Given an OutputSession with format "plain" Then creating a progress with empty label raises ValueError Scenario: Closed session rejects new handles Given an OutputSession with format "plain" When the session is closed Then creating a panel on closed session raises RuntimeError Scenario: Session close force-closes open handles Given an OutputSession with format "plain" using PlainMaterializer When I create a panel handle with title "Unclosed" And I set entry "A" to "B" And the session is closed Then the plain output contains "Unclosed" # ================================================================ # Coverage boost: ElementHandle validation guards # ================================================================ Scenario: ElementHandle rejects empty handle_id Given an OutputSession with format "plain" Then constructing a handle with empty handle_id raises ValueError Scenario: ElementHandle rejects empty element_type Given an OutputSession with format "plain" Then constructing a handle with empty element_type raises ValueError Scenario: ElementHandle rejects negative declaration_index Given an OutputSession with format "plain" Then constructing a handle with negative index raises ValueError Scenario: ElementHandle rejects None session Given an OutputSession with format "plain" Then constructing a handle with None session raises ValueError Scenario: ElementHandle rejects None element Given an OutputSession with format "plain" Then constructing a handle with None element raises ValueError # ================================================================ # Coverage boost: handle close and __exit__ edge cases # ================================================================ Scenario: Closing an already-closed handle is a no-op Given an OutputSession with format "plain" using PlainMaterializer When I create a panel handle with title "CloseMe" And I close the panel handle And I close the panel handle again Then the handle state is "closed" Scenario: Handle used as context manager auto-closes Given an OutputSession with format "plain" using PlainMaterializer When I use a panel handle as context manager with title "AutoClose" Then the handle state is "closed" And the plain output contains "AutoClose" # ================================================================ # Coverage boost: table rows as list (not dict) # ================================================================ Scenario: TableHandle accepts list rows Given an OutputSession with format "plain" using PlainMaterializer When I create a table handle with columns "A,B" And I add a list row with values "x,y" And I close the table handle And the session is closed Then the plain output contains "x" Scenario: TableHandle batch add accepts list rows Given an OutputSession with format "plain" using PlainMaterializer When I create a table handle with columns "A,B" And I batch add list rows with values "1,2" and "3,4" And I close the table handle And the session is closed Then the plain output contains "1" # ================================================================ # Coverage boost: color materializer table with title # ================================================================ Scenario: Color materializer renders table title Given an OutputSession with format "color" using ColorMaterializer When I create a table handle with columns "Col1,Col2" And I set table title to "Colored Title" And I add a dict row with Name "Col1" and Status "val" And I close the table handle And the session is closed Then the color output contains "Colored Title" # ================================================================ # Coverage boost: box-draw table dispatch for non-table # ================================================================ Scenario: Table materializer renders panel as plain text Given an OutputSession with format "table" using TableMaterializer When I create a panel handle with title "PanelInTable" And I set entry "info" to "details" And I close the panel handle And the session is closed Then the strategy output contains "PanelInTable" # ================================================================ # Coverage boost: session creates table with ColumnDef objects # ================================================================ Scenario: Session table accepts ColumnDef objects Given an OutputSession with format "plain" using PlainMaterializer When I create a table with ColumnDef objects "Alpha,Beta" And I add a dict row with Name "Alpha" and Status "val1" And I close the table handle And the session is closed Then the plain output contains "Alpha" # ================================================================ # Coverage boost: detect_capabilities function # ================================================================ Scenario: Detect terminal capabilities returns TerminalCapabilities When I detect terminal capabilities Then the capabilities object has is_tty field And the capabilities object has supports_ansi field # ================================================================ # Coverage boost: explicit format flag # ================================================================ Scenario: Explicit format flag creates strategy without fallback When I select a materializer for format "rich" with explicit flag Then the selected materializer is RichMaterializer # ================================================================ # Coverage boost: serializer on_session_end with buffered content # ================================================================ Scenario: Plain materializer flushes remaining buffers on session end Given an OutputSession with format "plain" using PlainMaterializer When I create a panel handle with title "First" And I create a second panel handle with title "Second" And I set entry "a" to "1" on handle 2 And I close handle 2 And I set entry "b" to "2" And I close the panel handle And the session is closed Then the plain output contains "First" And the plain output contains "Second" # ================================================================ # Coverage boost: serializer get_output with None session # ================================================================ Scenario: JSON serializer returns empty when session is None Given a JsonMaterializer with no session Then the serializer get_output returns empty string