Skip to content
Merged
2 changes: 2 additions & 0 deletions src/autoskillit/skills_extended/resolve-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ classified `REJECT` with `category: "arch_violation"`.
| Boot step symmetry | `test_boot_step_symmetry.py` | Both boot functions must call all required boot steps in the right order |
| BackendCapabilities consumed | `test_capability_consumption.py` | Every `BackendCapabilities` field must have a production consumer — unused capability fields are prohibited |
| BackendCapabilities documented | `test_capability_docstrings.py` | `BackendCapabilities` class and every field must have docstrings |
| Env var symmetry | `test_env_symmetry.py` | `build_skill_session_cmd` and `build_food_truck_cmd` must both set required base env vars; `AGENT_BACKEND_ENV_VAR` must appear in food_truck |
| No NotImplementedError in backends | `test_no_not_implemented.py` | Registered backend classes must not raise `NotImplementedError` — `CodingAgentBackend` is a Protocol, not an ABC |
| Channel B timeout floor | `test_channel_b_timeout_guard.py` | Channel B calls with `timeout` below `TimeoutTier.CHANNEL_B` minimum |
| Clone network timeouts | `test_clone_timeouts.py` | `subprocess.run()` with git network subcommands (clone/fetch/pull/push/ls-remote) in `clone.py` without `timeout=` |
| Dispatch timeout resolver | `test_dispatch_timeout_guard.py` | `_run_dispatch` using hardcoded timeout instead of `resolve_dispatch_timeout()` |
Expand Down
2 changes: 2 additions & 0 deletions tests/arch/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ AST enforcement, sub-package layer contracts, and architectural invariant tests.
| `test_backend_protocol_completeness.py` | Protocol completeness tests for CodingAgentBackend command builders |
| `test_audit_feature_gates_skill.py` | Structural integrity tests for the audit-feature-gates skill |
| `test_eval_agent_skill.py` | Structural integrity tests for the eval-agent skill |
| `test_env_symmetry.py` | Architectural invariant: skill and food-truck builders must set the same required base env vars |
| `test_boot_step_symmetry.py` | AST guard: both boot functions (_fleet_auto_gate_boot, _food_truck_auto_gate_boot) must call sweep_stale_dispatch_labels |
| `test_bundled_recipes_split.py` | Enforcement: test_bundled_recipes.py split structure guard |
| `test_cascade_map_guard.py` | REQ-GUARD-001..003, 005: CI guard validating cascade maps against AST-derived reverse import graph |
Expand Down Expand Up @@ -54,6 +55,7 @@ AST enforcement, sub-package layer contracts, and architectural invariant tests.
| `test_no_error_dict_return.py` | AST guard: load_and_validate must not return dicts with 'error' key — errors flow via exceptions |
| `test_flush_no_rid_guard.py` | AST guard: no requestId truthiness guard in flush_session_log turn extraction loop |
| `test_no_inline_jsonl_request_id_dedup.py` | AST guard: no inline requestId dedup in session_log.py or tool_sequence_analysis.py |
| `test_no_not_implemented.py` | Architectural invariant: no registered backend may raise NotImplementedError |
| `test_resolve_turn_id_guard.py` | AST guard: no direct .get("requestId") calls outside _resolve_turn_id() in tool_sequence_analysis.py |
| `test_protocol_names.py` | T5-T6: Protocol naming and DefaultSkillResolver export smoke tests |
| `test_provider_profile_contract.py` | Tier 3 contract: _resolve_provider_profile must never return step_name as profile |
Expand Down
68 changes: 68 additions & 0 deletions tests/arch/test_env_symmetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Architectural invariant: skill and food-truck builders must set the same required base env vars.""" # noqa: E501

from pathlib import Path

import pytest

pytestmark = [pytest.mark.layer("arch"), pytest.mark.small]

_REQUIRED_IN_BOTH: frozenset[str] = frozenset(
{
"AUTOSKILLIT_HEADLESS",
"AUTOSKILLIT_SESSION_TYPE",
"MAX_MCP_OUTPUT_TOKENS",
}
)


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
Comment thread
Trecek marked this conversation as resolved.
monkeypatch.delenv("AUTOSKILLIT_CAMPAIGN_ID", raising=False)
monkeypatch.delenv("AUTOSKILLIT_KITCHEN_SESSION_ID", raising=False)
monkeypatch.delenv("AUTOSKILLIT_AGENT_BACKEND", raising=False)


def test_skill_and_food_truck_share_required_env_vars() -> None:
"""build_skill_session_cmd and build_food_truck_cmd must both set the required base env vars.""" # noqa: E501
from autoskillit.core.types._type_plugin_source import DirectInstall
from autoskillit.execution.backends import BACKEND_REGISTRY

assert BACKEND_REGISTRY, "BACKEND_REGISTRY is empty — test provides no coverage"
for name, cls in BACKEND_REGISTRY.items():
backend = cls()
skill_spec = backend.build_skill_session_cmd(
"/autoskillit:investigate", "/repo", completion_marker="DONE"
)
food_truck_spec = backend.build_food_truck_cmd(
orchestrator_prompt="test prompt",
plugin_source=DirectInstall(plugin_dir=Path("/plugins")),
cwd="/repo",
completion_marker="DONE",
)
for var in _REQUIRED_IN_BOTH:
assert var in skill_spec.env, f"{name}: {var} missing from build_skill_session_cmd env"
assert var in food_truck_spec.env, (
f"{name}: {var} missing from build_food_truck_cmd env"
)


def test_agent_backend_env_var_in_food_truck(monkeypatch: pytest.MonkeyPatch) -> None:
"""AUTOSKILLIT_AGENT_BACKEND must appear in build_food_truck_cmd env for every backend."""
from autoskillit.core.types._type_plugin_source import DirectInstall
from autoskillit.execution.backends import BACKEND_REGISTRY

# Ensure clean environment - remove any residual AGENT_BACKEND_ENV_VAR from host
monkeypatch.delenv("AUTOSKILLIT_AGENT_BACKEND", raising=False)

assert BACKEND_REGISTRY, "BACKEND_REGISTRY is empty — test provides no coverage"
for name, cls in BACKEND_REGISTRY.items():
backend = cls()
food_truck_spec = backend.build_food_truck_cmd(
orchestrator_prompt="test prompt",
plugin_source=DirectInstall(plugin_dir=Path("/plugins")),
cwd="/repo",
completion_marker="DONE",
)
assert "AUTOSKILLIT_AGENT_BACKEND" in food_truck_spec.env, (
f"{name}: AUTOSKILLIT_AGENT_BACKEND missing from build_food_truck_cmd env"
)
37 changes: 37 additions & 0 deletions tests/arch/test_no_not_implemented.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Architectural invariant: no registered backend may raise NotImplementedError."""

