feat(038): Phase 5 — US4 immutable resolver
- T022-T025: ScenarioGraph.Resolver.Resolve — typed parameter/selector/manual/ remove-step resolutions emit immutable revisions linked via parent_revision_hash; stale base revision rejected (409 semantics); unrelated step ids/order unchanged - Selector hints recorded in step description for auditability - 58 scenario tests pass; ruff clean; regions balanced
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# #region Test.Scenario.Resolver [C:3] [TYPE Module] [SEMANTICS testing,scenario,resolver,revision]
|
||||
# @defgroup Test.Scenario Resolver immutable-revision tests.
|
||||
# @LAYER Test
|
||||
# @RELATION BINDS_TO -> [ScenarioGraph.Resolver.Resolve]
|
||||
# @RATIONALE Resolution produces immutable revisions; unrelated structure must never change.
|
||||
# @REJECTED Testing only happy-path resolution — would hide stale-base and unrelated-change regressions.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.services.dashboard_testing.scenario.models import DashboardTestScenario
|
||||
from src.services.dashboard_testing.scenario.resolver import ResolveChange, resolve_scenario
|
||||
|
||||
_FIXTURES = Path(__file__).resolve().parents[3] / "fixtures" / "dashboard_scenarios"
|
||||
|
||||
|
||||
def _load() -> DashboardTestScenario:
|
||||
return DashboardTestScenario.model_validate(json.loads((_FIXTURES / "scenario_valid.json").read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def test_parameter_resolution_creates_new_revision() -> None:
|
||||
sc = _load()
|
||||
resolved = resolve_scenario(sc, [ResolveChange(kind="parameter", target="test_date", value="2026-08-01")])
|
||||
assert resolved.revision_hash != sc.revision_hash
|
||||
assert resolved.parent_revision_hash == sc.revision_hash
|
||||
param = next(p for p in resolved.parameters if p.name == "test_date")
|
||||
assert param.value == "2026-08-01"
|
||||
assert param.status == "resolved"
|
||||
|
||||
|
||||
def test_unrelated_steps_unchanged() -> None:
|
||||
sc = _load()
|
||||
before = [(s.id, s.automation_status) for s in sc.steps]
|
||||
resolved = resolve_scenario(sc, [ResolveChange(kind="parameter", target="counterparty", value="Beta LLC")])
|
||||
after = [(s.id, s.automation_status) for s in resolved.steps]
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_selector_hint_resolution() -> None:
|
||||
sc = _load()
|
||||
resolved = resolve_scenario(sc, [ResolveChange(kind="selector", target="phase-2-B01-apply_filters", value="#filter-input")])
|
||||
assert resolved.revision_hash != sc.revision_hash
|
||||
assert resolved.parent_revision_hash == sc.revision_hash
|
||||
|
||||
|
||||
def test_stale_base_revision_rejected() -> None:
|
||||
sc = _load()
|
||||
with pytest.raises(ValueError, match="stale"):
|
||||
resolve_scenario(sc, [ResolveChange(kind="parameter", target="test_date", value="2026-08-01")], base_revision_hash="0" * 64)
|
||||
|
||||
|
||||
def test_invalid_parameter_type_rejected() -> None:
|
||||
sc = _load()
|
||||
with pytest.raises(ValidationError):
|
||||
resolve_scenario(sc, [ResolveChange(kind="parameter", target="test_date", value=12345)])
|
||||
|
||||
|
||||
def test_unknown_target_rejected() -> None:
|
||||
sc = _load()
|
||||
with pytest.raises(ValueError, match=r"unknown"):
|
||||
resolve_scenario(sc, [ResolveChange(kind="parameter", target="nonexistent_param", value="x")])
|
||||
Reference in New Issue
Block a user