from __future__ import annotations import pytest from activity_core.rules.actions import expand_rule_actions from activity_core.rules.evaluator import UnsafeExpression class _Attrs: def __init__(self, **kw): for k, v in kw.items(): setattr(self, k, v) class _Event: def __init__(self, **attrs): self.attributes = _Attrs(**attrs) def test_action_field_path_interpolation_resolves_context_value() -> None: rules = [ { "id": "flag-stale-sbom", "condition": "context.repos.sbom_age_days > 30", "action": { "task_template": "Run SBOM rescan for {context.repos.repo_slug}", "target_repo": "context.repos.repo_slug", "priority": "medium", "labels": ["sbom", "{context.repos.repo_slug}"], }, } ] specs = expand_rule_actions( rules, _Event(), {"repos": {"repo_slug": "activity-core", "sbom_age_days": 45}}, ) assert specs == [ { "title": "Run SBOM rescan for activity-core", "description": "", "target_repo": "activity-core", "priority": "medium", "labels": ["sbom", "activity-core"], "due_in_days": None, "source_type": "rule", "source_id": "flag-stale-sbom", "triggering_event_id": "", "activity_definition_id": "", "condition": "context.repos.sbom_age_days > 30", } ] def test_for_each_binds_each_list_item_before_condition_and_action_rendering() -> None: rules = [ { "id": "flag-stale-sbom", "for_each": "context.repos.repos", "bind_as": "repo", "condition": "context.repo.sbom_age_days > 30", "action": { "task_template": "Run SBOM rescan for {context.repo.repo_slug}", "target_repo": "context.repo.repo_slug", "priority": "medium", "labels": ["sbom", "security", "automated"], }, } ] context = { "repos": { "repos": [ {"repo_slug": "repo-a", "sbom_age_days": 60}, {"repo_slug": "repo-b", "sbom_age_days": 10}, {"repo_slug": "repo-c", "sbom_age_days": 45}, ] } } specs = expand_rule_actions(rules, _Event(), context) assert [spec["target_repo"] for spec in specs] == ["repo-a", "repo-c"] assert [spec["title"] for spec in specs] == [ "Run SBOM rescan for repo-a", "Run SBOM rescan for repo-c", ] def test_for_each_rejects_non_path_expression() -> None: rules = [ { "id": "bad", "for_each": "__import__('os')", "condition": "", "action": {"task_template": "bad"}, } ] with pytest.raises(UnsafeExpression): expand_rule_actions(rules, _Event(), {}) def test_template_placeholder_rejects_non_scalar_values() -> None: rules = [ { "id": "bad", "condition": "", "action": { "task_template": "Run {context.repos}", }, } ] with pytest.raises(UnsafeExpression): expand_rule_actions(rules, _Event(), {"repos": [{"repo_slug": "repo-a"}]})