fix(cli): fix JSON/YAML envelope messages[].text for delete/export/import
Extend the JSON/YAML envelope messages[].text fix to cover the remaining
three session subcommands that were still producing plain Rich output
instead of structured envelopes for non-rich format paths:
- session delete: route non-rich formats through format_output() with
messages=[{"level": "ok", "text": "Session deleted"}]
- session export: add --output-format/-f option; emit structured envelope
with session_export/contents/integrity data and "Export completed" message
- session import: add --format/-f option; emit structured envelope with
session_import/validation/merge data and "Import completed" message
Also add BDD scenarios to features/session_cli.feature for each new
envelope path, add corresponding step definitions, add CHANGELOG entry
under [Unreleased] Fixed, and assign milestone v3.2.0 to the PR.
ISSUES CLOSED: #6457
This commit is contained in:
@@ -1002,6 +1002,14 @@ iteration` and data corruption under concurrent plan execution. All public
|
||||
replaces the O(n) linear `status` lookup in the `as_completed()` loop with an O(1)
|
||||
`status_map` dict pre-computed before the executor block.
|
||||
|
||||
- **JSON/YAML envelope `messages[].text` content** (#6457): `agents session create`,
|
||||
`list`, `show`, `delete`, `export`, and `import` commands now populate the
|
||||
`messages[].text` field with human-readable text (`"Session created"`,
|
||||
`"N sessions listed"`, `"Session details loaded"`, `"Session deleted"`,
|
||||
`"Export completed"`, `"Import completed"`) instead of the generic `"ok"` fallback.
|
||||
The `export` command gains `--output-format` and the `import` command gains `--format`
|
||||
to select the output envelope format independently of the export/import file format.
|
||||
|
||||
- **Robot Framework TDD Listener Guards** (#5436): Added three guard conditions to the
|
||||
`tdd_expected_fail_listener` `end_test()` function to prevent blindly inverting ALL test
|
||||
failures to passes, which was masking infrastructure errors and causing flaky CI behavior.
|
||||
|
||||
@@ -112,6 +112,13 @@ Feature: Session CLI commands
|
||||
Then the session CLI delete should succeed
|
||||
And the session CLI output should contain "deleted"
|
||||
|
||||
Scenario: Delete session with JSON format emits envelope
|
||||
Given there is a mocked session to delete
|
||||
When I run session CLI delete with --yes and --format json
|
||||
Then the session CLI delete should succeed
|
||||
And the session CLI output should be valid JSON
|
||||
And the session CLI JSON envelope message should be "Session deleted"
|
||||
|
||||
Scenario: Delete non-existent session
|
||||
When I run session CLI delete with a non-existent ID
|
||||
Then the session CLI should exit with error
|
||||
@@ -163,6 +170,13 @@ Feature: Session CLI commands
|
||||
Then the session CLI export should succeed
|
||||
And the session CLI output should contain "Export completed"
|
||||
|
||||
Scenario: Export session with JSON output format emits envelope
|
||||
Given there is a mocked session for export
|
||||
When I run session CLI export with --output-format json
|
||||
Then the session CLI export should succeed
|
||||
And the session CLI output should be valid JSON
|
||||
And the session CLI JSON envelope message should be "Export completed"
|
||||
|
||||
Scenario: Export non-existent session
|
||||
When I run session CLI export with a non-existent session ID
|
||||
Then the session CLI should exit with error
|
||||
@@ -202,6 +216,13 @@ Feature: Session CLI commands
|
||||
Then the session CLI should exit with error
|
||||
And the session CLI output should contain "Import error"
|
||||
|
||||
Scenario: Import session with JSON format emits envelope
|
||||
Given there is a valid session export file
|
||||
When I run session CLI import with the export file and --format json
|
||||
Then the session CLI import should succeed
|
||||
And the session CLI output should be valid JSON
|
||||
And the session CLI JSON envelope message should be "Import completed"
|
||||
|
||||
Scenario: Import from invalid JSON file
|
||||
Given there is an invalid JSON file
|
||||
When I run session CLI import with the invalid JSON file
|
||||
|
||||
@@ -421,6 +421,13 @@ def step_delete_yes(context: Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
@when("I run session CLI delete with --yes and --format json")
|
||||
def step_delete_yes_json(context: Context) -> None:
|
||||
context.result = context.runner.invoke(
|
||||
session_app, ["delete", context.session_id, "--yes", "--format", "json"]
|
||||
)
|
||||
|
||||
|
||||
@when("I run session CLI delete with a non-existent ID")
|
||||
def step_delete_nonexistent(context: Context) -> None:
|
||||
context.mock_service.get.side_effect = SessionNotFoundError("Session not found")
|
||||
@@ -463,6 +470,13 @@ def step_export_stdout(context: Context) -> None:
|
||||
context.result = context.runner.invoke(session_app, ["export", context.session_id])
|
||||
|
||||
|
||||
@when("I run session CLI export with --output-format json")
|
||||
def step_export_output_format_json(context: Context) -> None:
|
||||
context.result = context.runner.invoke(
|
||||
session_app, ["export", context.session_id, "--output-format", "json"]
|
||||
)
|
||||
|
||||
|
||||
@when("I run session CLI export with --output to a temp file")
|
||||
def step_export_to_file(context: Context) -> None:
|
||||
fd, path = tempfile.mkstemp(suffix=".json")
|
||||
@@ -566,6 +580,13 @@ def step_import_valid(context: Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
@when("I run session CLI import with the export file and --format json")
|
||||
def step_import_valid_json(context: Context) -> None:
|
||||
context.result = context.runner.invoke(
|
||||
session_app, ["import", "--input", context.import_path, "--format", "json"]
|
||||
)
|
||||
|
||||
|
||||
@when("I run session CLI import with a non-existent file")
|
||||
def step_import_nonexistent(context: Context) -> None:
|
||||
context.result = context.runner.invoke(
|
||||
|
||||
@@ -747,8 +747,18 @@ def delete(
|
||||
console.print()
|
||||
console.print("[green]✓ OK[/green] Session deleted")
|
||||
else:
|
||||
# Non-rich formats: simple message
|
||||
console.print(f"[green]✓ OK[/green] Session {session_id} deleted")
|
||||
# Non-rich formats: emit a structured JSON/YAML envelope per spec
|
||||
# (docs/specification.md §"agents session delete" line ~1959)
|
||||
typer.echo(
|
||||
format_output(
|
||||
{"session_id": session_id},
|
||||
fmt.value,
|
||||
command=_command_label(
|
||||
"delete", session_id, "--yes", "--format", fmt.value
|
||||
),
|
||||
messages=[{"level": "ok", "text": "Session deleted"}],
|
||||
)
|
||||
)
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
@@ -783,6 +793,14 @@ def export_session(
|
||||
help="Export format: json (default) or md (Markdown transcript)",
|
||||
),
|
||||
] = "json",
|
||||
output_fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--output-format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Export a session as JSON or Markdown.
|
||||
|
||||
@@ -842,17 +860,75 @@ def export_session(
|
||||
# Create parent directories if needed
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(content, encoding="utf-8")
|
||||
else:
|
||||
elif output_fmt in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
# Only echo raw content to stdout when using rich output format;
|
||||
# for machine-readable formats the envelope wraps the metadata instead.
|
||||
typer.echo(content)
|
||||
|
||||
# Render Rich panels for both file and stdout export paths
|
||||
_render_export_panels(
|
||||
session_id=session_id,
|
||||
output=output,
|
||||
content=content,
|
||||
export_data=json_data,
|
||||
fmt=fmt,
|
||||
)
|
||||
if output_fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
# Non-rich output formats: emit a structured JSON/YAML envelope per spec
|
||||
# (docs/specification.md §"agents session export" line ~2085)
|
||||
size_bytes = len(content.encode("utf-8"))
|
||||
if size_bytes < 1024:
|
||||
size_str = f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
size_str = f"{size_bytes // 1024} KB"
|
||||
else:
|
||||
size_str = f"{size_bytes // (1024 * 1024)} MB"
|
||||
output_display = str(output) if output is not None else "(stdout)"
|
||||
format_display = "JSON" if fmt == "json" else "Markdown"
|
||||
message_count = len(json_data.get("messages", []))
|
||||
plan_refs = len(json_data.get("linked_plan_ids", []))
|
||||
metadata_keys = len(json_data.get("metadata", {}))
|
||||
actor_config = "included" if json_data.get("actor_name") else "none"
|
||||
schema_version = json_data.get("schema_version", "v1")
|
||||
checksum_raw = json_data.get("checksum", "")
|
||||
checksum_display = (
|
||||
f"sha256:{checksum_raw[:4]}...{checksum_raw[-4:]}"
|
||||
if len(checksum_raw) >= 8
|
||||
else checksum_raw or "n/a"
|
||||
)
|
||||
envelope_data = {
|
||||
"session_export": {
|
||||
"session": session_id,
|
||||
"output": output_display,
|
||||
"messages": message_count,
|
||||
"size": size_str,
|
||||
"format": format_display,
|
||||
},
|
||||
"contents": {
|
||||
"messages": message_count,
|
||||
"plan_references": plan_refs,
|
||||
"metadata_keys": metadata_keys,
|
||||
"actor_config": actor_config,
|
||||
"schema_version": schema_version,
|
||||
},
|
||||
"integrity": {
|
||||
"checksum": checksum_display,
|
||||
"encrypted": False,
|
||||
},
|
||||
}
|
||||
extra_tokens: list[str] = []
|
||||
if output is not None:
|
||||
extra_tokens.extend(["--output", str(output)])
|
||||
extra_tokens.extend(["--output-format", output_fmt])
|
||||
typer.echo(
|
||||
format_output(
|
||||
envelope_data,
|
||||
output_fmt,
|
||||
command=_command_label("export", session_id, *extra_tokens),
|
||||
messages=[{"level": "ok", "text": "Export completed"}],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Render Rich panels for both file and stdout export paths
|
||||
_render_export_panels(
|
||||
session_id=session_id,
|
||||
output=output,
|
||||
content=content,
|
||||
export_data=json_data,
|
||||
fmt=fmt,
|
||||
)
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
@@ -952,6 +1028,10 @@ def import_session(
|
||||
Path,
|
||||
typer.Option("--input", "-i", help="Input JSON file path"),
|
||||
],
|
||||
output_fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Import a session from a JSON file.
|
||||
|
||||
@@ -978,29 +1058,63 @@ def import_session(
|
||||
actor_name = data.get("actor_name")
|
||||
session = service.import_session(data)
|
||||
|
||||
# Session Import panel
|
||||
session_details = (
|
||||
f"[bold]Input:[/bold] {input_file}\n"
|
||||
f"[bold]Session ID:[/bold] {session.session_id}\n"
|
||||
f"[bold]Messages:[/bold] {session.message_count}\n"
|
||||
f"[bold]Schema:[/bold] {schema_version}"
|
||||
)
|
||||
console.print(Panel(session_details, title="Session Import", expand=False))
|
||||
if output_fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
# Non-rich output formats: emit a structured JSON/YAML envelope per spec
|
||||
# (docs/specification.md §"agents session import" line ~2205)
|
||||
actor_ref_status = "resolved" if actor_name else "none"
|
||||
envelope_data = {
|
||||
"session_import": {
|
||||
"input": str(input_file),
|
||||
"session_id": session.session_id,
|
||||
"messages": session.message_count,
|
||||
"schema": schema_version,
|
||||
},
|
||||
"validation": {
|
||||
"checksum": "verified",
|
||||
"schema": "compatible",
|
||||
"actor_ref": actor_ref_status,
|
||||
},
|
||||
"merge": {
|
||||
"existing": "none",
|
||||
"strategy": "create new",
|
||||
},
|
||||
}
|
||||
typer.echo(
|
||||
format_output(
|
||||
envelope_data,
|
||||
output_fmt,
|
||||
command=_command_label(
|
||||
"import", "--input", str(input_file), "--format", output_fmt
|
||||
),
|
||||
messages=[{"level": "ok", "text": "Import completed"}],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Session Import panel
|
||||
session_details = (
|
||||
f"[bold]Input:[/bold] {input_file}\n"
|
||||
f"[bold]Session ID:[/bold] {session.session_id}\n"
|
||||
f"[bold]Messages:[/bold] {session.message_count}\n"
|
||||
f"[bold]Schema:[/bold] {schema_version}"
|
||||
)
|
||||
console.print(Panel(session_details, title="Session Import", expand=False))
|
||||
|
||||
# Validation panel
|
||||
actor_ref_status = "resolved" if actor_name else "none"
|
||||
validation_details = (
|
||||
f"[bold]Checksum:[/bold] verified\n"
|
||||
f"[bold]Schema:[/bold] compatible\n"
|
||||
f"[bold]Actor Ref:[/bold] {actor_ref_status}"
|
||||
)
|
||||
console.print(Panel(validation_details, title="Validation", expand=False))
|
||||
# Validation panel
|
||||
actor_ref_status = "resolved" if actor_name else "none"
|
||||
validation_details = (
|
||||
f"[bold]Checksum:[/bold] verified\n"
|
||||
f"[bold]Schema:[/bold] compatible\n"
|
||||
f"[bold]Actor Ref:[/bold] {actor_ref_status}"
|
||||
)
|
||||
console.print(Panel(validation_details, title="Validation", expand=False))
|
||||
|
||||
# Merge panel
|
||||
merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new"
|
||||
console.print(Panel(merge_details, title="Merge", expand=False))
|
||||
# Merge panel
|
||||
merge_details = (
|
||||
"[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new"
|
||||
)
|
||||
console.print(Panel(merge_details, title="Merge", expand=False))
|
||||
|
||||
console.print("[green]✓ OK[/green] Import completed")
|
||||
console.print("[green]✓ OK[/green] Import completed")
|
||||
|
||||
except SessionImportError as exc:
|
||||
console.print(f"[red]Import error:[/red] {exc}")
|
||||
|
||||
Reference in New Issue
Block a user