feat(cli): add output rendering framework with materialization strategies
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
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
|
||||
Reference in New Issue
Block a user