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
20 changes: 19 additions & 1 deletion cueapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,24 @@ def delete(ctx: click.Context, cue_id: str, yes: bool) -> None:
@click.argument("cue_id")
@click.option("--payload-override", "payload_override", default=None, help="JSON payload override for this fire only")
@click.option("--merge-strategy", "merge_strategy", type=click.Choice(["merge", "replace"]), default=None, help="How payload-override combines with the cue's stored payload (default: merge, server-side)")
@click.option(
"--send-at",
"send_at",
default=None,
help=(
"Optional UTC timestamp (ISO 8601) to schedule this fire for the future. "
"Server gates dispatch until send-at <= now. Past timestamps are treated as "
"'fire now' (idempotent — no error). Hosted PR #618."
),
)
@click.pass_context
def fire(ctx: click.Context, cue_id: str, payload_override: Optional[str], merge_strategy: Optional[str]) -> None:
def fire(
ctx: click.Context,
cue_id: str,
payload_override: Optional[str],
merge_strategy: Optional[str],
send_at: Optional[str],
) -> None:
"""Fire an existing cue immediately, optionally overriding its payload."""
body: dict = {}
if payload_override:
Expand All @@ -465,6 +481,8 @@ def fire(ctx: click.Context, cue_id: str, payload_override: Optional[str], merge
raise click.UsageError("--payload-override must be valid JSON")
if merge_strategy:
body["merge_strategy"] = merge_strategy
if send_at:
body["send_at"] = send_at

try:
with CueAPIClient(api_key=ctx.obj.get("api_key"), profile=ctx.obj.get("profile")) as client:
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2160,3 +2160,81 @@ def test_top_level_help_lists_messages():
result = runner.invoke(main, ["--help"])
assert result.exit_code == 0
assert "messages" in result.output


# --- fire --send-at (hosted PR #618 port) ---


class _FireSendAtClient:
def __init__(self):
self.last_body: Optional[dict] = None

def __enter__(self):
return self

def __exit__(self, *_):
pass

def post(self, path, json=None, **_):
self.last_body = json
class _R:
status_code = 200
def json(self):
return {"id": "exec_test", "scheduled_for": "2026-05-04T20:00:00Z"}
return _R()


def _patch_fire_client(monkeypatch, holder):
import cueapi.cli as cli_mod

def fake_factory(*_, **__):
holder["client"] = _FireSendAtClient()
return holder["client"]

monkeypatch.setattr(cli_mod, "CueAPIClient", fake_factory)


def test_fire_help_lists_send_at():
result = runner.invoke(main, ["fire", "--help"])
assert result.exit_code == 0
assert "--send-at" in result.output


def test_fire_send_at_passed_to_body(monkeypatch):
holder: dict = {}
_patch_fire_client(monkeypatch, holder)
result = runner.invoke(
main,
["fire", "cue_x", "--send-at", "2026-05-04T20:00:00Z"],
)
assert result.exit_code == 0, result.output
assert holder["client"].last_body == {"send_at": "2026-05-04T20:00:00Z"}


def test_fire_omits_send_at_when_unset(monkeypatch):
# Pin: when --send-at isn't passed, the body must not include the key.
holder: dict = {}
_patch_fire_client(monkeypatch, holder)
result = runner.invoke(main, ["fire", "cue_x"])
assert result.exit_code == 0
assert "send_at" not in (holder["client"].last_body or {})


def test_fire_combines_send_at_with_payload_override(monkeypatch):
holder: dict = {}
_patch_fire_client(monkeypatch, holder)
result = runner.invoke(
main,
[
"fire", "cue_x",
"--payload-override", '{"task": "demo"}',
"--merge-strategy", "replace",
"--send-at", "2026-05-04T22:00:00Z",
],
)
assert result.exit_code == 0
assert holder["client"].last_body == {
"payload_override": {"task": "demo"},
"merge_strategy": "replace",
"send_at": "2026-05-04T22:00:00Z",
}