Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions cueapi/resources/executions.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ def heartbeat(self, execution_id: str) -> dict:
"""Send heartbeat to extend claim lease."""
return self._client._post(f"/v1/executions/{execution_id}/heartbeat", json={})

def replay(self, execution_id: str) -> dict:
"""Replay a terminal execution.

Creates a fresh execution against the same cue with the original
execution's ``payload_override`` carried forward. Server-side
constraint: only valid for terminal states (``success`` /
``failed`` / ``missed`` / ``outcome_timeout``); 409 if the
execution is still in flight.

Args:
execution_id: Execution UUID to replay.

Returns:
Dict with ``execution_id`` (new), ``scheduled_for``,
``status`` (``pending``), ``triggered_by`` (``replay``),
``replayed_from`` (the original execution_id).
"""
return self._client._post(
f"/v1/executions/{execution_id}/replay", json={}
)

def mark_verification_pending(self, execution_id: str) -> dict:
"""Mark execution outcome as pending verification."""
return self._client._post(
Expand All @@ -141,5 +162,29 @@ def mark_verified(
valid: bool = True,
reason: Optional[str] = None,
) -> dict:
"""Mark execution outcome as verified or verification failed."""
return self._client._post(f"/v1/executions/{execution_id}/verify", json={})
"""Mark execution outcome as verified or verification failed.

Args:
execution_id: Execution UUID.
valid: ``True`` (default) transitions to ``verified_success``;
``False`` transitions to ``verification_failed``.
reason: Optional human-readable reason (max 500 chars).
Appended to the execution's ``evidence_summary``. Most
useful with ``valid=False`` to record why verification
failed.

Returns:
Dict with the new ``outcome_state`` and timestamp fields.
"""
# Bug fix: the prior implementation accepted ``valid`` and
# ``reason`` kwargs but always sent ``json={}``. The server
# treated absent body as ``valid=true``, so callers passing
# ``valid=False`` or ``reason="..."`` got ``verified_success``
# regardless of intent — silently dropping the kwargs. Pinned
# by the corresponding regression test.
body: Dict[str, Any] = {"valid": valid}
if reason is not None:
body["reason"] = reason
return self._client._post(
f"/v1/executions/{execution_id}/verify", json=body
)
84 changes: 82 additions & 2 deletions tests/test_executions_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,93 @@ def test_mark_verification_pending(self):
"/v1/executions/exec_123/verification-pending", json={},
)

def test_mark_verified(self):
def test_mark_verified_default_sends_valid_true(self):
# Regression: prior implementation always sent ``json={}`` and
# silently dropped both kwargs. The server treated absent body
# as ``valid=true``, so the default-arg path produced the right
# outcome by accident — but ``valid=False`` and ``reason="..."``
# callers got ``verified_success`` instead of their intent.
# Pinning that the default-arg path now sends ``{"valid": True}``
# explicitly.
mock_client = MagicMock()
mock_client._post.return_value = {"outcome_state": "verified_success"}
resource = ExecutionsResource(mock_client)

resource.mark_verified("exec_123")

mock_client._post.assert_called_once_with(
"/v1/executions/exec_123/verify", json={},
"/v1/executions/exec_123/verify", json={"valid": True},
)

def test_mark_verified_with_invalid_sends_false(self):
# The fix: ``valid=False`` MUST land in the body. Pre-fix this
# was silently dropped.
mock_client = MagicMock()
mock_client._post.return_value = {"outcome_state": "verification_failed"}
resource = ExecutionsResource(mock_client)

resource.mark_verified("exec_123", valid=False)

mock_client._post.assert_called_once_with(
"/v1/executions/exec_123/verify", json={"valid": False},
)

def test_mark_verified_with_reason(self):
# The fix: ``reason`` MUST land in the body. Pre-fix this was
# silently dropped, so any caller passing a reason saw it
# disappear into the ether.
mock_client = MagicMock()
mock_client._post.return_value = {"outcome_state": "verification_failed"}
resource = ExecutionsResource(mock_client)

resource.mark_verified("exec_123", valid=False, reason="evidence missing")

mock_client._post.assert_called_once_with(
"/v1/executions/exec_123/verify",
json={"valid": False, "reason": "evidence missing"},
)

def test_mark_verified_omits_reason_when_none(self):
# ``reason=None`` must NOT serialize as ``"reason": null``; it
# must be omitted entirely. Pinning the omit-when-default
# behavior.
mock_client = MagicMock()
mock_client._post.return_value = {"outcome_state": "verified_success"}
resource = ExecutionsResource(mock_client)

resource.mark_verified("exec_123", valid=True, reason=None)

sent_body = mock_client._post.call_args.kwargs["json"]
assert "reason" not in sent_body


class TestReplay:
def test_replay_posts_to_replay_endpoint(self):
mock_client = MagicMock()
mock_client._post.return_value = {
"execution_id": "exec_new",
"scheduled_for": "2026-05-04T17:30:00Z",
"status": "pending",
"triggered_by": "replay",
"replayed_from": "exec_old",
}
resource = ExecutionsResource(mock_client)

result = resource.replay("exec_old")

mock_client._post.assert_called_once_with(
"/v1/executions/exec_old/replay", json={},
)
assert result["execution_id"] == "exec_new"
assert result["triggered_by"] == "replay"

def test_replay_returns_server_dict_unchanged(self):
# SDK doesn't transform the response — caller gets the raw dict
# the server returned. Pin so a future refactor can't silently
# start munging fields.
mock_client = MagicMock()
mock_client._post.return_value = {"execution_id": "exec_x", "extra": "field"}
resource = ExecutionsResource(mock_client)

result = resource.replay("exec_old")
assert result == {"execution_id": "exec_x", "extra": "field"}
Loading