Skip to content

Commit b134549

Browse files
authored
feat: add executions-list filters (rebase v4) (#27)
Same as before: 4 filters, 6 new tests. Rebased against main 2026-05-04 (post-#26, #28, #30, #33). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 5352c38 commit b134549

2 files changed

Lines changed: 175 additions & 1 deletion

File tree

cueapi/cli.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,10 +743,48 @@ def executions() -> None:
743743
@executions.command(name="list")
744744
@click.option("--cue-id", "cue_id", default=None, help="Filter to a specific cue")
745745
@click.option("--status", default=None, help="Filter by execution status")
746+
@click.option(
747+
"--outcome-state",
748+
"outcome_state",
749+
default=None,
750+
help=(
751+
"Filter by outcome_state: reported_success / reported_failure / "
752+
"verified_success / verification_pending / verification_failed / unknown."
753+
),
754+
)
755+
@click.option(
756+
"--result-type",
757+
"result_type",
758+
default=None,
759+
help="Filter by evidence result_type (e.g. 'pr', 'issue', 'comment').",
760+
)
761+
@click.option(
762+
"--has-evidence",
763+
"has_evidence",
764+
is_flag=True,
765+
default=False,
766+
help="Filter to executions that reported evidence (evidence_external_id is set).",
767+
)
768+
@click.option(
769+
"--triggered-by",
770+
"triggered_by",
771+
default=None,
772+
help="Filter by triggered_by: scheduled / manual_fire / chain.",
773+
)
746774
@click.option("--limit", default=20, type=int, help="Max results")
747775
@click.option("--offset", default=0, type=int, help="Offset for pagination")
748776
@click.pass_context
749-
def executions_list(ctx: click.Context, cue_id: Optional[str], status: Optional[str], limit: int, offset: int) -> None:
777+
def executions_list(
778+
ctx: click.Context,
779+
cue_id: Optional[str],
780+
status: Optional[str],
781+
outcome_state: Optional[str],
782+
result_type: Optional[str],
783+
has_evidence: bool,
784+
triggered_by: Optional[str],
785+
limit: int,
786+
offset: int,
787+
) -> None:
750788
"""List historical executions across all cues."""
751789
try:
752790
with CueAPIClient(api_key=ctx.obj.get("api_key"), profile=ctx.obj.get("profile")) as client:
@@ -755,6 +793,18 @@ def executions_list(ctx: click.Context, cue_id: Optional[str], status: Optional[
755793
params["cue_id"] = cue_id
756794
if status:
757795
params["status"] = status
796+
if outcome_state:
797+
params["outcome_state"] = outcome_state
798+
if result_type:
799+
params["result_type"] = result_type
800+
# Server-side `has_evidence` filter is meaningful only when True
801+
# (it ANDs `evidence_external_id IS NOT NULL`). Unset = no filter,
802+
# so omit from query params when False rather than send `false`
803+
# which would still mean the same thing but adds URL noise.
804+
if has_evidence:
805+
params["has_evidence"] = "true"
806+
if triggered_by:
807+
params["triggered_by"] = triggered_by
758808
resp = client.get("/executions", params=params)
759809
if resp.status_code != 200:
760810
echo_error(f"Failed (HTTP {resp.status_code})")

tests/test_cli.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,3 +1409,127 @@ def get(self, path, **_):
14091409
result = runner.invoke(main, ["executions", "get", "exec_abc"])
14101410
assert result.exit_code == 0, result.output
14111411
assert "Payload:" not in result.output
1412+
1413+
1414+
# --- executions list filter parity (cueapi-cli #25 manifest gap) ---
1415+
1416+
1417+
class _FakeResp:
1418+
def __init__(self, status_code: int, payload: dict):
1419+
self.status_code = status_code
1420+
self._payload = payload
1421+
1422+
def json(self):
1423+
return self._payload
1424+
1425+
1426+
class _ListClient:
1427+
def __init__(self):
1428+
self.last_params: Optional[dict] = None
1429+
1430+
def __enter__(self):
1431+
return self
1432+
1433+
def __exit__(self, *_):
1434+
pass
1435+
1436+
def get(self, path, params=None, **_):
1437+
self.last_params = params
1438+
return _FakeResp(200, {"executions": [], "total": 0, "limit": 20, "offset": 0})
1439+
1440+
1441+
def _patched_list_client(monkeypatch, holder):
1442+
import cueapi.cli as cli_mod
1443+
1444+
def fake_factory(*_, **__):
1445+
holder["client"] = _ListClient()
1446+
return holder["client"]
1447+
1448+
monkeypatch.setattr(cli_mod, "CueAPIClient", fake_factory)
1449+
1450+
1451+
def test_executions_list_help_includes_new_filters():
1452+
result = runner.invoke(main, ["executions", "list", "--help"])
1453+
assert result.exit_code == 0
1454+
assert "--outcome-state" in result.output
1455+
assert "--result-type" in result.output
1456+
assert "--has-evidence" in result.output
1457+
assert "--triggered-by" in result.output
1458+
1459+
1460+
def test_executions_list_outcome_state_passed_as_query_param(monkeypatch):
1461+
holder: dict = {}
1462+
_patched_list_client(monkeypatch, holder)
1463+
result = runner.invoke(
1464+
main,
1465+
["executions", "list", "--outcome-state", "verified_success"],
1466+
)
1467+
assert result.exit_code == 0, result.output
1468+
assert holder["client"].last_params.get("outcome_state") == "verified_success"
1469+
1470+
1471+
def test_executions_list_result_type_passed(monkeypatch):
1472+
holder: dict = {}
1473+
_patched_list_client(monkeypatch, holder)
1474+
result = runner.invoke(
1475+
main,
1476+
["executions", "list", "--result-type", "pr"],
1477+
)
1478+
assert result.exit_code == 0, result.output
1479+
assert holder["client"].last_params.get("result_type") == "pr"
1480+
1481+
1482+
def test_executions_list_has_evidence_only_sent_when_true(monkeypatch):
1483+
# has_evidence is a flag — present means True. Unset = omit. Pinning
1484+
# this so a refactor can't silently start sending `false` (which would
1485+
# still mean "no filter" server-side, but creates noisy URLs and invites
1486+
# future bugs).
1487+
holder: dict = {}
1488+
_patched_list_client(monkeypatch, holder)
1489+
result_no_flag = runner.invoke(main, ["executions", "list"])
1490+
assert result_no_flag.exit_code == 0
1491+
assert "has_evidence" not in (holder["client"].last_params or {})
1492+
1493+
holder2: dict = {}
1494+
_patched_list_client(monkeypatch, holder2)
1495+
result_with_flag = runner.invoke(main, ["executions", "list", "--has-evidence"])
1496+
assert result_with_flag.exit_code == 0
1497+
assert holder2["client"].last_params.get("has_evidence") == "true"
1498+
1499+
1500+
def test_executions_list_triggered_by_passed(monkeypatch):
1501+
holder: dict = {}
1502+
_patched_list_client(monkeypatch, holder)
1503+
result = runner.invoke(
1504+
main,
1505+
["executions", "list", "--triggered-by", "manual_fire"],
1506+
)
1507+
assert result.exit_code == 0, result.output
1508+
assert holder["client"].last_params.get("triggered_by") == "manual_fire"
1509+
1510+
1511+
def test_executions_list_combines_all_filters(monkeypatch):
1512+
holder: dict = {}
1513+
_patched_list_client(monkeypatch, holder)
1514+
result = runner.invoke(
1515+
main,
1516+
[
1517+
"executions", "list",
1518+
"--cue-id", "cue_xyz",
1519+
"--status", "success",
1520+
"--outcome-state", "verified_success",
1521+
"--result-type", "pr",
1522+
"--has-evidence",
1523+
"--triggered-by", "scheduled",
1524+
"--limit", "50",
1525+
],
1526+
)
1527+
assert result.exit_code == 0, result.output
1528+
p = holder["client"].last_params
1529+
assert p["cue_id"] == "cue_xyz"
1530+
assert p["status"] == "success"
1531+
assert p["outcome_state"] == "verified_success"
1532+
assert p["result_type"] == "pr"
1533+
assert p["has_evidence"] == "true"
1534+
assert p["triggered_by"] == "scheduled"
1535+
assert p["limit"] == 50

0 commit comments

Comments
 (0)