fix(cli): align output envelope with specification structure #1187
@@ -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={},
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user