diff --git a/planfile/core/store_tickets.py b/planfile/core/store_tickets.py index 631198c..80e961b 100644 --- a/planfile/core/store_tickets.py +++ b/planfile/core/store_tickets.py @@ -21,6 +21,15 @@ def _ticket_from_data(self, t_data: dict[str, Any]) -> Ticket | None: # legacy YAML compat: British 'cancelled' → American 'canceled' if t_data.get('status') == 'cancelled': t_data['status'] = 'canceled' + # Early queue writers used ``completed`` for both the ticket and + # execution terminal. Project that historical alias as the + # canonical ``done`` state without rewriting the authoritative + # YAML or its audit history merely because it was read. + if t_data.get('status') == 'completed': + t_data['status'] = 'done' + execution = t_data.get('execution') + if isinstance(execution, dict) and execution.get('state') == 'completed': + t_data['execution'] = {**execution, 'state': 'done'} if 'integration' in t_data and isinstance(t_data['integration'], str): t_data['labels'] = [t_data.pop('integration')] if hasattr(self, '_project_ticket_evidence'): diff --git a/tests/test_ticket_execution.py b/tests/test_ticket_execution.py index e021682..09fda4d 100644 --- a/tests/test_ticket_execution.py +++ b/tests/test_ticket_execution.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +import yaml from planfile import ( Planfile, @@ -15,6 +16,30 @@ from planfile.core.store import ImmutableTerminalReopenError +def test_legacy_completed_terminal_is_projected_as_done_without_rewriting_yaml(tmp_path): + pf = Planfile(str(tmp_path)) + ticket = pf.create_ticket( + name="Historical completed terminal", + execution=TicketExecution(state="running"), + ) + sprint_path = tmp_path / ".planfile" / "sprints" / "current.yaml" + stored = yaml.safe_load(sprint_path.read_text(encoding="utf-8")) + raw_ticket = stored["sprint"]["tickets"][ticket.id] + raw_ticket["status"] = "completed" + raw_ticket["execution"]["state"] = "completed" + sprint_path.write_text(yaml.safe_dump(stored, sort_keys=False), encoding="utf-8") + + loaded = pf.get_ticket(ticket.id) + + assert loaded is not None + assert loaded.status == "done" + assert loaded.execution is not None + assert loaded.execution.state == "done" + authoritative = yaml.safe_load(sprint_path.read_text(encoding="utf-8")) + assert authoritative["sprint"]["tickets"][ticket.id]["status"] == "completed" + assert authoritative["sprint"]["tickets"][ticket.id]["execution"]["state"] == "completed" + + def test_ticket_round_trip_with_execution_fields(tmp_path): pf = Planfile(str(tmp_path)) ticket = pf.create_ticket(