"""Profile-driven execution selection (ACT-ADR-006 / ACTIVITY-WP-0032-T02).""" from __future__ import annotations import pytest from activity_core.glas_profile import ( EXECUTION_REF_KEYS, ProfileRefError, normalise_execution_refs, resolve_execution_selector, validate_profile_ref, ) class TestProfileRefStructure: def test_accepts_a_pinned_ref(self) -> None: assert ( validate_profile_ref("harness.agent-dev-local@1.0.0") == "harness.agent-dev-local@1.0.0" ) def test_strips_surrounding_whitespace(self) -> None: assert validate_profile_ref(" harness.agent-dev@1.0.0\n") == "harness.agent-dev@1.0.0" def test_requires_a_version_pin(self) -> None: """Glas resolves an unpinned ref against every version and refuses it as ambiguous — catch that here instead, without knowing any profile.""" with pytest.raises(ProfileRefError, match="must pin a version"): validate_profile_ref("harness.agent-dev-local") @pytest.mark.parametrize("bad", ["", " ", None, 42, ["harness@1.0.0"]]) def test_rejects_empty_or_non_string(self, bad) -> None: with pytest.raises(ProfileRefError): validate_profile_ref(bad) @pytest.mark.parametrize( "bad", ["harness agent@1.0.0", "harness.agent@1.0 .0", "harness\tagent@1.0.0"], ) def test_rejects_internal_whitespace(self, bad: str) -> None: with pytest.raises(ProfileRefError, match="whitespace"): validate_profile_ref(bad) @pytest.mark.parametrize("bad", ["@1.0.0", "-harness@1.0.0", "har/ness@1.0.0"]) def test_rejects_invalid_ids(self, bad: str) -> None: with pytest.raises(ProfileRefError, match="invalid id"): validate_profile_ref(bad) @pytest.mark.parametrize("bad", ["harness@", "harness@v1.0.0", "harness@latest"]) def test_rejects_invalid_versions(self, bad: str) -> None: with pytest.raises(ProfileRefError, match="invalid version"): validate_profile_ref(bad) class TestNoFallbackToApproachHint: """ACT-ADR-006 §2 — the single most important property of this change. approach_hint must never override, synthesize, or fall back from an absent or invalid harness_profile_ref. A silent fallback would reintroduce the claim-time routing failure of 2026-08-17. """ def test_hint_does_not_synthesize_a_missing_profile(self) -> None: profile, hint = resolve_execution_selector(None, "sbom-rescan") assert profile is None, "approach_hint must not become a profile ref" assert hint == "sbom-rescan" def test_hint_does_not_rescue_a_malformed_profile(self) -> None: with pytest.raises(ProfileRefError, match="must pin a version"): resolve_execution_selector("harness.agent-dev-local", "sbom-rescan") def test_malformed_profile_raises_even_with_no_hint(self) -> None: with pytest.raises(ProfileRefError): resolve_execution_selector("not a ref", None) def test_profile_does_not_overwrite_the_legacy_hint(self) -> None: """Coexistence, not replacement: both are carried.""" profile, hint = resolve_execution_selector( "harness.agent-dev-local@1.0.0", "sbom-rescan" ) assert profile == "harness.agent-dev-local@1.0.0" assert hint == "sbom-rescan" def test_absent_profile_is_allowed_during_coexistence(self) -> None: assert resolve_execution_selector(None, None) == (None, None) def test_absent_profile_is_an_error_once_required(self, monkeypatch) -> None: monkeypatch.setenv("ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE", "true") with pytest.raises(ProfileRefError, match="cannot substitute"): resolve_execution_selector(None, "sbom-rescan") def test_required_mode_still_accepts_a_valid_profile(self, monkeypatch) -> None: monkeypatch.setenv("ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE", "1") profile, _ = resolve_execution_selector("harness.agent-dev@2.1.0", None) assert profile == "harness.agent-dev@2.1.0" def test_unknown_but_well_formed_profile_is_left_for_glas(self) -> None: profile, _ = resolve_execution_selector( "harness.not-in-any-local-catalog@9.9.9", None, ) assert profile == "harness.not-in-any-local-catalog@9.9.9" def test_blank_hint_is_normalised_away(self) -> None: assert resolve_execution_selector(None, " ") == (None, None) class TestExecutionRefs: def test_keeps_the_contract_refs(self) -> None: refs = normalise_execution_refs( { "correlation_id": "activity-42", "assignment_ref": "role-assignment:agent-7:42", "role_ref": "role:maintainer@3", "duty_ref": "duty:bounded-change@2", "goal_refs": ["goal:reliable-delivery@5"], "resource_envelope_refs": ["resource-envelope:standard-coding@1"], } ) assert set(refs) == set(EXECUTION_REF_KEYS) assert refs["goal_refs"] == ["goal:reliable-delivery@5"] def test_drops_unknown_keys(self) -> None: """We carry the contract's refs; we do not become a bag for anything.""" refs = normalise_execution_refs( {"role_ref": "role:maintainer@3", "api_key": "sk-nope", "extra": {"a": 1}} ) assert refs == {"role_ref": "role:maintainer@3"} def test_drops_empty_and_wrongly_typed_values(self) -> None: refs = normalise_execution_refs( { "correlation_id": " ", "role_ref": 17, "goal_refs": "not-a-list", "resource_envelope_refs": ["ok", "", " "], } ) assert refs == {"resource_envelope_refs": ["ok"]} def test_drops_non_string_items_in_reference_lists(self) -> None: refs = normalise_execution_refs( {"goal_refs": ["goal:1@1", {"credential": "must-drop"}, 17]} ) assert refs == {"goal_refs": ["goal:1@1"]} @pytest.mark.parametrize("bad", [None, "refs", ["a"], 5]) def test_non_dict_becomes_empty(self, bad) -> None: assert normalise_execution_refs(bad) == {} def test_ops_run_model_carries_the_new_columns() -> None: from activity_core.orm import OpsRun columns = OpsRun.__table__.columns assert "harness_profile_ref" in columns assert columns["harness_profile_ref"].nullable is True assert "execution_refs" in columns assert columns["execution_refs"].nullable is False def test_queue_projection_exposes_selection_fields() -> None: """Executors claim from this projection; the profile must reach them.""" import inspect from activity_core import ops_run_queue source = inspect.getsource(ops_run_queue) assert '"harness_profile_ref": row.harness_profile_ref' in source assert '"execution_refs"' in source