import ast
import inspect

import pytest

pytestmark = [pytest.mark.layer("arch"), pytest.mark.small]


def test_no_not_implemented_error_in_registered_backends() -> None:
"""No method on any BACKEND_REGISTRY class should raise NotImplementedError.

CodingAgentBackend is a typing.Protocol, not an ABC — there are no legitimate
abstract stubs. Any raise NotImplementedError in a registered backend is a bug.
"""
from autoskillit.execution.backends import BACKEND_REGISTRY

violations: list[str] = []
for name, cls in BACKEND_REGISTRY.items():
source = inspect.getsource(cls)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation from local review round 0:

[warning] defense: inspect.getsource(cls) can raise OSError (source unavailable, C extension, etc.) with no error context identifying which backend triggered it. Wrap in try/except and include name in the error to preserve context.

Evidence: Marginal improvement in test diagnostics for arch test code. Human judgment needed on whether defensive wrapping in test code is warranted.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation — flagged for design decision. The question is whether defensive try/except with backend name context is warranted in test code for inspect.getsource(). Human judgment needed on complexity vs. diagnostic value tradeoff.

tree = ast.parse(source)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation from local review round 0:

[warning] defense: ast.parse(source) can raise SyntaxError with no indication of which backend's source failed to parse. Wrap in try/except and include name in the error message.

Evidence: SyntaxError in well-maintained Python backends is unlikely but possible. Adding try/except with name improves triage. Human decision on whether worth adding complexity to test code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation — flagged for design decision. SyntaxError wrapping in ast.parse() for well-maintained Python backends is unlikely but possible. Human decision on whether the added complexity is worth the diagnostic improvement.

for node in ast.walk(tree):
if not isinstance(node, ast.Raise):
continue
exc = node.exc
if (
Comment thread
Trecek marked this conversation as resolved.
isinstance(exc, ast.Call)
and isinstance(exc.func, ast.Name)
and exc.func.id == "NotImplementedError"
) or (isinstance(exc, ast.Name) and exc.id == "NotImplementedError"):
violations.append(f"{name}:{node.lineno}")

assert not violations, (
f"Registered backends must not raise NotImplementedError "
f"(CodingAgentBackend is a Protocol, not an ABC): {violations}"
)
20 changes: 20 additions & 0 deletions tests/server/test_server_init_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ async def test_redisable_subsets_hides_kitchen_core(self) -> None:

mock_ctx.disable_components.assert_called_once_with(tags={"kitchen-core"})

@pytest.mark.anyio
async def test_tool_list_changes_after_enable_within_session(self) -> None:
"""Tool list visible to a client changes when kitchen tags are enabled mid-session.""" # noqa: E501
from fastmcp.client import Client

from autoskillit.pipeline.gate import GATED_TOOLS
from autoskillit.server import mcp

async with Client(mcp) as client:
tools_before = {t.name for t in await client.list_tools()}
Comment thread
Trecek marked this conversation as resolved.
assert not (tools_before & GATED_TOOLS), "Kitchen tools should be hidden before enable"

mcp.enable(tags={"kitchen"})

tools_after = {t.name for t in await client.list_tools()}
new_tools = tools_after - tools_before
assert new_tools & GATED_TOOLS, (
"Enabling kitchen tags should make kitchen-gated tools visible"
)


class TestGatedToolAccess:
"""Prompt-gated tool access: tools disabled by default, user-only activation."""
Expand Down
Loading