feat(ansible): a run's scope reads as the target or group it names

#5014 "Ansible run scope is shown as a raw id"

- inventory_gen.describe_scope / scope_labels turn "steward:target:<uuid>"
  into "target web-1", "steward:group:<uuid>" into "group Docker Swarm",
  and a deleted one into "target abcdef01 (deleted)".
- MCP run and schedule rows carry scope_label beside the stored scope; the
  ansible.run.started audit detail records it too.
- The web run page and schedules table show the name, with the stored
  scope as the tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 10:29:06 -04:00
co-authored by Claude Opus 5.5
parent 30cb375079
commit 06e89308a8
7 changed files with 74 additions and 12 deletions
+10
View File
@@ -128,3 +128,13 @@ def test_yaml_injected_hostvars_survive():
def test_yaml_empty_targets():
parsed = yaml.safe_load(inventory_to_yaml(generate_inventory([])))
assert parsed == {"all": {"hosts": {}}}
def test_describe_scope_names_what_a_run_targeted():
from steward.ansible.inventory_gen import describe_scope
targets, groups = {"t1": "web-1"}, {"g1": "Docker Swarm"}
assert describe_scope("steward:all", targets, groups) == "all inventory targets"
assert describe_scope("steward:target:t1", targets, groups) == "target web-1"
assert describe_scope("steward:group:g1", targets, groups) == "group Docker Swarm"
assert describe_scope("steward:target:abcdef0123", targets, groups) == "target abcdef01 (deleted)"
assert describe_scope("repo:homelab:hosts.yml", targets, groups) == "homelab/hosts.yml"
+4 -1
View File
@@ -121,6 +121,8 @@ def test_runs_schedules_and_alert_actions_through_mcp(app, tmp_path):
done = await a.wait_for_run(started["run_id"], timeout_s=120)
assert done["finished"] and done["status"] == "success", done["tail"]
assert done["check"] is True and done["triggered_by"] == username
# The scope reads as the target's name, not its id.
assert done["scope"].startswith("steward:target:") and done["scope_label"] == f"target {target_name}"
assert not out_file.exists()
log = "\n".join(done["tail"])
assert "GREETING-yo" in log and f"TOKEN-LEN-{len(SECRET)}" in log
@@ -134,6 +136,7 @@ def test_runs_schedules_and_alert_actions_through_mcp(app, tmp_path):
detail = json.loads(rows[0].detail_json)
assert detail["check"] is True and detail["token_id"] == 42
assert detail["secret_vars"] == ["api_token"] and SECRET not in rows[0].detail_json
assert detail["scope_label"].startswith(f"target {target_name}")
# ── applying a confirm playbook needs confirmed=true ──────────────
with pytest.raises(ValueError, match="flagged destructive"):
@@ -166,7 +169,7 @@ def test_runs_schedules_and_alert_actions_through_mcp(app, tmp_path):
sc = await a.create_schedule(f"sched-{tag}", src, "play.yml", 3600,
scope=f"target:{target_name}",
extra_vars={"out_file": str(out_file)})
assert sc["check"] is True and sc["enabled"] is True
assert sc["check"] is True and sc["enabled"] is True and sc["scope_label"] == f"target {target_name}"
up = await a.update_schedule(f"sched-{tag}", enabled=False, interval_s=86400)
assert (up["enabled"], up["interval_s"], up["check"]) == (False, 86400, True)
assert up["extra_vars"] == {"out_file": str(out_file)} # untouched fields stay