Skip to content

Commit 43e9ae4

Browse files
Trecekclaude
andauthored
Implementation Plan: T5-P3-A1-WP1 Recipe × Backend Composition Matrix CI Gate (#4080)
## Summary Add `tests/recipe/test_recipe_backend_composition_matrix.py` — a parametrized test module that validates every bundled recipe composes validly under every registered backend. The module uses: - **Full cross-product parametrization** from live `BACKEND_REGISTRY × all_validated_recipe_names()` (currently 20 recipes × 2 backends = 40 cells) - **`DECLARED_UNSUPPORTED` frozenset** for by-design unsupported combos (currently empty — all combos are supported) - **`KNOWN_BROKEN` dict** for should-work-but-broken combos decorated with `xfail(strict=True)` (currently 4 cells: agent-eval and skill-eval on both backends, tracking #4069) - **`UNSUPPORTED_REASONS` dict** mapping each `DECLARED_UNSUPPORTED` entry to `{reason, issue_ref}` - **Orphan meta-test** preventing skip-rot (stale `DECLARED_UNSUPPORTED` entries) - **Collection-count meta-test** preventing matrix shrinkage Per live cell, the test calls `load_and_validate(recipe_name, project_dir=_PROJECT_ROOT, backend_name=backend_name, ingredient_overrides=_backend_capability_overrides(get_backend(backend_name)), lister=DefaultSkillResolver())` and asserts: `result['valid'] is True`, `result['content']` is non-empty, no `[post-prune] dangling route:` suggestion, no `backend-incompatible-skill` ERROR suggestion. Closes #4008 ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260611-182135-648906/.autoskillit/temp/make-plan/t5-p3-a1-wp1-recipe-backend-composition-matrix_plan_2026-06-11_182600.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 85 | 23.7k | 2.0M | 101.8k | 48 | 80.3k | 30m 21s | | verify* | sonnet | 1 | 76 | 9.3k | 394.4k | 56.6k | 23 | 35.7k | 5m 12s | | implement* | MiniMax-M3 | 1 | 1.1M | 6.8k | 0 | 0 | 49 | 0 | 5m 37s | | fix* | sonnet | 1 | 134 | 5.7k | 834.4k | 69.1k | 48 | 48.2k | 3m 28s | | audit_impl* | sonnet | 1 | 1.1k | 5.3k | 240.1k | 40.1k | 17 | 23.5k | 3m 0s | | prepare_pr* | MiniMax-M3 | 1 | 234.5k | 2.2k | 0 | 0 | 15 | 0 | 56s | | compose_pr* | MiniMax-M3 | 1 | 251.6k | 1.7k | 0 | 0 | 15 | 0 | 59s | | **Total** | | | 1.6M | 54.8k | 3.5M | 101.8k | | 187.7k | 49m 35s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 140 | 0.0 | 0.0 | 48.8 | | fix | 4 | 208595.2 | 12058.8 | 1436.8 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **144** | 24281.4 | 1303.7 | 380.6 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 85 | 23.7k | 2.0M | 80.3k | 30m 21s | | sonnet | 3 | 1.3k | 20.4k | 1.5M | 107.5k | 11m 41s | | MiniMax-M3 | 3 | 1.6M | 10.7k | 0 | 0 | 7m 32s | --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2b59b5a commit 43e9ae4

4 files changed

Lines changed: 144 additions & 0 deletions

File tree

tests/_test_filter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ class ImportContext(enum.StrEnum):
682682
# recipe direct-import entries (import autoskillit.workspace at AST level):
683683
"recipe/test_contracts.py",
684684
"recipe/test_backend_reachability.py",
685+
"recipe/test_recipe_backend_composition_matrix.py",
685686
"recipe/test_rules_backend_compat.py",
686687
"recipe/test_rules_skill_content.py",
687688
"recipe/test_rules_stamp_ownership.py",

tests/arch/test_layer_enforcement.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,10 @@ def test_default_classes_only_instantiated_inside_factory_or_allowlist() -> None
14801480
"tests/recipe/test_backend_reachability.py": frozenset(
14811481
{"autoskillit.server", "autoskillit.execution", "autoskillit.workspace"}
14821482
),
1483+
# composition matrix crosses the same packages as reachability — full pipeline
1484+
"tests/recipe/test_recipe_backend_composition_matrix.py": frozenset(
1485+
{"autoskillit.server", "autoskillit.execution", "autoskillit.workspace"}
1486+
),
14831487
# review loop routing integration imports root-level smoke_utils
14841488
"tests/recipe/test_review_loop_routing_integration.py": frozenset({"autoskillit.smoke_utils"}),
14851489
# migration tests — migration engine integrates with execution.session

tests/recipe/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Recipe I/O, validation, semantic rules, schema, and bundled recipe tests.
8383
| `test_planner_recipe.py` | Tests for the planner recipe structure |
8484
| `test_promote_to_main_wrapper.py` | Tests for the promote-to-main wrapper recipe |
8585
| `test_pseudocode_sync_rule.py` | Tests for the pseudocode-callable-divergence semantic rule |
86+
| `test_recipe_backend_composition_matrix.py` | Recipe x backend composition matrix: parametrized CI gate validating every (recipe, backend) combination with declared-unsupported and known-broken governance |
8687
| `test_recipe_ci_applicable_routing.py` | Structural tests for ci_applicable routing guards across all wait_for_ci chains |
8788
| `test_recipe_ci_contracts.py` | Cross-recipe ci_event/branch coherence and remote_url structural tests |
8889
| `test_recipe_ci_watch_event.py` | Tests for CI watch event in recipe steps |
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Recipe x backend composition matrix -- full cross-product CI gate.
2+
3+
Validates every bundled recipe composes validly under every registered backend.
4+
DECLARED_UNSUPPORTED governs by-design unsupported combos; orphan and
5+
collection-count meta-tests prevent skip-rot and matrix shrinkage.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
from typing import Any
12+
13+
import pytest
14+
15+
from autoskillit.core import Severity
16+
from autoskillit.execution.backends import BACKEND_REGISTRY, get_backend
17+
from autoskillit.recipe._api import load_and_validate
18+
from autoskillit.recipe.io import all_validated_recipe_names
19+
from autoskillit.server.tools._auto_overrides import _backend_capability_overrides
20+
from autoskillit.workspace.skills import DefaultSkillResolver
21+
22+
pytestmark = [pytest.mark.layer("recipe"), pytest.mark.medium]
23+
24+
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
25+
26+
_ALL_RECIPE_NAMES = sorted(all_validated_recipe_names(_PROJECT_ROOT))
27+
_BACKEND_NAMES = sorted(BACKEND_REGISTRY.keys())
28+
29+
30+
# -- By-design unsupported combos (skip) ------------------------------------
31+
DECLARED_UNSUPPORTED: frozenset[tuple[str, str]] = frozenset()
32+
33+
UNSUPPORTED_REASONS: dict[tuple[str, str], dict[str, str]] = {}
34+
35+
_MATRIX_IDS: list[tuple[str, str]] = [
36+
(r, b) for r in _ALL_RECIPE_NAMES for b in _BACKEND_NAMES if (r, b) not in DECLARED_UNSUPPORTED
37+
]
38+
39+
40+
# -- Known-broken combos (xfail strict) -------------------------------------
41+
KNOWN_BROKEN: dict[tuple[str, str], str] = {
42+
("agent-eval", "claude-code"): (
43+
"tracking: #4069 -- violates all-dispatchable-stops-have-sentinel + dead-output"
44+
),
45+
("agent-eval", "codex"): (
46+
"tracking: #4069 -- violates all-dispatchable-stops-have-sentinel + dead-output"
47+
),
48+
("skill-eval", "claude-code"): (
49+
"tracking: #4069 -- violates all-dispatchable-stops-have-sentinel + dead-output"
50+
),
51+
("skill-eval", "codex"): (
52+
"tracking: #4069 -- violates all-dispatchable-stops-have-sentinel + dead-output"
53+
),
54+
}
55+
56+
_SKILL_RESOLVER = DefaultSkillResolver()
57+
58+
59+
def _apply_marks(matrix_ids: list[tuple[str, str]]) -> list[Any]:
60+
"""Wrap matrix tuples in pytest.param, attaching xfail marks for KNOWN_BROKEN."""
61+
params: list[Any] = []
62+
for r, b in matrix_ids:
63+
marks: list[Any] = []
64+
if (r, b) in KNOWN_BROKEN:
65+
marks.append(pytest.mark.xfail(strict=True, reason=KNOWN_BROKEN[(r, b)]))
66+
params.append(pytest.param(r, b, marks=marks, id=f"{r}/{b}"))
67+
return params
68+
69+
70+
@pytest.mark.parametrize("recipe_name,backend_name", _apply_marks(_MATRIX_IDS))
71+
def test_recipe_backend_matrix_cell(recipe_name: str, backend_name: str) -> None:
72+
backend = get_backend(backend_name)
73+
result = load_and_validate(
74+
recipe_name,
75+
project_dir=_PROJECT_ROOT,
76+
backend_name=backend_name,
77+
ingredient_overrides=_backend_capability_overrides(backend),
78+
lister=_SKILL_RESOLVER,
79+
)
80+
81+
assert result["valid"] is True, (
82+
f"Recipe '{recipe_name}' invalid on backend '{backend_name}': "
83+
+ "; ".join(
84+
f"[{s.get('rule')}] {s.get('message', '')[:80]}"
85+
for s in result.get("suggestions", [])
86+
if s.get("severity") == Severity.ERROR
87+
)
88+
)
89+
assert len(result.get("content", "")) > 0, (
90+
f"Recipe '{recipe_name}' on backend '{backend_name}' produced empty content"
91+
)
92+
93+
suggestions: list[dict[str, Any]] = result.get("suggestions", [])
94+
dangling = [
95+
s for s in suggestions if s.get("message", "").startswith("[post-prune] dangling route:")
96+
]
97+
assert not dangling, (
98+
f"Recipe '{recipe_name}' on backend '{backend_name}' has dangling routes: "
99+
+ "; ".join(s.get("message", "") for s in dangling)
100+
)
101+
102+
backend_compat_errors = [
103+
s
104+
for s in suggestions
105+
if s.get("rule") == "backend-incompatible-skill" and s.get("severity") == Severity.ERROR
106+
]
107+
assert not backend_compat_errors, (
108+
f"Recipe '{recipe_name}' on backend '{backend_name}' has "
109+
f"backend-incompatible-skill errors: "
110+
+ "; ".join(s.get("message", "") for s in backend_compat_errors)
111+
)
112+
113+
114+
def test_declared_unsupported_orphan_check() -> None:
115+
"""Every DECLARED_UNSUPPORTED entry must reference a live recipe and backend."""
116+
recipe_names = frozenset(_ALL_RECIPE_NAMES)
117+
backend_names = frozenset(BACKEND_REGISTRY.keys())
118+
for recipe_name, backend_name in DECLARED_UNSUPPORTED:
119+
assert recipe_name in recipe_names, (
120+
f"DECLARED_UNSUPPORTED entry ({recipe_name!r}, {backend_name!r}) "
121+
f"references unknown recipe {recipe_name!r}. "
122+
f"Valid: {sorted(recipe_names)}"
123+
)
124+
assert backend_name in backend_names, (
125+
f"DECLARED_UNSUPPORTED entry ({recipe_name!r}, {backend_name!r}) "
126+
f"references unknown backend {backend_name!r}. "
127+
f"Valid: {sorted(backend_names)}"
128+
)
129+
130+
131+
def test_matrix_collection_count() -> None:
132+
"""Matrix size = recipes x backends - declared unsupported."""
133+
expected = len(_ALL_RECIPE_NAMES) * len(_BACKEND_NAMES) - len(DECLARED_UNSUPPORTED)
134+
assert len(_MATRIX_IDS) == expected, (
135+
f"Matrix size mismatch: got {len(_MATRIX_IDS)}, "
136+
f"expected {len(_ALL_RECIPE_NAMES)} recipes x {len(_BACKEND_NAMES)} backends "
137+
f"- {len(DECLARED_UNSUPPORTED)} unsupported = {expected}"
138+
)

0 commit comments

Comments
 (0)