diff --git a/ddogctl/commands/incident.py b/ddogctl/commands/incident.py index afbe9c7..4be3b47 100644 --- a/ddogctl/commands/incident.py +++ b/ddogctl/commands/incident.py @@ -43,7 +43,7 @@ def list_incidents(format): "id": inc.id, "title": getattr(attrs, "title", ""), "severity": getattr(attrs, "severity", ""), - "status": getattr(attrs, "status", ""), + "status": getattr(attrs, "state", ""), "created": str(getattr(attrs, "created", "")), "modified": str(getattr(attrs, "modified", "")), } @@ -59,7 +59,7 @@ def list_incidents(format): for inc in incidents: attrs = inc.attributes - status_str = str(getattr(attrs, "status", "")) + status_str = str(getattr(attrs, "state", "")) severity_str = str(getattr(attrs, "severity", "")) status_color = { "active": "red", @@ -100,7 +100,7 @@ def get_incident(incident_id, format): "id": inc.id, "title": getattr(attrs, "title", ""), "severity": getattr(attrs, "severity", ""), - "status": getattr(attrs, "status", ""), + "status": getattr(attrs, "state", ""), "created": str(getattr(attrs, "created", "")), "modified": str(getattr(attrs, "modified", "")), } @@ -112,7 +112,7 @@ def get_incident(incident_id, format): severity_str = str(getattr(attrs, "severity", "")) console.print(f"[bold]Severity:[/bold] [yellow]{severity_str}[/yellow]") - status_str = str(getattr(attrs, "status", "")) + status_str = str(getattr(attrs, "state", "")) status_color = { "active": "red", "stable": "yellow", @@ -166,7 +166,7 @@ def create_incident(title, severity, format): "id": inc.id, "title": getattr(attrs, "title", ""), "severity": severity, - "status": getattr(attrs, "status", ""), + "status": getattr(attrs, "state", ""), } print(json.dumps(output, indent=2)) else: @@ -200,12 +200,12 @@ def update_incident(incident_id, title, status, severity, format): attrs_kwargs = {} if title is not None: attrs_kwargs["title"] = title - if status is not None: - attrs_kwargs["status"] = status fields = {} if severity is not None: fields["severity"] = {"type": "dropdown", "value": severity} + if status is not None: + fields["state"] = {"type": "dropdown", "value": status} if fields: attrs_kwargs["fields"] = fields @@ -227,7 +227,7 @@ def update_incident(incident_id, title, status, severity, format): output = { "id": inc.id, "title": getattr(attrs, "title", ""), - "status": getattr(attrs, "status", ""), + "status": getattr(attrs, "state", ""), } print(json.dumps(output, indent=2)) else: diff --git a/tests/commands/test_incident.py b/tests/commands/test_incident.py index bcc2f4d..4c7390a 100644 --- a/tests/commands/test_incident.py +++ b/tests/commands/test_incident.py @@ -8,14 +8,20 @@ def _make_incident( id, title, severity, status, created="2026-01-15T10:00:00Z", modified="2026-01-15T12:00:00Z" ): - """Create a mock incident object.""" + """Create a mock incident object. + + The Datadog v2 SDK exposes the incident lifecycle as ``state`` on + ``IncidentResponseAttributes`` — not ``status`` — so the mock uses + ``spec_set`` to reject the old attribute name and keep tests honest. + """ inc = Mock() inc.id = id inc.type = "incidents" inc.attributes = Mock( + spec_set=["title", "severity", "state", "created", "modified", "fields"], title=title, severity=severity, - status=status, + state=status, created=created, modified=modified, fields={}, @@ -194,6 +200,47 @@ def test_update_incident_no_fields(self, runner): assert result.exit_code != 0 assert "No update fields" in result.output + def test_update_incident_status_sent_as_fields_state(self, mock_client, runner): + """--status must be sent via fields["state"], not as a top-level attribute. + + Regression test: the Datadog v2 SDK's ``IncidentUpdateAttributes`` has + no ``status`` (or ``state``) attribute — state changes flow through + the ``fields`` dict. Putting ``status`` on the attributes object + silently no-ops the lifecycle update. + """ + inc = _make_incident("inc-1", "Service outage", "SEV-1", "resolved") + response = Mock(data=inc) + mock_client.incidents.update_incident.return_value = response + + with patch("ddogctl.commands.incident.get_datadog_client", return_value=mock_client): + result = runner.invoke(incident, ["update", "inc-1", "--status", "resolved"]) + + assert result.exit_code == 0, f"Command failed: {result.output}" + mock_client.incidents.update_incident.assert_called_once() + body = mock_client.incidents.update_incident.call_args.kwargs["body"] + attrs = body.data.attributes + state_field = attrs.fields["state"] + assert str(state_field["type"]) == "dropdown" + assert str(state_field["value"]) == "resolved" + assert not hasattr(attrs, "status") + assert not hasattr(attrs, "state") + + def test_update_incident_json_reads_state(self, mock_client, runner): + """JSON output's ``status`` key must reflect the SDK's ``state`` attribute.""" + inc = _make_incident("inc-1", "Service outage", "SEV-1", "resolved") + response = Mock(data=inc) + mock_client.incidents.update_incident.return_value = response + + with patch("ddogctl.commands.incident.get_datadog_client", return_value=mock_client): + result = runner.invoke( + incident, + ["update", "inc-1", "--status", "resolved", "--format", "json"], + ) + + assert result.exit_code == 0, f"Command failed: {result.output}" + output = json.loads(result.output) + assert output["status"] == "resolved" + class TestDeleteIncident: def test_delete_incident_with_confirm(self, mock_client, runner):