Skip to content

Commit 4cadec7

Browse files
authored
Implementation Plan: Produce the Complete Parametrized Conformance Test Class (#3991)
## Summary Create `tests/execution/backends/test_coding_agent_backend_conformance.py` containing `TestCodingAgentBackendConformance` — a parametrized test class inheriting `BackendContractBase`, exercising the `CodingAgentBackend` Protocol contract across all registered backends (`claude-code`, `codex`). The class covers core properties (8 tests), sub-protocol factories (6 tests), command-builder contracts (6 tests), `setup_session_dir` branching (1 test), `SessionLocator` P3 methods (3 tests), capability-gated command builders (2 tests), and inspector not-capable assertion (1 test) — 27 test methods total. ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260610-120819-718947/.autoskillit/temp/make-plan/t4_p4_a2_wp1_conformance_test_plan_2026-06-10_121600.md` Closes #3936 🤖 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 | 1.6k | 22.2k | 1.7M | 109.9k | 55 | 88.7k | 12m 43s | | verify* | sonnet | 1 | 124 | 12.9k | 784.5k | 64.6k | 38 | 43.6k | 5m 31s | | implement* | MiniMax-M3 | 1 | 1.2M | 5.8k | 0 | 0 | 58 | 0 | 2m 47s | | audit_impl* | sonnet | 1 | 44 | 14.6k | 166.2k | 41.8k | 13 | 36.8k | 4m 6s | | prepare_pr* | MiniMax-M3 | 1 | 203.2k | 1.8k | 0 | 0 | 12 | 0 | 47s | | compose_pr* | MiniMax-M3 | 1 | 213.6k | 1.7k | 0 | 0 | 15 | 0 | 40s | | **Total** | | | 1.6M | 59.1k | 2.7M | 109.9k | | 169.1k | 26m 34s | \* *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 | 195 | 0.0 | 0.0 | 29.7 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **195** | 13633.5 | 867.2 | 303.0 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 1.6k | 22.2k | 1.7M | 88.7k | 12m 43s | | sonnet | 2 | 168 | 27.5k | 950.7k | 80.4k | 9m 37s | | MiniMax-M3 | 3 | 1.6M | 9.3k | 0 | 0 | 4m 13s |
1 parent 3cfec31 commit 4cadec7

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

tests/execution/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,4 @@ Subprocess integration, headless session, process lifecycle, and session result
153153
| `test_codex_result_parser.py` | Tests for CodexResultParser |
154154
| `test_codex_config.py` | Tests for TOML read/write primitives, _is_autoskillit_registered, and ensure_codex_mcp_registered |
155155
| `test_cmd_builder.py` | CmdBuilder ordering invariant and CmdSpec origin tests |
156+
| `test_coding_agent_backend_conformance.py` | Parametrized conformance tests for all CodingAgentBackend implementations via BackendContractBase |
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from autoskillit.core import (
8+
BackendCapabilities,
9+
BackendConventions,
10+
CmdSpec,
11+
CodingAgentBackend,
12+
DirectInstall,
13+
EnvPolicy,
14+
ResultParser,
15+
SessionLocator,
16+
SkillSessionConfig,
17+
StreamParser,
18+
)
19+
from autoskillit.execution.backends import BACKEND_REGISTRY, get_backend
20+
21+
from .test_backend_contract_base import BackendContractBase
22+
23+
pytestmark = [pytest.mark.layer("execution"), pytest.mark.small]
24+
25+
26+
def make_backend(backend_name: str) -> CodingAgentBackend:
27+
return get_backend(backend_name)
28+
29+
30+
@pytest.mark.parametrize("backend_name", list(BACKEND_REGISTRY))
31+
class TestCodingAgentBackendConformance(BackendContractBase):
32+
@pytest.fixture(autouse=True)
33+
def _setup_backend(self, backend_name: str) -> None:
34+
self.backend = make_backend(backend_name)
35+
36+
def make_backend(self) -> CodingAgentBackend:
37+
return self.backend
38+
39+
# --- Group 1: Core Properties ---
40+
41+
def test_isinstance_coding_agent_backend(self) -> None:
42+
assert isinstance(self.backend, CodingAgentBackend)
43+
44+
def test_name_is_non_empty_string(self) -> None:
45+
assert isinstance(self.backend.name, str)
46+
assert len(self.backend.name) > 0
47+
48+
def test_capabilities_returns_backend_capabilities(self) -> None:
49+
assert isinstance(self.backend.capabilities, BackendCapabilities)
50+
51+
def test_conventions_returns_backend_conventions(self) -> None:
52+
assert isinstance(self.backend.conventions, BackendConventions)
53+
54+
def test_write_tool_names_returns_nonempty_frozenset(self) -> None:
55+
result = self.backend.write_tool_names()
56+
assert isinstance(result, frozenset)
57+
assert len(result) >= 1
58+
59+
def test_binary_name_returns_non_empty_string(self) -> None:
60+
result = self.backend.binary_name()
61+
assert isinstance(result, str)
62+
assert len(result) > 0
63+
64+
def test_translate_model_returns_string(self) -> None:
65+
result = self.backend.translate_model("sonnet")
66+
assert isinstance(result, str)
67+
68+
def test_model_config_overrides_returns_tuple(self) -> None:
69+
result = self.backend.model_config_overrides("sonnet")
70+
assert isinstance(result, tuple)
71+
72+
# --- Group 2: Sub-protocol Factories ---
73+
74+
def test_stream_parser_no_marker_returns_stream_parser(self) -> None:
75+
result = self.backend.stream_parser()
76+
assert isinstance(result, StreamParser)
77+
78+
def test_stream_parser_with_marker_stores_completion_marker(self) -> None:
79+
parser = self.backend.stream_parser(completion_marker="%%DONE%%")
80+
assert parser.completion_marker == "%%DONE%%" # type: ignore[attr-defined]
81+
82+
def test_result_parser_returns_result_parser(self) -> None:
83+
result = self.backend.result_parser()
84+
assert isinstance(result, ResultParser)
85+
86+
def test_env_policy_returns_env_policy(self) -> None:
87+
result = self.backend.env_policy()
88+
assert isinstance(result, EnvPolicy)
89+
90+
def test_session_locator_returns_session_locator(self) -> None:
91+
result = self.backend.session_locator()
92+
assert isinstance(result, SessionLocator)
93+
94+
def test_session_locator_locate_empty_string_returns_none(self) -> None:
95+
locator = self.backend.session_locator()
96+
assert locator.locate_session("") is None
97+
98+
# --- Group 3: Command-builder Contracts ---
99+
100+
def test_build_cmd_returns_cmd_spec(self) -> None:
101+
result = self.backend.build_cmd(skill_command="do stuff", cwd="/tmp")
102+
assert isinstance(result, CmdSpec)
103+
assert isinstance(result.cmd, tuple)
104+
105+
def test_build_skill_session_cmd_with_default_config_returns_cmd_spec(self) -> None:
106+
result = self.backend.build_skill_session_cmd(
107+
skill_command="/test-skill", cwd="/work", config=SkillSessionConfig()
108+
)
109+
assert isinstance(result, CmdSpec)
110+
assert isinstance(result.cmd, tuple)
111+
112+
def test_build_interactive_cmd_returns_cmd_spec(self) -> None:
113+
result = self.backend.build_interactive_cmd()
114+
assert isinstance(result, CmdSpec)
115+
assert isinstance(result.cmd, tuple)
116+
117+
def test_validate_session_layout_returns_list(self, tmp_path: Path) -> None:
118+
result = self.backend.validate_session_layout(tmp_path)
119+
assert isinstance(result, list)
120+
121+
def test_validate_skill_content_returns_list(self) -> None:
122+
result = self.backend.validate_skill_content("")
123+
assert isinstance(result, list)
124+
125+
def test_list_plugins_returns_list(self) -> None:
126+
result = self.backend.list_plugins()
127+
assert isinstance(result, list)
128+
129+
# --- Group 4: Setup + SessionLocator P3 ---
130+
131+
def test_setup_session_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
132+
if self.backend.name == "claude-code":
133+
self.backend.setup_session_dir(tmp_path)
134+
elif self.backend.name == "codex":
135+
fake_home = tmp_path / "fake_home"
136+
fake_home.mkdir()
137+
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
138+
fake_log_dir = tmp_path / "fake_logs"
139+
fake_log_dir.mkdir()
140+
monkeypatch.setattr(
141+
"autoskillit.execution.backends.codex.default_log_dir",
142+
lambda: fake_log_dir,
143+
)
144+
session_dir = tmp_path / "session"
145+
session_dir.mkdir()
146+
with pytest.raises(FileNotFoundError):
147+
self.backend.setup_session_dir(session_dir)
148+
else:
149+
# Intentional sentinel: when a new backend is added to BACKEND_REGISTRY,
150+
# add an elif branch above with explicit setup_session_dir coverage rather
151+
# than removing this guard.
152+
pytest.fail(
153+
f"test_setup_session_dir has no coverage for backend {self.backend.name!r}"
154+
)
155+
156+
def test_project_log_dir_returns_absolute_path(self) -> None:
157+
locator = self.backend.session_locator()
158+
result = locator.project_log_dir("/tmp/fake-cwd")
159+
assert isinstance(result, Path)
160+
assert result.is_absolute()
161+
162+
def test_session_log_path_returns_none_for_empty_id(self) -> None:
163+
locator = self.backend.session_locator()
164+
result = locator.session_log_path("/tmp/fake-cwd", "")
165+
assert result is None
166+
167+
def test_session_log_path_returns_none_for_no_session_prefix(self) -> None:
168+
locator = self.backend.session_locator()
169+
result = locator.session_log_path("/tmp/fake-cwd", "no_session_abc")
170+
assert result is None
171+
172+
# --- Group 5: Capability-Gated Command Builders ---
173+
174+
def test_build_resume_cmd_when_capable(self) -> None:
175+
self._require_capability("session_resume_capable")
176+
result = self.backend.build_resume_cmd(
177+
resume_session_id="test-session-id", prompt="test prompt"
178+
)
179+
assert isinstance(result, CmdSpec)
180+
assert len(result.cmd) > 0
181+
182+
def test_build_food_truck_cmd_when_capable(self) -> None:
183+
self._require_capability("food_truck_capable")
184+
result = self.backend.build_food_truck_cmd(
185+
orchestrator_prompt="x",
186+
plugin_source=DirectInstall(plugin_dir=Path("/tmp")),
187+
cwd="/tmp",
188+
completion_marker="%%X%%",
189+
)
190+
assert isinstance(result, CmdSpec)
191+
192+
def test_build_inspector_cmd_raises_when_not_capable(self) -> None:
193+
if self.backend.capabilities.inspector_capable:
194+
pytest.skip("backend is inspector_capable — not testing the not-capable path")
195+
with pytest.raises(RuntimeError) as exc_info:
196+
self.backend.build_inspector_cmd("test prompt")
197+
assert "not yet implemented" in str(exc_info.value)

0 commit comments

Comments
 (0)