diff --git a/src/autoskillit/recipes/contracts/smoke-test.yaml b/.autoskillit/recipes/contracts/smoke-test.yaml similarity index 100% rename from src/autoskillit/recipes/contracts/smoke-test.yaml rename to .autoskillit/recipes/contracts/smoke-test.yaml diff --git a/src/autoskillit/recipes/diagrams/smoke-test.md b/.autoskillit/recipes/diagrams/smoke-test.md similarity index 100% rename from src/autoskillit/recipes/diagrams/smoke-test.md rename to .autoskillit/recipes/diagrams/smoke-test.md diff --git a/src/autoskillit/recipes/smoke-test.yaml b/.autoskillit/recipes/smoke-test.yaml similarity index 100% rename from src/autoskillit/recipes/smoke-test.yaml rename to .autoskillit/recipes/smoke-test.yaml diff --git a/docs/recipes.md b/docs/recipes.md index cf845a09..7e48ee0d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -4,7 +4,7 @@ Recipes are YAML pipeline definitions that automate multi-step workflows. Each r ## Bundled Recipes -AutoSkillit ships with 6 recipes: +AutoSkillit ships with 5 recipes: ### implementation @@ -98,12 +98,6 @@ Two-phase technical research recipe. Phase 1 scopes a research question and open **Requires pack:** `research` (pack members: `scope`, `plan-experiment`, `implement-experiment`, `run-experiment`, `write-report`) -### smoke-test - -Integration self-test of the orchestration engine. Creates a local bare repo and exercises the full pipeline with stub skills. - -**When to use:** Internal testing only. Verifies that step routing, tool dispatch, capture/context threading, retry logic, and merge mechanics all work correctly. - ## Project Recipes You can create custom recipes in `.autoskillit/recipes/`: diff --git a/tests/migration/test_engine.py b/tests/migration/test_engine.py index 758f5573..d4df822a 100644 --- a/tests/migration/test_engine.py +++ b/tests/migration/test_engine.py @@ -24,6 +24,8 @@ ) from autoskillit.migration.loader import MigrationChange, MigrationNote +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -292,8 +294,9 @@ async def test_contract_adapter_migrate_regenerates_card_on_disk(self, tmp_path: contracts_dir = recipes_dir / "contracts" contracts_dir.mkdir(parents=True) - # Copy a real bundled recipe so generate_recipe_card has valid input - src_recipe = pkg_root() / "recipes" / "smoke-test.yaml" + # Copy the project-local smoke-test recipe so generate_recipe_card has valid input + src_recipe = PROJECT_ROOT / ".autoskillit" / "recipes" / "smoke-test.yaml" + assert src_recipe.exists(), f"smoke-test source missing: {src_recipe}" shutil.copy2(src_recipe, recipes_dir / "smoke-test.yaml") contract_path = contracts_dir / "smoke-test.yaml" diff --git a/tests/recipe/test_bundled_recipe_hidden_policy.py b/tests/recipe/test_bundled_recipe_hidden_policy.py index 9a663c88..9bca70c6 100644 --- a/tests/recipe/test_bundled_recipe_hidden_policy.py +++ b/tests/recipe/test_bundled_recipe_hidden_policy.py @@ -11,7 +11,6 @@ "remediation", "implementation-groups", "merge-prs", - "smoke-test", ] diff --git a/tests/recipe/test_bundled_recipes.py b/tests/recipe/test_bundled_recipes.py index b4ff87a9..c8f8dde0 100644 --- a/tests/recipe/test_bundled_recipes.py +++ b/tests/recipe/test_bundled_recipes.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest import yaml @@ -10,6 +12,9 @@ from autoskillit.recipe.io import builtin_recipes_dir, load_recipe from autoskillit.recipe.validator import analyze_dataflow, run_semantic_rules +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +SMOKE_RECIPE = PROJECT_ROOT / ".autoskillit" / "recipes" / "smoke-test.yaml" + def _assert_ci_conflict_fix_on_context_limit(recipe) -> None: """Shared assertion: ci_conflict_fix must abort via release_issue_failure on context limit.""" @@ -1132,8 +1137,7 @@ class TestSmokeTestStructure: @pytest.fixture() def smoke_yaml(self) -> dict: - recipe_path = builtin_recipes_dir() / "smoke-test.yaml" - return yaml.safe_load(recipe_path.read_text()) + return yaml.safe_load(SMOKE_RECIPE.read_text()) # T_ST1 def test_create_branch_is_run_cmd(self, smoke_yaml: dict) -> None: @@ -1247,8 +1251,13 @@ def test_bundled_recipes_diagrams_dir_exists() -> None: def test_all_predicate_steps_have_on_failure() -> None: """Every tool/python step with on_result.conditions must declare on_failure.""" - for recipe_name in ["implementation", "remediation", "smoke-test"]: - recipe = load_recipe(builtin_recipes_dir() / f"{recipe_name}.yaml") + paths = { + "implementation": builtin_recipes_dir() / "implementation.yaml", + "remediation": builtin_recipes_dir() / "remediation.yaml", + "smoke-test": SMOKE_RECIPE, + } + for recipe_name, recipe_path in paths.items(): + recipe = load_recipe(recipe_path) for step_name, step in recipe.steps.items(): is_tool = step.tool is not None or step.python is not None if is_tool and step.on_result and step.on_result.conditions: @@ -1267,7 +1276,7 @@ def test_audit_impl_on_failure_routes_to_escalation() -> None: def test_smoke_check_summary_has_error_escalation() -> None: """check_summary must have a result.error condition routing to a non-done step.""" - recipe = load_recipe(builtin_recipes_dir() / "smoke-test.yaml") + recipe = load_recipe(SMOKE_RECIPE) step = recipe.steps["check_summary"] error_routes = [ c.route @@ -1600,7 +1609,7 @@ def test_recipe_base_branch_auto_detects(self, recipe_name: str) -> None: def test_smoke_test_base_branch_remains_main(self) -> None: """smoke-test.yaml must keep base_branch default 'main' — isolated scratch repo context.""" - recipe = load_recipe(builtin_recipes_dir() / "smoke-test.yaml") + recipe = load_recipe(SMOKE_RECIPE) assert recipe.ingredients["base_branch"].default == "main", ( "smoke-test.yaml creates a fresh git repo initialized with 'main' — " "its base_branch default must stay 'main'" diff --git a/tests/recipe/test_smoke_pipeline.py b/tests/recipe/test_smoke_pipeline.py index 9a16ee92..89225f9b 100644 --- a/tests/recipe/test_smoke_pipeline.py +++ b/tests/recipe/test_smoke_pipeline.py @@ -36,7 +36,7 @@ test_check.__test__ = False # type: ignore[attr-defined] PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent -SMOKE_SCRIPT = builtin_recipes_dir() / "smoke-test.yaml" +SMOKE_SCRIPT = PROJECT_ROOT / ".autoskillit" / "recipes" / "smoke-test.yaml" _TOOL_MAP = { "run_cmd": run_cmd, @@ -194,7 +194,7 @@ def _is_success(self, step_def: dict, result: dict) -> bool: def smoke_recipe(): from autoskillit.recipe.io import load_recipe as _load_recipe - return _load_recipe(builtin_recipes_dir() / "smoke-test.yaml") + return _load_recipe(SMOKE_SCRIPT) @pytest.fixture() @@ -204,11 +204,16 @@ def smoke_script_path() -> Path: @pytest.fixture() def smoke_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Create a temp project dir for smoke tests. + """Create a temp project dir with smoke-test as a project-local recipe. - Bundled recipes (including smoke-test) are discovered via recipe_parser, - so no recipe files need to be copied into the project dir. + smoke-test is a project-local recipe (not bundled), so it must be copied + into the temp dir's .autoskillit/recipes/ for discovery via list_recipes(). """ + import shutil + + recipes_dir = tmp_path / ".autoskillit" / "recipes" + recipes_dir.mkdir(parents=True) + shutil.copy2(SMOKE_SCRIPT, recipes_dir / "smoke-test.yaml") monkeypatch.chdir(tmp_path) return tmp_path @@ -404,6 +409,32 @@ async def test_assess_step_references_bug_report(self) -> None: assess_cmd = pipeline["steps"]["assess"]["with"]["skill_command"] assert "bug_report.json" in assess_cmd + def test_smoke_test_not_in_bundled_dir(self) -> None: + """smoke-test.yaml must not exist in the bundled recipes directory.""" + assert not (builtin_recipes_dir() / "smoke-test.yaml").exists() + + def test_smoke_test_exists_in_project_local(self) -> None: + """smoke-test.yaml must exist in the project-local recipes directory.""" + assert SMOKE_SCRIPT.exists(), f"Expected smoke-test at {SMOKE_SCRIPT}" + + async def test_smoke_test_source_is_project(self, smoke_project: Path) -> None: + """smoke-test must be listed with source PROJECT, not BUILTIN.""" + result = json.loads(await list_recipes()) + smoke = next((r for r in result["recipes"] if r["name"] == "smoke-test"), None) + assert smoke is not None, "smoke-test not found in list_recipes output" + assert smoke["source"] == "project" + + async def test_smoke_test_invisible_from_external_project( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """smoke-test must NOT appear in list_recipes from a project without it.""" + bare_dir = tmp_path / "external" + bare_dir.mkdir() + monkeypatch.chdir(bare_dir) + result = json.loads(await list_recipes()) + names = [r["name"] for r in result["recipes"]] + assert "smoke-test" not in names + def test_smoke_commit_dirty_step_exists(smoke_recipe) -> None: """commit_dirty step must exist and route back to merge.""" diff --git a/tests/server/test_tools_recipe.py b/tests/server/test_tools_recipe.py index 8fc33fc7..8bdc10f0 100644 --- a/tests/server/test_tools_recipe.py +++ b/tests/server/test_tools_recipe.py @@ -244,7 +244,7 @@ async def test_list_recipes_includes_builtins_with_empty_project_dir( names = {r["name"] for r in result["recipes"]} assert "implementation" in names assert "remediation" in names - assert "smoke-test" in names + assert "smoke-test" not in names # SS9 @pytest.mark.anyio