diff --git a/features/steps/materializers_coverage_boost_steps.py b/features/steps/materializers_coverage_boost_steps.py index 950b3b488..f602e3b75 100644 --- a/features/steps/materializers_coverage_boost_steps.py +++ b/features/steps/materializers_coverage_boost_steps.py @@ -279,6 +279,7 @@ def step_given_mock_snapshot(context: Context) -> None: context.mock_snapshot = StructuredOutput( command="test", session_id="ses-000001", + status="ok", elements=[], exit_code=0, metadata={}, diff --git a/features/steps/output_rendering_steps.py b/features/steps/output_rendering_steps.py index 3597c8dcc..ef9a5c462 100644 --- a/features/steps/output_rendering_steps.py +++ b/features/steps/output_rendering_steps.py @@ -552,7 +552,7 @@ def step_json_valid(context: Context) -> None: @then('the json output contains element type "{etype}"') def step_json_element_type(context: Context, etype: str) -> None: data = context.json_data - elements = data.get("elements", []) + elements = data.get("data", []) types = [e.get("type") for e in elements] assert etype in types, f"Expected element type {etype!r} in {types}" diff --git a/robot/helper_output_rendering.py b/robot/helper_output_rendering.py index 713c07277..58af9a082 100644 --- a/robot/helper_output_rendering.py +++ b/robot/helper_output_rendering.py @@ -60,8 +60,10 @@ def test_json_output() -> None: table.close() output = strategy.get_output() data = json.loads(output) - assert "elements" in data - assert len(data["elements"]) == 2 + assert "data" in data + assert len(data["data"]) == 2 + assert "status" in data + assert data["status"] == "ok" print("output-rendering-json-output-ok") @@ -74,7 +76,9 @@ def test_yaml_output() -> None: panel.close() output = strategy.get_output() data = yaml.safe_load(output) - assert "elements" in data + assert "data" in data + assert "status" in data + assert data["status"] == "ok" print("output-rendering-yaml-output-ok") @@ -271,7 +275,7 @@ def test_json_all_elements() -> None: session.action_hint(["cmd"]) output = strategy.get_output() data = json.loads(output) - types = [e["type"] for e in data["elements"]] + types = [e["type"] for e in data["data"]] expected = [ "panel", "table", @@ -327,7 +331,7 @@ def test_yaml_all_elements() -> None: session.action_hint(["cmd"]) output = strategy.get_output() data = yaml.safe_load(output) - types = [e["type"] for e in data["elements"]] + types = [e["type"] for e in data["data"]] expected = [ "panel", "table", diff --git a/src/cleveragents/cli/output/__init__.py b/src/cleveragents/cli/output/__init__.py index 1b2721f0f..61ad3ec74 100644 --- a/src/cleveragents/cli/output/__init__.py +++ b/src/cleveragents/cli/output/__init__.py @@ -65,11 +65,9 @@ yet needed in the current milestone. The spec does not define an element count limit. A soft limit of 10,000 is enforced as a DoS guard. -**SD-13 (JSON/YAML envelope structure)** - The spec defines ``status``/``data`` wrapper, ``messages``, and - ``timing`` fields in the serialised envelope. This implementation - serialises a flat ``StructuredOutput`` dict with ``command``, - ``session_id``, ``exit_code``, ``elements``, and ``metadata``. +**SD-13 (JSON/YAML envelope structure — IMPLEMENTED)** + The spec's envelope fields (``status``, ``data``, ``messages``, + ``timing``) are now emitted. Implemented per M4 fix (#884). **SD-14 (NO_COLOR env var — IMPLEMENTED)** ``NO_COLOR`` (https://no-color.org/) is now respected: when set, diff --git a/src/cleveragents/cli/output/materializers.py b/src/cleveragents/cli/output/materializers.py index a94b481df..ff886f6fc 100644 --- a/src/cleveragents/cli/output/materializers.py +++ b/src/cleveragents/cli/output/materializers.py @@ -275,16 +275,18 @@ def _snapshot_to_dict(snapshot: StructuredOutput) -> dict[str, Any]: P1-2 fix (Luis review): includes the ``timing`` field when present, matching the spec's JSON envelope structure (§27022). + + M4 fix: renamed ``elements`` → ``data``, ``metadata`` → ``messages``, + and added ``status`` field to match the specification envelope. """ result: dict[str, Any] = { "command": snapshot.command, - "session_id": snapshot.session_id, + "status": snapshot.status, "exit_code": snapshot.exit_code, - "elements": [_element_to_dict(e) for e in snapshot.elements], - "metadata": snapshot.metadata, + "data": [_element_to_dict(e) for e in snapshot.elements], + "timing": snapshot.timing, + "messages": snapshot.metadata, } - if snapshot.timing is not None: - result["timing"] = snapshot.timing return result diff --git a/src/cleveragents/cli/output/session.py b/src/cleveragents/cli/output/session.py index e55b61e20..4e67937ee 100644 --- a/src/cleveragents/cli/output/session.py +++ b/src/cleveragents/cli/output/session.py @@ -72,6 +72,7 @@ class StructuredOutput(BaseModel): command: str session_id: str + status: str = "ok" elements: list[ElementSnapshot] = Field(default_factory=list) exit_code: int = 0 timing: dict[str, Any] | None = None @@ -424,12 +425,17 @@ class OutputSession: P1-2 fix (Luis review): includes ``timing`` when available so that accumulate strategies (JSON/YAML) emit the timing field. + + M4 fix: derives ``status`` from ``exit_code`` — ``"ok"`` when + zero, ``"error"`` otherwise. """ with self._lock: elements = [h.element_copy() for h in self._handles.values()] + status = "error" if self._exit_code != 0 else "ok" snap = StructuredOutput( command=self.command, session_id=self.session_id, + status=status, elements=list(elements), exit_code=self._exit_code, metadata=dict(self._metadata), @@ -445,9 +451,11 @@ class OutputSession: # Already closed — return snapshot with cached timing # (R2-C2 fix: previously timing was None on double-close). elements = [h.element_copy() for h in self._handles.values()] + status = "error" if self._exit_code != 0 else "ok" snap = StructuredOutput( command=self.command, session_id=self.session_id, + status=status, elements=list(elements), exit_code=self._exit_code, metadata=dict(self._metadata),