Skip to content

Commit 6d5750b

Browse files
authored
Add manifest-driven fixture selection for degradation curves
1 parent 90a0114 commit 6d5750b

2 files changed

Lines changed: 101 additions & 10 deletions

File tree

src/validation/degradation_curve_generator.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,43 @@ def _load_fixture_manifest(self, manifest_path: Path = MANIFEST_PATH) -> tuple[d
118118
raise ValueError(f"invalid fixture manifest format: {manifest_path}")
119119
return tuple(fixtures)
120120

121-
def fixtures_for_layered_admissibility_curve(self, manifest_path: Path = MANIFEST_PATH) -> tuple[Path, ...]:
121+
def fixtures_for_manifest_family(
122+
self,
123+
family: str,
124+
levels: tuple[str, ...] = LAYERED_CURVE_LEVELS,
125+
manifest_path: Path = MANIFEST_PATH,
126+
) -> tuple[Path, ...]:
122127
level_to_path: dict[str, Path] = {}
123128

124129
for entry in self._load_fixture_manifest(manifest_path):
125-
if entry.get("family") != LAYERED_CURVE_FAMILY:
130+
if entry.get("family") != family:
126131
continue
132+
127133
level = entry.get("degradation_level")
128-
if level in LAYERED_CURVE_LEVELS:
129-
path_str = entry.get("path")
130-
if not path_str:
131-
raise ValueError(f"missing path for fixture in manifest: {entry.get('fixture_id')}")
132-
level_to_path[str(level)] = Path(path_str)
134+
if level not in levels:
135+
continue
136+
137+
path_str = entry.get("path")
138+
if not path_str:
139+
raise ValueError(f"missing path for fixture in manifest: {entry.get('fixture_id')}")
133140

134-
missing_levels = [level for level in LAYERED_CURVE_LEVELS if level not in level_to_path]
141+
level_key = str(level)
142+
if level_key in level_to_path:
143+
raise ValueError(f"duplicate fixture for family '{family}' level '{level_key}'")
144+
level_to_path[level_key] = Path(path_str)
145+
146+
missing_levels = [level for level in levels if level not in level_to_path]
135147
if missing_levels:
136-
raise ValueError(f"missing layered admissibility fixtures for levels: {missing_levels}")
148+
raise ValueError(f"missing fixtures for family '{family}' levels: {missing_levels}")
149+
150+
return tuple(level_to_path[level] for level in levels)
137151

138-
return tuple(level_to_path[level] for level in LAYERED_CURVE_LEVELS)
152+
def fixtures_for_layered_admissibility_curve(self, manifest_path: Path = MANIFEST_PATH) -> tuple[Path, ...]:
153+
return self.fixtures_for_manifest_family(
154+
family=LAYERED_CURVE_FAMILY,
155+
levels=LAYERED_CURVE_LEVELS,
156+
manifest_path=manifest_path,
157+
)
139158

140159
def generate(self, fixtures: list[Path] | tuple[Path, ...], curve_id: str) -> DegradationCurve:
141160
points = tuple(self.evaluate_fixture(path) for path in fixtures)

tests/test_degradation_curve_generator.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
MILD_FIXTURE = Path("fixtures/coding_workflow_pr_review_mild_v1")
1313
MODERATE_FIXTURE = Path("fixtures/coding_workflow_pr_review_moderate_v1")
1414
NEG_FIXTURE = Path("fixtures/coding_workflow_pr_review_degraded_v1")
15+
INCIDENT_POS_FIXTURE = Path("fixtures/incident_response_page_triage_v1")
16+
INCIDENT_MILD_FIXTURE = Path("fixtures/incident_response_page_triage_mild_v1")
17+
INCIDENT_MODERATE_FIXTURE = Path("fixtures/incident_response_page_triage_moderate_v1")
18+
INCIDENT_NEG_FIXTURE = Path("fixtures/incident_response_page_triage_degraded_v1")
1519
ARTIFACT_PATH = Path("artifacts/layered_admissibility_results.json")
1620
CURVE_ID = "coding_workflow_pr_review_curve_v1"
1721

@@ -57,6 +61,74 @@ def test_layered_curve_fixtures_are_loaded_from_manifest_order() -> None:
5761
NEG_FIXTURE.as_posix(),
5862
]
5963

64+
65+
def test_manifest_family_fixtures_for_coding_workflow_are_loaded_in_level_order() -> None:
66+
fixtures = DegradationCurveGenerator().fixtures_for_manifest_family("coding_workflow_pr_review")
67+
assert [fixture.as_posix() for fixture in fixtures] == [
68+
POS_FIXTURE.as_posix(),
69+
MILD_FIXTURE.as_posix(),
70+
MODERATE_FIXTURE.as_posix(),
71+
NEG_FIXTURE.as_posix(),
72+
]
73+
74+
75+
def test_manifest_family_fixtures_for_incident_response_are_loaded_in_level_order() -> None:
76+
fixtures = DegradationCurveGenerator().fixtures_for_manifest_family("incident_response_page_triage")
77+
assert [fixture.as_posix() for fixture in fixtures] == [
78+
INCIDENT_POS_FIXTURE.as_posix(),
79+
INCIDENT_MILD_FIXTURE.as_posix(),
80+
INCIDENT_MODERATE_FIXTURE.as_posix(),
81+
INCIDENT_NEG_FIXTURE.as_posix(),
82+
]
83+
84+
85+
def test_layered_curve_wrapper_remains_compatible_with_coding_workflow_family() -> None:
86+
generator = DegradationCurveGenerator()
87+
assert generator.fixtures_for_layered_admissibility_curve() == generator.fixtures_for_manifest_family(
88+
"coding_workflow_pr_review"
89+
)
90+
91+
92+
def test_manifest_family_selection_missing_family_or_level_raises_value_error() -> None:
93+
generator = DegradationCurveGenerator()
94+
with pytest.raises(ValueError, match="missing fixtures for family 'nonexistent_family' levels"):
95+
generator.fixtures_for_manifest_family("nonexistent_family")
96+
97+
with pytest.raises(ValueError, match="missing fixtures for family 'coding_workflow_pr_review' levels"):
98+
generator.fixtures_for_manifest_family("coding_workflow_pr_review", levels=("baseline", "unknown_level"))
99+
100+
101+
def test_manifest_family_selection_duplicate_level_raises_value_error(tmp_path: Path) -> None:
102+
manifest_path = tmp_path / "manifest.json"
103+
manifest_path.write_text(
104+
json.dumps(
105+
{
106+
"fixtures": [
107+
{
108+
"fixture_id": "fixture_a",
109+
"family": "dup_family",
110+
"degradation_level": "baseline",
111+
"path": "fixtures/a",
112+
},
113+
{
114+
"fixture_id": "fixture_b",
115+
"family": "dup_family",
116+
"degradation_level": "baseline",
117+
"path": "fixtures/b",
118+
},
119+
]
120+
}
121+
),
122+
encoding="utf-8",
123+
)
124+
125+
with pytest.raises(ValueError, match="duplicate fixture for family 'dup_family' level 'baseline'"):
126+
DegradationCurveGenerator().fixtures_for_manifest_family(
127+
"dup_family",
128+
levels=("baseline",),
129+
manifest_path=manifest_path,
130+
)
131+
60132
def test_to_dict_is_json_compatible_and_sorted() -> None:
61133
generator = DegradationCurveGenerator()
62134
curve = generator.generate(generator.fixtures_for_layered_admissibility_curve(), curve_id=CURVE_ID)

0 commit comments

Comments
 (0)