afe6b849fe
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 19s
CI / helm (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 3m19s
CI / quality (pull_request) Successful in 3m52s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 4m5s
CI / integration_tests (pull_request) Successful in 7m2s
CI / unit_tests (pull_request) Successful in 11m51s
CI / docker (pull_request) Successful in 1m31s
CI / coverage (pull_request) Successful in 11m50s
CI / e2e_tests (pull_request) Successful in 16m31s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 17s
CI / helm (push) Successful in 21s
CI / lint (push) Successful in 24s
CI / quality (push) Successful in 3m41s
CI / integration_tests (push) Successful in 3m46s
CI / typecheck (push) Successful in 3m55s
CI / security (push) Successful in 4m6s
CI / unit_tests (push) Successful in 7m14s
CI / docker (push) Successful in 1m32s
CI / benchmark-regression (push) Has been skipped
CI / coverage (push) Successful in 11m46s
CI / e2e_tests (push) Successful in 18m1s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 28m19s
CI / benchmark-regression (pull_request) Successful in 54m43s
Implement LiveMaterializationStrategy as the third materialization strategy type (alongside sequential_buffer and accumulate) per spec §26456-26492. The live strategy renders element updates in-place at ~15 fps by tracking a dirty-element set and coalescing updates into frame redraws. RichMaterializer now extends LiveMaterializationStrategy instead of _BaseBufferStrategy, enabling supports_incremental_updates=True. Element rendering still delegates to the colour renderer for visual formatting, maintaining backward compatibility. Changes: - Add _LiveMaterializationStrategy class with frame-rate throttling, dirty-element tracking, and per-frame composition in declaration order - Update RichMaterializer to extend _LiveMaterializationStrategy - Export LiveMaterializationStrategy from materializers.py and __init__.py - Update SD-2 and SD-7 documentation in __init__.py - Add vulture whitelist entry for LiveMaterializationStrategy - Add 5 Behave scenarios covering live strategy rendering, incremental update support, dirty-element coalescing, all-element-type rendering, and frame rate validation - Add step definitions for the new scenarios ISSUES CLOSED: #903
2237 lines
95 KiB
Gherkin
2237 lines
95 KiB
Gherkin
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 table when no cursor support but TTY
|
|
Given a terminal with ANSI but no cursor support
|
|
When I select a materializer for format "rich"
|
|
Then the selected materializer is TableMaterializer
|
|
|
|
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: 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
|
|
|
|
# ================================================================
|
|
# TreeHandle
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle creates a tree with root label
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then the tree root label is "Root"
|
|
|
|
Scenario: TreeHandle add_child adds children to root
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Project"
|
|
And I add a tree child "src" under "Project"
|
|
And I add a tree child "tests" under "Project"
|
|
Then the tree root has 2 children
|
|
|
|
Scenario: TreeHandle add_child adds nested children
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child" under "Root"
|
|
And I add a tree child "grandchild" under "Root/child"
|
|
Then the tree node "Root/child" has 1 children
|
|
|
|
Scenario: TreeHandle set_node_style updates style hint
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "item" under "Root"
|
|
And I set tree node style "Root/item" to "success"
|
|
Then the tree node "Root/item" has style "success"
|
|
|
|
Scenario: TreeHandle rejects empty label
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then adding a tree child with empty label raises ValueError
|
|
|
|
Scenario: TreeHandle rejects invalid parent path
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then adding a tree child under invalid path raises ValueError
|
|
|
|
Scenario: TreeHandle rejects empty path in set_node_style
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then setting tree node style with empty path raises ValueError
|
|
|
|
Scenario: Writing to closed TreeHandle raises ElementClosedError
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
And I close the tree handle
|
|
Then adding a tree child on closed handle raises ElementClosedError
|
|
|
|
Scenario: Session rejects tree with empty root label
|
|
Given an OutputSession with format "plain"
|
|
Then creating a tree with empty root label raises ValueError
|
|
|
|
# ================================================================
|
|
# TextHandle
|
|
# ================================================================
|
|
|
|
Scenario: TextHandle creates a text block
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "Hello world"
|
|
Then the text content is "Hello world"
|
|
|
|
Scenario: TextHandle append adds text
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "Hello"
|
|
And I append text " world"
|
|
Then the text content is "Hello world"
|
|
|
|
Scenario: TextHandle set_content replaces text
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "old"
|
|
And I set text content to "new"
|
|
Then the text content is "new"
|
|
|
|
Scenario: TextHandle rejects None in append
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "x"
|
|
Then appending None text raises ValueError
|
|
|
|
Scenario: TextHandle rejects None in set_content
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "x"
|
|
Then setting None text content raises ValueError
|
|
|
|
Scenario: Writing to closed TextHandle raises ElementClosedError
|
|
Given an OutputSession with format "plain"
|
|
When I create a text handle with content "x"
|
|
And I close the text handle
|
|
Then appending to closed text handle raises ElementClosedError
|
|
|
|
# ================================================================
|
|
# CodeHandle
|
|
# ================================================================
|
|
|
|
Scenario: CodeHandle creates a code block
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "print('hi')"
|
|
Then the code content is "print('hi')"
|
|
|
|
Scenario: CodeHandle set_content replaces code
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "old"
|
|
And I set code content to "new code"
|
|
Then the code content is "new code"
|
|
|
|
Scenario: CodeHandle set_language updates language
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "x = 1" and language "python"
|
|
And I set code language to "javascript"
|
|
Then the code language is "javascript"
|
|
|
|
Scenario: CodeHandle set_highlight_lines sets lines
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "a\nb\nc"
|
|
And I set code highlight lines to "1,3"
|
|
Then the code highlight lines are "1,3"
|
|
|
|
Scenario: CodeHandle rejects None content
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "x"
|
|
Then setting None code content raises ValueError
|
|
|
|
Scenario: CodeHandle rejects empty language
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "x"
|
|
Then setting empty code language raises ValueError
|
|
|
|
Scenario: CodeHandle rejects None highlight lines
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "x"
|
|
Then setting None code highlight lines raises ValueError
|
|
|
|
Scenario: Writing to closed CodeHandle raises ElementClosedError
|
|
Given an OutputSession with format "plain"
|
|
When I create a code handle with content "x"
|
|
And I close the code handle
|
|
Then setting code content on closed handle raises ElementClosedError
|
|
|
|
# ================================================================
|
|
# DiffHandle
|
|
# ================================================================
|
|
|
|
Scenario: DiffHandle creates a diff block
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
Then the diff file_a is "a.py"
|
|
And the diff file_b is "b.py"
|
|
|
|
Scenario: DiffHandle add_hunk adds hunks
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1,3 +1,4 @@" and context line "line1" and add line "new"
|
|
Then the diff has 1 hunks
|
|
|
|
Scenario: DiffHandle set_stats sets statistics
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I set diff stats with 5 insertions and 3 deletions
|
|
Then the diff stats insertions is 5
|
|
And the diff stats deletions is 3
|
|
|
|
Scenario: DiffHandle rejects empty hunk header
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
Then adding a diff hunk with empty header raises ValueError
|
|
|
|
Scenario: DiffHandle rejects None hunk lines
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
Then adding a diff hunk with None lines raises ValueError
|
|
|
|
Scenario: DiffHandle rejects negative insertions
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
Then setting diff stats with negative insertions raises ValueError
|
|
|
|
Scenario: DiffHandle rejects negative deletions
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
Then setting diff stats with negative deletions raises ValueError
|
|
|
|
Scenario: Writing to closed DiffHandle raises ElementClosedError
|
|
Given an OutputSession with format "plain"
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I close the diff handle
|
|
Then adding a diff hunk on closed handle raises ElementClosedError
|
|
|
|
# ================================================================
|
|
# SeparatorHandle
|
|
# ================================================================
|
|
|
|
Scenario: SeparatorHandle is auto-closed on creation
|
|
Given an OutputSession with format "plain"
|
|
When I create a separator with style "line"
|
|
Then the separator handle is closed
|
|
|
|
Scenario: SeparatorHandle accepts blank style
|
|
Given an OutputSession with format "plain"
|
|
When I create a separator with style "blank"
|
|
Then the separator handle is closed
|
|
|
|
Scenario: SeparatorHandle accepts double style
|
|
Given an OutputSession with format "plain"
|
|
When I create a separator with style "double"
|
|
Then the separator handle is closed
|
|
|
|
Scenario: SeparatorHandle rejects invalid style
|
|
Given an OutputSession with format "plain"
|
|
Then creating a separator with invalid style raises ValueError
|
|
|
|
# ================================================================
|
|
# ActionHintHandle
|
|
# ================================================================
|
|
|
|
Scenario: ActionHintHandle is auto-closed on creation
|
|
Given an OutputSession with format "plain"
|
|
When I create an action hint with commands "agents list,agents deploy"
|
|
Then the action hint handle is closed
|
|
|
|
Scenario: ActionHintHandle stores commands and description
|
|
Given an OutputSession with format "plain"
|
|
When I create an action hint with commands "agents list" and description "Try these"
|
|
Then the action hint has 1 commands
|
|
And the action hint description is "Try these"
|
|
|
|
Scenario: ActionHintHandle rejects empty commands
|
|
Given an OutputSession with format "plain"
|
|
Then creating an action hint with empty commands raises ValueError
|
|
|
|
# ================================================================
|
|
# Plain materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: Plain materializer renders tree with guides
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child1" under "Root"
|
|
And I add a tree child "child2" under "Root"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "Root"
|
|
And the plain output contains "child1"
|
|
And the plain output contains "child2"
|
|
And the plain output does not contain ANSI codes
|
|
|
|
Scenario: Plain materializer renders tree without guides
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Flat" and show_guides false
|
|
And I add a tree child "item" under "Flat"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "Flat"
|
|
And the plain output contains "item"
|
|
|
|
Scenario: Plain materializer renders tree with max depth hint
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Deep" and max_depth_hint 1
|
|
And I add a tree child "level1" under "Deep"
|
|
And I add a tree child "level2" under "Deep/level1"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "Deep"
|
|
And the plain output contains "level1"
|
|
And the plain output contains "more"
|
|
|
|
Scenario: Plain materializer renders text block
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a text handle with content "Hello plain world"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the plain output contains "Hello plain world"
|
|
And the plain output does not contain ANSI codes
|
|
|
|
Scenario: Plain materializer renders text with indent
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a text handle with content "indented" and indent 4
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the plain output contains " indented"
|
|
|
|
Scenario: Plain materializer renders code block
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a code handle with content "x = 1" and language "python"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the plain output contains "x = 1"
|
|
And the plain output does not contain ANSI codes
|
|
|
|
Scenario: Plain materializer renders code with line numbers
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a code handle with content "a\nb\nc" and line_numbers true
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the plain output contains "1 |"
|
|
And the plain output contains "2 |"
|
|
|
|
Scenario: Plain materializer renders diff block
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a diff handle with file_a "old.py" and file_b "new.py"
|
|
And I add a diff hunk with header "@@ -1,2 +1,3 @@" and context line "same" and add line "added"
|
|
And I set diff stats with 1 insertions and 0 deletions
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the plain output contains "--- old.py"
|
|
And the plain output contains "+++ new.py"
|
|
And the plain output contains "+added"
|
|
And the plain output contains "1 insertion(s)"
|
|
And the plain output does not contain ANSI codes
|
|
|
|
Scenario: Plain materializer renders separator line
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a separator with style "line"
|
|
And the session is closed
|
|
Then the plain output contains "----"
|
|
|
|
Scenario: Plain materializer renders separator double
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a separator with style "double"
|
|
And the session is closed
|
|
Then the plain output contains "===="
|
|
|
|
Scenario: Plain materializer renders separator blank
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a separator with style "blank"
|
|
And the session is closed
|
|
Then the session completed without error
|
|
|
|
Scenario: Plain materializer renders action hint
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create an action hint with commands "agents list,agents deploy" and description "Try these"
|
|
And the session is closed
|
|
Then the plain output contains "Next steps"
|
|
And the plain output contains "$ agents list"
|
|
And the plain output contains "Try these"
|
|
And the plain output does not contain ANSI codes
|
|
|
|
# ================================================================
|
|
# Color materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders tree with ANSI
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child1" under "Root"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "Root"
|
|
|
|
Scenario: Color materializer renders text block
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a text handle with content "Color text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the color output contains "Color text"
|
|
|
|
Scenario: Color materializer renders code with language header
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a code handle with content "x = 1" and language "python"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "python"
|
|
|
|
Scenario: Color materializer renders diff with coloured lines
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1,2 +1,3 @@" and context line "same" and add line "new"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
Scenario: Color materializer renders diff with stats
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I set diff stats with 3 insertions and 2 deletions
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "insertion"
|
|
|
|
Scenario: Color materializer renders separator with ANSI
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a separator with style "line"
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
Scenario: Color materializer renders double separator
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a separator with style "double"
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
Scenario: Color materializer renders action hint with ANSI
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create an action hint with commands "agents list" and description "Hint"
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "agents list"
|
|
|
|
# ================================================================
|
|
# Table materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: Table materializer renders tree using colour renderer
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a tree handle with root label "TreeRoot"
|
|
And I add a tree child "branch" under "TreeRoot"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the strategy output contains "TreeRoot"
|
|
|
|
Scenario: Table materializer renders code using colour renderer
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a code handle with content "code()" and language "python"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the strategy output contains "code()"
|
|
|
|
Scenario: Table materializer renders diff using colour renderer
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "new"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the strategy output contains "a.py"
|
|
|
|
Scenario: Table materializer renders text as plain
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a text handle with content "plain text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the strategy output contains "plain text"
|
|
|
|
# ================================================================
|
|
# Rich materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: Rich materializer renders tree with ANSI
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child" under "Root"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the rich output contains "Root"
|
|
|
|
Scenario: Rich materializer renders code with language
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a code handle with content "fn()" and language "python"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the rich output contains "fn()"
|
|
|
|
# ================================================================
|
|
# JSON materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: JSON materializer serializes tree element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a tree handle with root label "Project"
|
|
And I add a tree child "src" under "Project"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "tree"
|
|
And the json output contains "Project"
|
|
And the json output contains "src"
|
|
|
|
Scenario: JSON materializer serializes text element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a text handle with content "Some text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "text"
|
|
And the json output contains "Some text"
|
|
|
|
Scenario: JSON materializer serializes code element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a code handle with content "x = 1" and language "python"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "code"
|
|
And the json output contains "python"
|
|
|
|
Scenario: JSON materializer serializes code with highlight lines
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a code handle with content "a\nb\nc"
|
|
And I set code highlight lines to "1,3"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "highlight_lines"
|
|
|
|
Scenario: JSON materializer serializes diff element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a diff handle with file_a "old.py" and file_b "new.py"
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "added"
|
|
And I set diff stats with 1 insertions and 0 deletions
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "diff"
|
|
And the json output contains "old.py"
|
|
And the json output contains "stats"
|
|
|
|
Scenario: JSON materializer serializes separator element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a separator with style "line"
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "separator"
|
|
|
|
Scenario: JSON materializer serializes action hint element
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create an action hint with commands "agents list" and description "Try this"
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains element type "action_hint"
|
|
And the json output contains "agents list"
|
|
|
|
# ================================================================
|
|
# YAML materializer rendering: new element types
|
|
# ================================================================
|
|
|
|
Scenario: YAML materializer serializes tree element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a tree handle with root label "Project"
|
|
And I add a tree child "src" under "Project"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "tree"
|
|
And the yaml output contains "Project"
|
|
|
|
Scenario: YAML materializer serializes text element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a text handle with content "yaml text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "text"
|
|
|
|
Scenario: YAML materializer serializes code element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a code handle with content "fn()" and language "ruby"
|
|
And I close the code handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "ruby"
|
|
|
|
Scenario: YAML materializer serializes diff element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "diff"
|
|
|
|
Scenario: YAML materializer serializes separator element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a separator with style "double"
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "separator"
|
|
|
|
Scenario: YAML materializer serializes action hint element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create an action hint with commands "agents list" and description "Do this"
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "action_hint"
|
|
|
|
# ================================================================
|
|
# Plain diff rendering: edge cases
|
|
# ================================================================
|
|
|
|
Scenario: Plain materializer renders diff with no files
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a diff handle with no files
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the plain output contains "@@ -1 +1 @@"
|
|
|
|
Scenario: Plain materializer renders diff with remove lines
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1,2 +1,1 @@" and remove line "deleted"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the plain output contains "-deleted"
|
|
|
|
# ================================================================
|
|
# Color diff rendering: edge cases
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders diff with no files
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a diff handle with no files
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
Scenario: Color materializer renders separator blank
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a separator with style "blank"
|
|
And the session is closed
|
|
Then the session completed without error
|
|
|
|
# ================================================================
|
|
# TreeHandle: find_node with empty path returns root
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle find_node with empty path returns root
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child" under the root via empty path
|
|
Then the tree root has 1 children
|
|
|
|
# ================================================================
|
|
# F5: Rich materializer — missing 7 element types
|
|
# ================================================================
|
|
|
|
Scenario: Rich materializer renders table
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a table handle with columns "Name,Status"
|
|
And I add a dict row with Name "svc" and Status "ok"
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the rich output contains "svc"
|
|
|
|
Scenario: Rich materializer renders status
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a status handle with message "All good"
|
|
And I close the status handle
|
|
And the session is closed
|
|
Then the rich output contains "All good"
|
|
|
|
Scenario: Rich materializer renders progress
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a progress handle labelled "Loading" with total 100
|
|
And I set progress to 50 of 100
|
|
And I close the progress handle
|
|
And the session is closed
|
|
Then the rich output contains "Loading"
|
|
|
|
Scenario: Rich materializer renders text
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a text handle with content "Some rich text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the rich output contains "Some rich text"
|
|
|
|
Scenario: Rich materializer renders diff
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a diff handle with file_a "old.py" and file_b "new.py"
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "added"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the rich output contains "old.py"
|
|
|
|
Scenario: Rich materializer renders separator
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a separator with style "line"
|
|
And the session is closed
|
|
Then the session completed without error
|
|
|
|
Scenario: Rich materializer renders action hint
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create an action hint with commands "deploy"
|
|
And the session is closed
|
|
Then the rich output contains "deploy"
|
|
|
|
# ================================================================
|
|
# N4: strip_terminal_escapes — security-critical function
|
|
# ================================================================
|
|
|
|
Scenario: strip_terminal_escapes removes 7-bit CSI sequences
|
|
When I strip terminal escapes from "hello\x1b[31mred\x1b[0m world"
|
|
Then the stripped result is "hellored world"
|
|
|
|
Scenario: strip_terminal_escapes removes 8-bit CSI sequences
|
|
When I strip terminal escapes from text with 8-bit CSI
|
|
Then the stripped result does not contain bytes 0x9b
|
|
|
|
Scenario: strip_terminal_escapes removes OSC sequences
|
|
When I strip terminal escapes from text with OSC title set
|
|
Then the stripped result does not contain OSC payload
|
|
|
|
Scenario: strip_terminal_escapes removes C1 control characters
|
|
When I strip terminal escapes from text with C1 control chars
|
|
Then the stripped result does not contain bytes 0x80 through 0x9f
|
|
|
|
Scenario: strip_terminal_escapes preserves normal text
|
|
When I strip terminal escapes from "hello world 123"
|
|
Then the stripped result is "hello world 123"
|
|
|
|
Scenario: strip_terminal_escapes preserves tabs and newlines
|
|
When I strip terminal escapes from "line1\tindented\nline2"
|
|
Then the stripped result is "line1\tindented\nline2"
|
|
|
|
Scenario: strip_terminal_escapes removes BEL-terminated OSC
|
|
When I strip terminal escapes from text with BEL-terminated OSC
|
|
Then the stripped result is "safetext"
|
|
|
|
Scenario: strip_terminal_escapes removes private-mode CSI
|
|
When I strip terminal escapes from text with private-mode CSI
|
|
Then the stripped result is "content"
|
|
|
|
Scenario: strip_terminal_escapes removes hyperlink OSC injection
|
|
When I strip terminal escapes from text with hyperlink OSC
|
|
Then the stripped result is "click here"
|
|
|
|
Scenario: strip_terminal_escapes handles empty input
|
|
When I strip terminal escapes from ""
|
|
Then the stripped result is ""
|
|
|
|
Scenario: strip_terminal_escapes handles mixed escape combinations
|
|
When I strip terminal escapes from text with mixed escapes
|
|
Then the stripped result is "hello world"
|
|
|
|
# ================================================================
|
|
# N11: YAML materializer — missing Panel, Table, Status, Progress
|
|
# ================================================================
|
|
|
|
Scenario: YAML materializer serializes panel element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a panel handle with title "Info"
|
|
And I set entry "Name" to "myapp"
|
|
And I close the panel handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "panel"
|
|
And the yaml output contains "Info"
|
|
|
|
Scenario: YAML materializer serializes table element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
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 yaml output is valid YAML
|
|
And the yaml output contains "table"
|
|
And the yaml output contains "Col1"
|
|
|
|
Scenario: YAML materializer serializes status element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a status handle with message "System ready"
|
|
And I close the status handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "status"
|
|
And the yaml output contains "System ready"
|
|
|
|
Scenario: YAML materializer serializes progress element
|
|
Given an OutputSession with format "yaml" using YamlMaterializer
|
|
When I create a progress handle labelled "Deploying" with total 50
|
|
And I set progress to 25 of 50
|
|
And I close the progress handle
|
|
And the session is closed
|
|
Then the yaml output is valid YAML
|
|
And the yaml output contains "progress"
|
|
And the yaml output contains "Deploying"
|
|
|
|
# ================================================================
|
|
# N12: Table materializer — missing Status, Progress, Separator, ActionHint
|
|
# ================================================================
|
|
|
|
Scenario: Table materializer renders status
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a status handle with message "System OK"
|
|
And I close the status handle
|
|
And the session is closed
|
|
Then the strategy output contains "System OK"
|
|
|
|
Scenario: Table materializer renders progress
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a progress handle labelled "Building" with total 10
|
|
And I set progress to 5 of 10
|
|
And I close the progress handle
|
|
And the session is closed
|
|
Then the strategy output contains "Building"
|
|
|
|
Scenario: Table materializer renders separator
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a separator with style "double"
|
|
And the session is closed
|
|
Then the session completed without error
|
|
|
|
Scenario: Table materializer renders action hint
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create an action hint with commands "agents list"
|
|
And the session is closed
|
|
Then the strategy output contains "agents list"
|
|
|
|
# ================================================================
|
|
# N13: Concurrency tests
|
|
# ================================================================
|
|
|
|
Scenario: Concurrent writes to the same handle are thread-safe
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a text handle with content ""
|
|
And I concurrently append 100 lines to the same text handle from 4 threads
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the text handle has exactly 100 appended segments
|
|
|
|
Scenario: Concurrent handle creation is thread-safe
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I concurrently create 20 panels from 4 threads
|
|
And the session is closed
|
|
Then the session snapshot has 20 elements
|
|
|
|
Scenario: Race between session close and handle write does not crash
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a text handle with content "initial"
|
|
And I race session close against handle writes
|
|
Then no unhandled exception was raised
|
|
|
|
# ================================================================
|
|
# N14: Progress throttling
|
|
# ================================================================
|
|
|
|
Scenario: Progress handle throttles rapid updates
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a progress handle labelled "Throttle test" with total 1000
|
|
And I rapidly tick the progress 200 times within 50ms
|
|
And I close the progress handle
|
|
Then fewer than 20 element-updated events were emitted for the progress handle
|
|
|
|
# ================================================================
|
|
# Luis review fixes — M2: TreeHandle rejects slash in labels
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle rejects label containing slash
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then adding a tree child with slash in label raises ValueError
|
|
|
|
# ================================================================
|
|
# Luis review fixes — M3: MAX_ELEMENTS_PER_SESSION enforcement
|
|
# ================================================================
|
|
|
|
Scenario: Session enforces MAX_ELEMENTS_PER_SESSION limit
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create elements up to the session limit
|
|
Then creating one more element raises ValueError
|
|
|
|
# ================================================================
|
|
# R2-C2 fix #15: MAX_TABLE_ROWS enforcement
|
|
# ================================================================
|
|
|
|
Scenario: Table enforces MAX_TABLE_ROWS limit on add_row
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a table handle with columns "Name"
|
|
And I fill the table to its MAX_TABLE_ROWS limit
|
|
Then adding one more row raises ValueError
|
|
|
|
# ================================================================
|
|
# Luis review fixes — M4: sort_descending rendering
|
|
# ================================================================
|
|
|
|
Scenario: Table renders rows in descending sort order
|
|
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 "Alpha" and Status "1"
|
|
And I add a dict row with Name "Charlie" and Status "3"
|
|
And I add a dict row with Name "Bravo" and Status "2"
|
|
And I set sort key to "Name" descending
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the plain output has "Charlie" before "Alpha"
|
|
|
|
# ================================================================
|
|
# Luis review fixes — L4: TextBlock indent validation
|
|
# ================================================================
|
|
|
|
Scenario: TextBlock rejects negative indent
|
|
Given an OutputSession with format "plain"
|
|
Then creating a text handle with negative indent raises ValueError
|
|
|
|
# ================================================================
|
|
# Luis review fixes — L9: TextBlock with wrap=False
|
|
# ================================================================
|
|
|
|
Scenario: Plain materializer renders text with wrap=False verbatim
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a text handle with content "first line" and wrap false and indent 4
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the plain output contains " first line"
|
|
|
|
# ================================================================
|
|
# Luis review fixes — L10: DiffBlock with no files and no hunks
|
|
# ================================================================
|
|
|
|
Scenario: Plain materializer renders empty diff block
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a diff handle with no files
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the session completed without error
|
|
|
|
# ================================================================
|
|
# Review fix — collapsed tree node rendering (L3 fix validation)
|
|
# ================================================================
|
|
|
|
Scenario: Plain materializer renders collapsed tree child with marker
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a collapsed tree child "hidden" under "Root"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "[collapsed]"
|
|
And the plain output contains "hidden"
|
|
|
|
Scenario: Plain materializer renders collapsed child in flat mode
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Flat" and show_guides false
|
|
And I add a collapsed tree child "hidden" under "Flat"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "[collapsed]"
|
|
And the plain output contains "hidden"
|
|
|
|
# ================================================================
|
|
# Review fix — detect_terminal_capabilities actual assertion
|
|
# ================================================================
|
|
|
|
Scenario: Detect terminal capabilities returns correct types
|
|
When I detect terminal capabilities
|
|
Then the capabilities is_tty is a boolean
|
|
And the capabilities supports_ansi is a boolean
|
|
And the capabilities supports_cursor is a boolean
|
|
And the capabilities term is a string
|
|
|
|
# ================================================================
|
|
# Review fix — session double-close behavior (C1 fix validation)
|
|
# ================================================================
|
|
|
|
Scenario: Session double-close does not deadlock or raise
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a panel handle with title "DoubleClose"
|
|
And I set entry "K" to "V"
|
|
And the session is closed
|
|
And the session is closed again
|
|
Then no deadlock occurred and both closes returned snapshots
|
|
|
|
# ================================================================
|
|
# Review fix — ColumnDef non-default field serialization
|
|
# ================================================================
|
|
|
|
Scenario: JSON materializer serializes non-default ColumnDef fields
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a table with non-default ColumnDef fields
|
|
And I add a dict row with Name "Name" and Status "Alice"
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "alignment"
|
|
And the json output contains "sortable"
|
|
And the json output contains "width_hint"
|
|
|
|
# ================================================================
|
|
# P1-1: ProgressHandle validates total parameter
|
|
# ================================================================
|
|
|
|
Scenario: ProgressHandle rejects negative total in set_progress
|
|
Given an OutputSession with format "plain"
|
|
When I create a progress handle labelled "Test"
|
|
Then setting progress total to negative raises ValueError
|
|
|
|
Scenario: Session progress factory rejects negative total
|
|
Given an OutputSession with format "plain"
|
|
Then creating a progress with negative total raises ValueError
|
|
|
|
# ================================================================
|
|
# P1-2: JSON/YAML output includes timing field
|
|
# ================================================================
|
|
|
|
Scenario: JSON output includes timing after session close
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a panel handle with title "Timed"
|
|
And I close the panel handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "timing"
|
|
|
|
# ================================================================
|
|
# P1-4: NO_COLOR environment variable forces plain format
|
|
# ================================================================
|
|
|
|
Scenario: NO_COLOR env var forces plain materializer for rich
|
|
Given the NO_COLOR environment variable is set
|
|
When I select a materializer for format "rich"
|
|
Then the selected materializer is PlainMaterializer
|
|
|
|
Scenario: NO_COLOR env var forces plain materializer for color
|
|
Given the NO_COLOR environment variable is set
|
|
When I select a materializer for format "color"
|
|
Then the selected materializer is PlainMaterializer
|
|
|
|
Scenario: NO_COLOR env var forces plain materializer for table
|
|
Given the NO_COLOR environment variable is set
|
|
When I select a materializer for format "table"
|
|
Then the selected materializer is PlainMaterializer
|
|
|
|
Scenario: NO_COLOR env var does not affect json format
|
|
Given the NO_COLOR environment variable is set
|
|
When I select a materializer for format "json"
|
|
Then the selected materializer is JsonMaterializer
|
|
|
|
# ================================================================
|
|
# P2-1: DiffLine.line_type backward compatibility
|
|
# ================================================================
|
|
|
|
Scenario: DiffLine accepts line_type field name
|
|
When I create a DiffLine with line_type "add" and content "new"
|
|
Then the DiffLine line_type is "add"
|
|
|
|
Scenario: DiffLine accepts type alias for backward compatibility
|
|
When I create a DiffLine with type alias "remove" and content "old"
|
|
Then the DiffLine line_type is "remove"
|
|
|
|
# ================================================================
|
|
# P2-2: Color materializer renders text with ANSI codes
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders text block with ANSI codes
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a text handle with content "Color styled text"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "Color styled text"
|
|
|
|
# ================================================================
|
|
# P2-3: Numeric column sorting
|
|
# ================================================================
|
|
|
|
Scenario: Table sorts numeric column by value not lexicographically
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a table with a numeric column "Score"
|
|
And I add rows with scores 9, 10, 100, 20
|
|
And I set sort key to "Score" ascending
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the plain output has "9" before "10"
|
|
And the plain output has "10" before "20"
|
|
And the plain output has "20" before "100"
|
|
|
|
# ================================================================
|
|
# P2-4: ColumnDef serialization includes all fields
|
|
# ================================================================
|
|
|
|
Scenario: JSON output includes all ColumnDef fields even when default
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a table with default ColumnDef fields
|
|
And I add a dict row with Name "Name" and Status "Alice"
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "col_type"
|
|
And the json output contains "alignment"
|
|
|
|
# ================================================================
|
|
# P2-5: Unicode box-drawing characters
|
|
# ================================================================
|
|
|
|
Scenario: Table materializer uses Unicode box-drawing characters
|
|
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 strategy output contains Unicode box-drawing chars
|
|
|
|
# ================================================================
|
|
# P3-3: MAX_TABLE_ROWS enforcement in add_rows batch
|
|
# ================================================================
|
|
|
|
Scenario: Table enforces MAX_TABLE_ROWS limit on add_rows batch
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a table handle with columns "Name"
|
|
And I fill the table close to its limit
|
|
Then adding a batch that exceeds the limit raises ValueError
|
|
|
|
# ================================================================
|
|
# P3-5: Stress test with 10+ concurrent producers
|
|
# ================================================================
|
|
|
|
Scenario: 10 concurrent producers create handles without corruption
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I concurrently create 10 panels from 10 threads
|
|
And the session is closed
|
|
Then the session snapshot has 10 elements
|
|
|
|
# ================================================================
|
|
# P3-6: Boxdraw summary truncation
|
|
# ================================================================
|
|
|
|
Scenario: Boxdraw table truncates long summary to fit border
|
|
Given an OutputSession with format "table" using TableMaterializer
|
|
When I create a table with a 2-char column "AB"
|
|
And I set a very long summary string
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the strategy output does not have summary exceeding table width
|
|
|
|
# ================================================================
|
|
# P3-7: Explicit format flag for color and table
|
|
# ================================================================
|
|
|
|
Scenario: Explicit color format creates ColorMaterializer without fallback
|
|
Given a non-TTY terminal environment
|
|
When I select a materializer for format "color" with explicit flag
|
|
Then the selected materializer is ColorMaterializer
|
|
|
|
Scenario: Explicit table format creates TableMaterializer without fallback
|
|
Given a non-TTY terminal environment
|
|
When I select a materializer for format "table" with explicit flag
|
|
Then the selected materializer is TableMaterializer
|
|
|
|
# ================================================================
|
|
# P3-8: ProgressHandle rejects zero delta
|
|
# ================================================================
|
|
|
|
Scenario: ProgressHandle rejects zero increment delta
|
|
Given an OutputSession with format "plain"
|
|
When I create a progress handle labelled "Test"
|
|
Then incrementing by zero delta raises ValueError
|
|
|
|
# ================================================================
|
|
# Coverage: color materializer table with summary
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders table with summary
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a table handle with columns "Name,Count"
|
|
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 color output contains ANSI codes
|
|
And the color output contains "total"
|
|
|
|
# ================================================================
|
|
# Coverage: color diff with remove lines
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders diff with remove lines
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1,2 +1,1 @@" and remove line "deleted"
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
# ================================================================
|
|
# Coverage: color text with wrap=False and multiline
|
|
# ================================================================
|
|
|
|
Scenario: Color materializer renders text with wrap=False
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a text handle with content "first line" and wrap false and indent 2
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
And the color output contains "first line"
|
|
|
|
Scenario: Color materializer renders multiline text with empty lines
|
|
Given an OutputSession with format "color" using ColorMaterializer
|
|
When I create a text handle with multiline content including empty lines
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the color output contains ANSI codes
|
|
|
|
# ================================================================
|
|
# Coverage: _reset_counters test helper
|
|
# ================================================================
|
|
|
|
Scenario: ID counter reset helper works correctly
|
|
When I reset the ID counters
|
|
Then the next session ID starts from 1
|
|
And the next handle ID starts from 1
|
|
|
|
# ================================================================
|
|
# Coverage: numeric sort with non-numeric values
|
|
# ================================================================
|
|
|
|
Scenario: Table numeric sort handles non-numeric values gracefully
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a table with a numeric column "Score"
|
|
And I add rows with scores 10, N/A, 5
|
|
And I set sort key to "Score" ascending
|
|
And I close the table handle
|
|
And the session is closed
|
|
Then the plain output has "5" before "10"
|
|
|
|
# ================================================================
|
|
# Coverage: JSON diff with line numbers
|
|
# ================================================================
|
|
|
|
Scenario: JSON materializer serializes diff with line numbers
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with line numbers
|
|
And I close the diff handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "line_number_old"
|
|
And the json output contains "line_number_new"
|
|
|
|
# ================================================================
|
|
# Coverage: JSON tree with style_hint and metadata
|
|
# ================================================================
|
|
|
|
Scenario: JSON materializer serializes tree with style hint and metadata
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "styled" under "Root" with style "success" and metadata
|
|
And I close the tree 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 "metadata"
|
|
|
|
# ================================================================
|
|
# Coverage: flat tree (no guides) depth guard
|
|
# ================================================================
|
|
|
|
Scenario: Flat tree rendering truncates at max_depth_hint
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree handle with root label "Flat" and show_guides false and max_depth_hint 1
|
|
And I add a tree child "child" under "Flat"
|
|
And I add a tree child "grandchild" under "Flat/child"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the plain output contains "Flat"
|
|
And the plain output contains "child"
|
|
And the plain output contains "more"
|
|
|
|
# ================================================================
|
|
# Coverage: _find_node cache miss traversal
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle find_node traverses path on cache miss
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
And I build a 3-level deep tree and query a deep path
|
|
Then the deep tree node was found correctly
|
|
|
|
# ================================================================
|
|
# Coverage: TextHandle _emit_update with incremental strategy
|
|
# ================================================================
|
|
|
|
Scenario: TextHandle emits updates when strategy supports incremental
|
|
Given an OutputSession with a mock incremental strategy
|
|
When I create a text handle with content ""
|
|
And I append text "hello"
|
|
Then the incremental strategy received an update event
|
|
|
|
# ================================================================
|
|
# Coverage: table format falls back to color on non-TTY with ANSI
|
|
# ================================================================
|
|
|
|
Scenario: Table format falls back to color when non-TTY but ANSI
|
|
Given a terminal with ANSI but non-TTY
|
|
When I select a materializer for format "table"
|
|
Then the selected materializer is ColorMaterializer
|
|
|
|
# ================================================================
|
|
# Coverage: set_node_style with non-existent path
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle set_node_style rejects non-existent path
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then setting tree node style on non-existent path raises ValueError
|
|
|
|
# ================================================================
|
|
# Coverage: session status factory rejects invalid level
|
|
# ================================================================
|
|
|
|
Scenario: Session status factory rejects invalid level
|
|
Given an OutputSession with format "plain"
|
|
Then creating a status with invalid level raises ValueError
|
|
|
|
# ================================================================
|
|
# Coverage: JSON tree with collapsed node
|
|
# ================================================================
|
|
|
|
Scenario: JSON materializer serializes tree with collapsed node
|
|
Given an OutputSession with format "json" using JsonMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a collapsed tree child "hidden" under "Root"
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the json output is valid JSON
|
|
And the json output contains "collapsed"
|
|
|
|
# ================================================================
|
|
# Coverage: deep tree at MAX_TREE_DEPTH rendering
|
|
# ================================================================
|
|
|
|
Scenario: Deep tree at MAX_TREE_DEPTH triggers render depth guard
|
|
Given an OutputSession with format "plain" using PlainMaterializer
|
|
When I create a tree at maximum depth
|
|
And I close the tree handle
|
|
And the session is closed
|
|
Then the deep tree output contains "more"
|
|
|
|
# ================================================================
|
|
# Coverage: JSON tree truncated at depth limit
|
|
# ================================================================
|
|
|
|
Scenario: JSON tree node serialization truncates at depth limit
|
|
When I serialize a tree node beyond MAX_TREE_DEPTH
|
|
Then the serialized node contains truncated flag
|
|
|
|
# ================================================================
|
|
# Coverage: selection.py — rich with cursor support
|
|
# ================================================================
|
|
|
|
Scenario: Rich format uses RichMaterializer when cursor supported
|
|
Given a terminal with cursor support
|
|
When I select a materializer for format "rich"
|
|
Then the selected materializer is RichMaterializer
|
|
|
|
Scenario: Rich format falls back to color when ANSI but no TTY no cursor
|
|
Given a terminal with ANSI only no TTY no cursor
|
|
When I select a materializer for format "rich"
|
|
Then the selected materializer is ColorMaterializer
|
|
|
|
# ================================================================
|
|
# Coverage: TreeHandle add_child exceeds MAX_TREE_DEPTH
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle rejects child beyond MAX_TREE_DEPTH
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree at exactly max depth via handle
|
|
Then adding one more child level raises ValueError
|
|
|
|
# ================================================================
|
|
# Coverage: detect_terminal_capabilities with cursor-capable TERM
|
|
# ================================================================
|
|
|
|
Scenario: detect_terminal_capabilities detects cursor support for xterm
|
|
When I detect capabilities with TERM "xterm-256color" on a TTY
|
|
Then the capabilities supports_cursor is true
|
|
|
|
# ================================================================
|
|
# Coverage: _find_node with mismatched root label
|
|
# ================================================================
|
|
|
|
Scenario: TreeHandle find_node returns None for mismatched root
|
|
Given an OutputSession with format "plain"
|
|
When I create a tree handle with root label "Root"
|
|
Then querying a path with wrong root returns None
|
|
|
|
# ================================================================
|
|
# LiveMaterializationStrategy
|
|
# ================================================================
|
|
|
|
Scenario: LiveMaterializationStrategy renders element on creation
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a panel handle with title "Live Panel"
|
|
And I set entry "Key" to "Val"
|
|
And I close the panel handle
|
|
And the session is closed
|
|
Then the rich output contains "Live Panel"
|
|
And the rich output contains "Key"
|
|
|
|
Scenario: LiveMaterializationStrategy supports incremental updates
|
|
Given a RichMaterializer instance
|
|
Then the rich materializer supports incremental updates
|
|
And the rich materializer strategy name is "rich"
|
|
|
|
Scenario: LiveMaterializationStrategy coalesces dirty elements
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a text handle with content "initial"
|
|
And I append text " more"
|
|
And I close the text handle
|
|
And the session is closed
|
|
Then the rich output contains "initial more"
|
|
|
|
Scenario: LiveMaterializationStrategy renders all element types
|
|
Given an OutputSession with format "rich" using RichMaterializer
|
|
When I create a tree handle with root label "Root"
|
|
And I add a tree child "child" under "Root"
|
|
And I close the tree handle
|
|
And I create a code handle with content "x = 1" and language "python"
|
|
And I close the code handle
|
|
And I create a diff handle with file_a "a.py" and file_b "b.py"
|
|
And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "new"
|
|
And I close the diff handle
|
|
And I create a separator with style "line"
|
|
And I create an action hint with commands "deploy"
|
|
And the session is closed
|
|
Then the rich output contains "Root"
|
|
And the rich output contains "x = 1"
|
|
And the rich output contains "a.py"
|
|
And the rich output contains "deploy"
|
|
|
|
Scenario: LiveMaterializationStrategy frame rate is approximately 15 fps
|
|
Given a RichMaterializer instance
|
|
Then the live strategy frame rate is 15.0
|