forked from cleveragents/cleveragents-core
a5361a93ae
Implement the TuiMaterializer — the glue layer between the Output Rendering Framework and the Textual widget tree. It maps ElementHandle events from A2A task updates to Textual widgets. Key design: - TuiMaterializer implements the MaterializationStrategy protocol - Widget registry maps element kinds to Textual widget factories: panel → RichLog, table → DataTable, progress/status → Static - Per-session isolation: each tab gets its own materializer instance with independent widget map - Streaming via on_element_updated pushes incremental deltas directly into mounted widgets - Same widget tree serves TUI, Web (Textual Web), and IDE plugin - Callbacks (on_widget_created, on_widget_removed) let the host screen mount/unmount widgets into the DOM - Thread-safe via internal lock on widget/snapshot maps New files: - src/cleveragents/tui/materializer.py — TuiMaterializer class - features/tui_materializer.feature — 20 BDD scenarios - features/steps/tui_materializer_steps.py — Behave step definitions - robot/tui_materializer.robot — 12 Robot Framework integration tests - robot/helper_tui_materializer.py — Robot helper script Quality checks: typecheck (0 errors), lint (clean), format (clean), unit_tests (20/20 pass), integration_tests (12/12 pass) ISSUES CLOSED: #696
133 lines
5.9 KiB
Gherkin
133 lines
5.9 KiB
Gherkin
Feature: TuiMaterializer A2A integration layer
|
|
As a developer
|
|
I want a TuiMaterializer that maps ElementHandle events to Textual widgets
|
|
So that CLI command producers render in the TUI without modification
|
|
|
|
# ---------- Instantiation ----------
|
|
|
|
Scenario: TuiMaterializer can be instantiated with defaults
|
|
Given a TuiMaterializer with session id "s1"
|
|
Then the materializer session id should be "s1"
|
|
And the materializer strategy name should be "tui"
|
|
And the materializer should not be closed
|
|
And the materializer widget count should be 0
|
|
|
|
Scenario: TuiMaterializer rejects non-string session id
|
|
When a TuiMaterializer is created with a non-string session id
|
|
Then a TypeError should be raised for materializer session id
|
|
|
|
# ---------- Widget registry ----------
|
|
|
|
Scenario: Default registry maps all element kinds
|
|
Given a TuiMaterializer with session id "s1"
|
|
Then the materializer registry should contain kind "panel"
|
|
And the materializer registry should contain kind "table"
|
|
And the materializer registry should contain kind "progress"
|
|
And the materializer registry should contain kind "status"
|
|
|
|
Scenario: Custom factory can be registered
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a custom widget factory is registered for kind "code"
|
|
Then the materializer registry should contain kind "code"
|
|
|
|
Scenario: Registering empty kind raises ValueError
|
|
Given a TuiMaterializer with session id "s1"
|
|
When an empty element kind is registered on the materializer
|
|
Then a ValueError should be raised for materializer empty kind
|
|
|
|
Scenario: Registering non-callable factory raises TypeError
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a non-callable factory is registered on the materializer
|
|
Then a TypeError should be raised for materializer non-callable
|
|
|
|
# ---------- Element creation ----------
|
|
|
|
Scenario: Panel element creates a RichLog widget
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a panel element is created on the materializer with title "Details"
|
|
Then a materializer widget should exist for the last handle
|
|
And the materializer widget should be a RichLog
|
|
|
|
Scenario: Table element creates a DataTable widget
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a table element is created on the materializer with columns "Name" and "Value"
|
|
Then a materializer widget should exist for the last handle
|
|
And the materializer widget should be a DataTable
|
|
|
|
Scenario: Progress element creates a Static widget
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a progress element is created on the materializer with label "Loading"
|
|
Then a materializer widget should exist for the last handle
|
|
And the materializer widget should be a Static widget
|
|
|
|
Scenario: Status element creates a Static widget
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a status element is created on the materializer with message "OK"
|
|
Then a materializer widget should exist for the last handle
|
|
And the materializer widget should be a Static widget
|
|
|
|
Scenario: Unknown element kind is logged and skipped
|
|
Given a TuiMaterializer with session id "s1"
|
|
When an element with unknown kind "sparkle" is created on the materializer
|
|
Then the materializer widget count should be 0
|
|
|
|
Scenario: Element without initial state is skipped
|
|
Given a TuiMaterializer with session id "s1"
|
|
When an element is created on the materializer without initial state
|
|
Then the materializer widget count should be 0
|
|
|
|
# ---------- Widget callbacks ----------
|
|
|
|
Scenario: on_widget_created callback fires on element creation
|
|
Given a TuiMaterializer with a widget-created callback
|
|
When a panel element is created on the materializer with title "CB Test"
|
|
Then the materializer widget-created callback should have been called
|
|
|
|
Scenario: on_widget_removed callback fires on element close
|
|
Given a TuiMaterializer with a widget-removed callback
|
|
When a status element is created on the materializer and then closed
|
|
Then the materializer widget-removed callback should have been called
|
|
|
|
# ---------- Updates ----------
|
|
|
|
Scenario: Panel entry update refreshes widget snapshot
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a panel element is created on the materializer with title "Info"
|
|
And the materializer panel snapshot is updated with entry "key1" "val1"
|
|
Then the materializer stored snapshot should have entry "key1"
|
|
|
|
Scenario: Status update changes widget text via snapshot
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a status element is created on the materializer with message "Starting"
|
|
And the materializer status snapshot is updated to "Done"
|
|
Then the materializer stored status snapshot message should be "Done"
|
|
|
|
# ---------- Session lifecycle ----------
|
|
|
|
Scenario: Session end marks materializer closed
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a materializer session end event is dispatched
|
|
Then the materializer should be closed
|
|
|
|
Scenario: Close releases all widgets
|
|
Given a TuiMaterializer with session id "s1"
|
|
When a panel element is created on the materializer with title "Temp"
|
|
And the materializer is explicitly closed
|
|
Then the materializer widget count should be 0
|
|
And the materializer should be closed
|
|
|
|
# ---------- Multi-session isolation ----------
|
|
|
|
Scenario: Two materializers have independent widget maps
|
|
Given two TuiMaterializer instances "a" and "b"
|
|
When a panel is created on materializer instance "a"
|
|
Then materializer instance "a" should have 1 widget
|
|
And materializer instance "b" should have 0 widgets
|
|
|
|
# ---------- Integration with OutputSession ----------
|
|
|
|
Scenario: TuiMaterializer works as OutputSession strategy
|
|
Given an OutputSession using a TuiMaterializer strategy
|
|
When a panel is created via the output session with title "Session Panel"
|
|
Then the materializer strategy should have 1 widget
|