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:
2026-07-31 13:00:11 +03:00
parent 9224d1a9ca
commit 31e8524a32
3 changed files with 219 additions and 5 deletions

View File

@@ -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")])