Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 54bf3bee69 fix(cli): fix session list --format json empty-case structure (PR #6695)
When `agents session list --format json` was called with no sessions, the
output envelope used ``{"sessions": [], "total": 0}`` instead of having a
consistent ``summary`` sub-object. This commit adds the missing ``summary``
key with typed entries (total, most_recent, oldest, total_messages, storage)
matching the non-empty schema so downstream consumers always receive a stable
payload shape.

ISSUES CLOSED: #6695
2026-05-09 00:16:55 +00:00
5 changed files with 173 additions and 3 deletions
+10
View File
@@ -14,6 +14,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
from the TDD test so both scenarios run as normal regression guards. (#988)
### Fixed
- **`agents session list --format json` empty-case structure** (#6695): When there
were no sessions, the JSON output emitted ``{"sessions": [], "total": 0}`` but
with a populated list it returned ``{"sessions": [...], "summary": {"total": …,
"most_recent": …, "oldest": …, "total_messages": …, "storage": …}}``. The empty-
case envelope has been corrected to include the ``summary`` key with all five
sub-fields (``total``, ``most_recent``, ``oldest``, ``total_messages``,
``storage``) so that downstream consumers always receive a consistent schema
regardless of whether sessions exist.
- **TUI Prompt Symbol Mode Awareness** (#6431): The prompt widget now displays a
mode-dependent symbol (`` normal, `/` command, `$` shell, `` multi-line),
implemented via `_PromptSymbolMixin` and `InputMode.MULTILINE`. The widget uses
+1
View File
@@ -38,3 +38,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* HAL 9000 has contributed the `agents session list --format json` empty-case structure fix (PR #6695): corrected the JSON envelope returned when no sessions exist so it includes a ``summary`` sub-object matching the populated-list schema (``total``, ``most_recent``, ``oldest``, ``total_messages``, ``storage``), eliminating the mismatch between empty and non-empty output structures that broke downstream consumers expecting a consistent payload shape.
@@ -0,0 +1,27 @@
@tdd_issue @pr_6695
Feature: Session list --format json empty-case envelope consistency
As an API consumer or automation script
I want the ``session list --format json`` output to have a consistent structure
regardless of whether there are any sessions
So that I can reliably parse the JSON schema (sessions + summary)
Background:
Given a session CLI runner with mocked service and no existing sessions
Scenario: Empty list returns valid JSON envelope with summary
When I run session CLI list with --format json
Then the session CLI output should be valid JSON
And the session CLI JSON should contain "sessions"
And the session CLI JSON should contain "summary"
Scenario: Empty list JSON summary has all required sub-fields
When I run session CLI list with --format json
Then the JSON summary total should exist and equal zero
And the JSON summary most_recent should be null
And the JSON summary oldest should be null
And the JSON summary total_messages should exist and equal zero
And the JSON summary storage should be "0 KB"
Scenario: Empty list structure matches non-empty list schema keys
When I run session CLI list with --format json
Then the top-level JSON envelope should have keys "command", "status", "exit_code", "data", "timing", "messages"
@@ -0,0 +1,117 @@
"""Step definitions for session_list_empty_json_structure.feature (PR #6695)."""
from __future__ import annotations
import json
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.commands.session import app as session_app
def _assert_valid_envelope(parsed: dict) -> dict:
"""Verify the output is a valid spec envelope and return the data field."""
expected_keys = {"command", "status", "exit_code", "data", "timing", "messages"}
assert expected_keys.issubset(parsed.keys()), (
f"Missing envelope keys. Expected {sorted(expected_keys)}, "
f"got {sorted(parsed.keys())}"
)
return parsed["data"]
RUNNER = CliRunner()
@given("a session CLI runner with mocked service and no existing sessions")
def step_session_env_setup(context: Context) -> None:
from cleveragents.cli.commands import session as session_mod
from unittest.mock import MagicMock
context.mock_service = MagicMock()
context.mock_service.list.return_value = []
session_mod._service = context.mock_service
@when("I run session CLI list with --format json")
def step_run_list_json(context: Context) -> None:
result = RUNNER.invoke(session_app, ["list", "--format", "json"])
assert result.exit_code == 0, (
f"Command failed with exit {result.exit_code}: {result.output}"
)
context.session_list_json_output = result.output.strip()
# --- Envelope / structure assertions ---
@then('the top-level JSON envelope should have keys "command", "status", "exit_code", "data", "timing", "messages"')
def step_verify_envelope_keys(context: Context) -> None:
parsed = json.loads(context.session_list_json_output)
expected_keys = {"command", "status", "exit_code", "data", "timing", "messages"}
assert expected_keys.issubset(parsed.keys()), (
f"Missing envelope keys. Expected {sorted(expected_keys)}, "
f"got {sorted(parsed.keys())}\nOutput: {context.session_list_json_output}"
)
# --- Summary sub-field assertions ---
@then("the JSON summary total should exist and equal zero")
def step_summary_total_zero(context: Context) -> None:
data = _assert_valid_envelope(json.loads(context.session_list_json_output))
summary = data["summary"]
assert "total" in summary, f"'total' missing from summary: {context.session_list_json_output}"
assert summary["total"] == 0, (
f"Expected total=0, got {summary['total']}\nOutput: {context.session_list_json_output}"
)
@then("the JSON summary most_recent should be null")
def step_summary_most_recent_null(context: Context) -> None:
data = _assert_valid_envelope(json.loads(context.session_list_json_output))
summary = data["summary"]
assert "most_recent" in summary, (
f"'most_recent' missing from summary: {context.session_list_json_output}"
)
assert summary["most_recent"] is None, (
f"Expected most_recent=null, got {summary['most_recent']}\nOutput: {context.session_list_json_output}"
)
@then("the JSON summary oldest should be null")
def step_summary_oldest_null(context: Context) -> None:
data = _assert_valid_envelope(json.loads(context.session_list_json_output))
summary = data["summary"]
assert "oldest" in summary, (
f"'oldest' missing from summary: {context.session_list_json_output}"
)
assert summary["oldest"] is None, (
f"Expected oldest=null, got {summary['oldest']}\nOutput: {context.session_list_json_output}"
)
@then("the JSON summary total_messages should exist and equal zero")
def step_summary_total_messages_zero(context: Context) -> None:
data = _assert_valid_envelope(json.loads(context.session_list_json_output))
summary = data["summary"]
assert "total_messages" in summary, (
f"'total_messages' missing from summary: {context.session_list_json_output}"
)
assert summary["total_messages"] == 0, (
f"Expected total_messages=0, got {summary['total_messages']}\nOutput: {context.session_list_json_output}"
)
@then('the JSON summary storage should be "0 KB"')
def step_summary_storage_0kb(context: Context) -> None:
data = _assert_valid_envelope(json.loads(context.session_list_json_output))
summary = data["summary"]
assert "storage" in summary, (
f"'storage' missing from summary: {context.session_list_json_output}"
)
assert summary["storage"] == "0 KB", (
f"Expected storage='0 KB', got {summary['storage']}\nOutput: {context.session_list_json_output}"
)
+18 -3
View File
@@ -326,10 +326,25 @@ def list_sessions(
mcp_logger.setLevel(orig_level)
if not sessions:
# For machine-readable formats, always emit a structured empty list
# so that callers parsing JSON/YAML receive valid output.
# For machine-readable formats, always emit a structured empty-list
# response whose shape mirrors the non-empty path so downstream
# consumers receive a consistent envelope. The envelope must contain
# the same ``summary`` top-level key with typed entries for every
# field that the populated path emits (total / most_recent / oldest /
# total_messages / storage) rather than ad-hoc keys such as
# ``{"sessions": [], "total": 0}`` which previously broke callers
# expecting a stable schema.
empty_summary = {
"total": 0,
"most_recent": None,
"oldest": None,
"total_messages": 0,
"storage": "0 KB",
}
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
typer.echo(format_output({"sessions": [], "total": 0}, fmt))
typer.echo(
format_output({"sessions": [], "summary": empty_summary}, fmt)
)
return
console.print("[yellow]No sessions found.[/yellow]")
console.print("Create one with 'agents session create'")