"""state-tx script nodes must declare CP_URL, STATE_TX_NEW_STATE and STATE_TX_FROM_STATES. scripts/workflow/state-tx fails with exit 2 when any is missing (from_states_missing for STATE_TX_FROM_STATES). CP_SERVICE_TOKEN_FILE is supplied by the workflow-script/v1 composite, not runtime_env, and must not be declared here. STATE_TX_NEW_STATE must equal the template's own output.state. STATE_TX_FROM_STATES is a comma list of acceptable current flow_states and must be non-empty. """ from __future__ import annotations from pathlib import Path import pytest import yaml _DIR = Path(__file__).resolve().parents[2] / "task-templates" / "workflow" # Every template with a state-tx node, discovered from disk so new @N variants # are covered automatically. _NAMES = sorted( p.name for p in _DIR.glob("*.yaml") if any( n.get("cmd") == "scripts/workflow/state-tx" for n in (yaml.safe_load(p.read_text()).get("nodes") or []) ) ) def test_state_tx_templates_discovered(): assert { "test-validate@1.yaml", "frozen-fixture-emit@1.yaml", } <= set(_NAMES) assert "bug-ancestor-auto-advance@1.yaml" not in _NAMES # archived (WT-ANC-15) @pytest.mark.parametrize("name", _NAMES) def test_state_tx_node_declares_required_runtime_env(name): doc = yaml.safe_load((_DIR / name).read_text()) nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"] assert len(nodes) == 1 env = nodes[0].get("runtime_env") or {} assert env.get("CP_URL") assert env.get("STATE_TX_NEW_STATE") == doc["output"]["state"] assert "CP_SERVICE_TOKEN_FILE" not in env assert not any(k.startswith("OIDC_") for k in env) @pytest.mark.parametrize("name", _NAMES) def test_state_tx_node_declares_from_states(name): doc = yaml.safe_load((_DIR / name).read_text()) nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"] env = nodes[0].get("runtime_env") or {} raw = env.get("STATE_TX_FROM_STATES") assert isinstance(raw, str) and raw.strip() states = [s.strip() for s in raw.split(",")] assert all(states), f"empty entry in STATE_TX_FROM_STATES {raw!r}" assert doc["output"]["state"] not in states # The template's own input state must be an acceptable from-state. assert doc["input"]["state"] in states # --------------------------------------------------------------------------- # CP_URL must be the ingress form, never the in-cluster Service on :8100. # # The deploy repo's networkpolicy-cp-internal-traefik-only.yaml admits :8100 only # from dashboard / git-planner-backend / cert-expiry-checker and the traefik and # cms-proxy namespaces. Agent/script pods are dropped, so a template pointing # CP_URL at svc.cluster.local:8100 times out (30s) and, with on_fail: continue, # silently loses the transition. The workflow scripts join CP_URL + "/v1/...", # so the /api ingress path prefix is preserved. # --------------------------------------------------------------------------- CP_URL_INGRESS = "https://agents.oreillyit.nz/api" _FORBIDDEN_CP_URL_MARKERS = ("svc.cluster.local:8100", "controlplane.agent-runtimes.svc") _ALL_TEMPLATES = sorted(_DIR.glob("*.yaml")) def _cp_url_values(path: Path) -> list[str]: doc = yaml.safe_load(path.read_text()) return [ (n.get("runtime_env") or {})["CP_URL"] for n in (doc.get("nodes") or []) if "CP_URL" in (n.get("runtime_env") or {}) ] def test_cp_url_templates_discovered(): names = {p.name for p in _ALL_TEMPLATES if _cp_url_values(p)} assert { "test-validate@1.yaml", "frozen-fixture-emit@1.yaml", "scope-decompose-sonnet@1.yaml", "scope-decompose-sonnet@2.yaml", "bug-verify-sonnet@1.yaml", "bug-verify-sonnet@2.yaml", } <= names @pytest.mark.parametrize("path", _ALL_TEMPLATES, ids=lambda p: p.name) def test_cp_url_is_ingress_not_in_cluster_service(path): for value in _cp_url_values(path): assert not any(m in value for m in _FORBIDDEN_CP_URL_MARKERS), ( f"{path.name}: CP_URL={value!r} targets the in-cluster CP service; agent pods " "are blocked on :8100 by NetworkPolicy" ) assert value == CP_URL_INGRESS, f"{path.name}: CP_URL={value!r}" @pytest.mark.parametrize("path", _ALL_TEMPLATES, ids=lambda p: p.name) def test_no_template_mentions_in_cluster_cp_service(path): text = path.read_text() assert not any(m in text for m in _FORBIDDEN_CP_URL_MARKERS)