Render list-valued blocked_by consistently in state --compact

`smriti current` already normalizes a list-valued blocked_by to a clean
comma-separated string via the backend CurrentTask validator, but `smriti
state` rendered it through an f-string and leaked the raw Python list repr
(['a', 'b']) into the state brief — the surface agents read at session start.

Add a `_coerce_blocked_by` helper in the CLI formatters mirroring that
validator, and apply it in `_normalize_task_item` so every `smriti state`
task line renders blocked_by the same way `smriti current` does. Adds
regression tests.
This commit is contained in:
Himanshu Dongre 2026-05-18 00:02:07 +05:30
parent c43310caa1
commit eb60d03f4b
2 changed files with 39 additions and 0 deletions

View file

@ -120,6 +120,25 @@ def _list_section(heading: str, items: list[str]) -> str:
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"
def _coerce_blocked_by(value: object) -> str | None:
"""Normalize blocked_by — string, list of dependency labels, or null —
to a single display string.
Real task payloads carry blocked_by as a plain string or as a list (a
task blocked by several others). Mirrors the backend CurrentTask
validator so `smriti state` and `smriti current` render it identically
instead of leaking a raw Python list repr into the state brief.
"""
if value is None:
return None
if isinstance(value, str):
return value.strip() or None
if isinstance(value, (list, tuple)):
labels = [str(item).strip() for item in value if str(item).strip()]
return ", ".join(labels) or None
return str(value).strip() or None
def _normalize_task_item(item) -> dict: def _normalize_task_item(item) -> dict:
"""Normalize a task to a dict with at least a 'text' key. """Normalize a task to a dict with at least a 'text' key.
@ -131,6 +150,8 @@ def _normalize_task_item(item) -> dict:
task = dict(item) task = dict(item)
if not task.get("intent_hint") and task.get("intent_type"): if not task.get("intent_hint") and task.get("intent_type"):
task["intent_hint"] = task["intent_type"] task["intent_hint"] = task["intent_type"]
if "blocked_by" in task:
task["blocked_by"] = _coerce_blocked_by(task["blocked_by"])
return task return task
# Fallback: coerce to string # Fallback: coerce to string
return {"text": str(item)} return {"text": str(item)}

View file

@ -504,6 +504,13 @@ def test_normalize_task_item_dict():
assert result == task assert result == task
def test_normalize_task_item_coerces_list_blocked_by():
"""A list-valued blocked_by is normalized to a comma-separated string."""
task = {"text": "Wire the limiter", "blocked_by": ["middleware", "load-test"]}
result = _normalize_task_item(task)
assert result["blocked_by"] == "middleware, load-test"
def test_task_section_empty(): def test_task_section_empty():
"""Empty task list produces empty string.""" """Empty task list produces empty string."""
assert _task_section([]) == "" assert _task_section([]) == ""
@ -547,6 +554,17 @@ def test_task_section_structured_with_blocked_by():
assert "→ blocked by: freshness-impl" in out assert "→ blocked by: freshness-impl" in out
def test_task_section_renders_list_valued_blocked_by():
"""A list-valued blocked_by renders as a clean comma-separated string,
not a raw Python list repr consistent with `smriti current`."""
tasks = [
{"text": "Wire the limiter", "blocked_by": ["middleware", "load-test"]},
]
out = _task_section(tasks)
assert "→ blocked by: middleware, load-test" in out
assert "['" not in out # not the raw Python list repr
def test_task_section_structured_with_done_status(): def test_task_section_structured_with_done_status():
"""Task with status=done renders inline marker.""" """Task with status=done renders inline marker."""
tasks = [ tasks = [