diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d92c0d8..7fc4a2fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -800,6 +800,13 @@ uko-oo:Class` triple emission in `PythonAnalyzer._extract_class()` so that had `@tdd_expected_fail` removed and now run as permanent regression guards. Net result: 629 features active in CI (up from ~545), zero `@skip` tags remain. +- **Spec alignment for `agents project delete` output** (#7872): Updated + `docs/specification.md` to document the correct JSON/YAML output structure + for the project delete command, replacing the legacy `deletion_summary` + object with the `deleted`, `success`, and `deleted_at` fields that match + the actual implementation introduced in PR #6639. Added BDD feature file + and step definitions to validate the output format. + - **Git Worktree Sandbox Apply** (#4454): The `plan apply` command now merges LLM-generated changes via `git merge` from an isolated worktree branch instead of flat `shutil.copy2`. Displays spec-aligned Apply Summary diff --git a/docs/specification.md b/docs/specification.md index 7bc88ec91..a87242b32 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -3605,21 +3605,9 @@ Delete a project and unlink all associated resources. The resources themselves r "status": "ok", "exit_code": 0, "data": { - "deletion_summary": { - "project": "local/docs", - "resources_unlinked": 1, - "data_dir": "/repos/docs/.cleveragents" - }, - "index_cleanup": { - "text_index": "cleared", - "vectors": "240 removed", - "graph_triples": "none", - "storage_freed_mb": 12 - }, - "backups": { - "snapshot": "/backups/local-docs-2026-02-08.tgz", - "retention_days": 7 - } + "deleted": "local/docs", + "success": true, + "deleted_at": "2026-04-10T19:45:43.123456+00:00" }, "timing": { "duration_ms": 530 }, "messages": [{ "level": "ok", "text": "Project deleted" }] @@ -3633,18 +3621,9 @@ Delete a project and unlink all associated resources. The resources themselves r status: ok exit_code: 0 data: - deletion_summary: - project: local/docs - resources_unlinked: 1 - data_dir: /repos/docs/.cleveragents - index_cleanup: - text_index: cleared - vectors: 240 removed - graph_triples: none - storage_freed_mb: 12 - backups: - snapshot: /backups/local-docs-2026-02-08.tgz - retention_days: 7 + deleted: local/docs + success: true + deleted_at: '2026-04-10T19:45:43.123456+00:00' timing: duration_ms: 530 messages: diff --git a/features/project_cli_delete_spec_alignment.feature b/features/project_cli_delete_spec_alignment.feature new file mode 100644 index 000000000..5604ee665 --- /dev/null +++ b/features/project_cli_delete_spec_alignment.feature @@ -0,0 +1,58 @@ +Feature: Project CLI delete spec alignment + As a developer + I want the project delete command output to match the v3 specification + So that JSON and YAML output include the deleted, success, and deleted_at fields + + Background: + Given a project delete spec alignment CLI runner + And a project delete spec alignment mocked repository + + Scenario: Project delete JSON output includes deleted field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment output should contain "deleted" + + Scenario: Project delete JSON output includes success field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment output should contain "success" + + Scenario: Project delete JSON output includes deleted_at field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment output should contain "deleted_at" + + Scenario: Project delete JSON output does not include deletion_summary + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment output should not contain "deletion_summary" + + Scenario: Project delete YAML output includes deleted field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "yaml" + Then the project delete spec alignment output should contain "deleted" + + Scenario: Project delete YAML output includes success field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "yaml" + Then the project delete spec alignment output should contain "success" + + Scenario: Project delete YAML output includes deleted_at field + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "yaml" + Then the project delete spec alignment output should contain "deleted_at" + + Scenario: Project delete YAML output does not include deletion_summary + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "yaml" + Then the project delete spec alignment output should not contain "deletion_summary" + + Scenario: Project delete success field is true + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment JSON success field should be true + + Scenario: Project delete deleted field matches project name + Given a project delete spec alignment project "local/docs" exists + When I run project delete spec alignment for "local/docs" with format "json" + Then the project delete spec alignment JSON deleted field should be "local/docs" diff --git a/features/steps/project_cli_delete_spec_alignment_steps.py b/features/steps/project_cli_delete_spec_alignment_steps.py new file mode 100644 index 000000000..ab5f9d448 --- /dev/null +++ b/features/steps/project_cli_delete_spec_alignment_steps.py @@ -0,0 +1,131 @@ +"""Step definitions for project_cli_delete_spec_alignment.feature. + +Validates that the ``agents project delete`` command produces JSON/YAML output +with the ``deleted``, ``success``, and ``deleted_at`` fields as specified in +docs/specification.md (§agents project delete), matching the implementation +introduced in PR #6639. + +These tests exercise the ``format_output`` function directly with the data +structure that the ``delete`` command produces, ensuring the spec-aligned +output fields are present and correctly structured. +""" + +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import Any + +from behave import given, then, when # type: ignore[import-untyped] + +# --------------------------------------------------------------------------- +# Background +# --------------------------------------------------------------------------- + + +@given("a project delete spec alignment CLI runner") +def step_pdsa_runner(context: Any) -> None: + """Initialise context storage for the spec alignment tests.""" + context.pdsa_output = "" + context.pdsa_project_name = "" + + +@given("a project delete spec alignment mocked repository") +def step_pdsa_mock_repo(context: Any) -> None: + """No-op: these tests call format_output directly, no repo needed.""" + + +# --------------------------------------------------------------------------- +# Given +# --------------------------------------------------------------------------- + + +@given('a project delete spec alignment project "{name}" exists') +def step_pdsa_project_exists(context: Any, name: str) -> None: + """Record the project name that will be used in the delete output.""" + context.pdsa_project_name = name + + +# --------------------------------------------------------------------------- +# When +# --------------------------------------------------------------------------- + + +@when('I run project delete spec alignment for "{name}" with format "{fmt}"') +def step_pdsa_run_delete(context: Any, name: str, fmt: str) -> None: + """Simulate the project delete command output by calling format_output directly. + + The ``delete`` command in ``cleveragents.cli.commands.project`` calls:: + + format_output( + {"deleted": name, "success": True, "deleted_at": datetime.now(tz=UTC)}, + output_format, + ) + + We replicate that call here and capture the output written to sys.stdout. + """ + import sys + from io import StringIO + + from cleveragents.cli.formatting import format_output + + deleted_at = datetime(2026, 4, 10, 19, 45, 43, 123456, tzinfo=UTC) + data: dict[str, object] = { + "deleted": name, + "success": True, + "deleted_at": deleted_at, + } + + buf = StringIO() + real_stdout = sys.stdout + sys.stdout = buf + try: + format_output(data, fmt) + finally: + sys.stdout = real_stdout + + context.pdsa_output = buf.getvalue() + context.pdsa_format = fmt + + +# --------------------------------------------------------------------------- +# Then +# --------------------------------------------------------------------------- + + +@then('the project delete spec alignment output should contain "{text}"') +def step_pdsa_output_contains(context: Any, text: str) -> None: + """Assert the output contains the expected text.""" + output = context.pdsa_output + assert text in output, f"Expected '{text}' in output but got:\n{output}" + + +@then('the project delete spec alignment output should not contain "{text}"') +def step_pdsa_output_not_contains(context: Any, text: str) -> None: + """Assert the output does not contain the given text.""" + output = context.pdsa_output + assert text not in output, ( + f"Expected '{text}' NOT in output but it was present:\n{output}" + ) + + +@then("the project delete spec alignment JSON success field should be true") +def step_pdsa_json_success_true(context: Any) -> None: + """Parse the JSON envelope output and assert data.success is True.""" + output = context.pdsa_output.strip() + envelope = json.loads(output) + data = envelope.get("data", {}) + assert data.get("success") is True, ( + f"Expected data.success=True in JSON envelope but got: {envelope}" + ) + + +@then('the project delete spec alignment JSON deleted field should be "{name}"') +def step_pdsa_json_deleted_field(context: Any, name: str) -> None: + """Parse the JSON envelope output and assert data.deleted matches the project name.""" + output = context.pdsa_output.strip() + envelope = json.loads(output) + data = envelope.get("data", {}) + assert data.get("deleted") == name, ( + f"Expected data.deleted='{name}' in JSON envelope but got: {envelope}" + )