Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/`:
Expand Down
7 changes: 5 additions & 2 deletions tests/migration/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
)
from autoskillit.migration.loader import MigrationChange, MigrationNote

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion tests/recipe/test_bundled_recipe_hidden_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"remediation",
"implementation-groups",
"merge-prs",
"smoke-test",
]


Expand Down
21 changes: 15 additions & 6 deletions tests/recipe/test_bundled_recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from pathlib import Path

import pytest
import yaml

Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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'"
Expand Down
41 changes: 36 additions & 5 deletions tests/recipe/test_smoke_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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:
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.

[warning] arch: test_smoke_test_source_is_project calls list_recipes() after smoke_project fixture chdirs. If list_recipes() caches the CWD at import time or uses a different resolution mechanism, this test may produce a false positive. The test has no assertion that list_recipes actually reads from tmp_path, not the real CWD or a module-level cache.

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.

Investigated — this is intentional. list_recipes() calls Path.cwd() at runtime (tools_recipe.py:50: tool_ctx.recipes.list_all(Path.cwd())), so no module-level caching occurs. The smoke_project fixture's monkeypatch.chdir(tmp_path) correctly sets the CWD used when list_recipes() is called. This pattern is already validated bidirectionally by test_smoke_test_invisible_from_external_project (lines 426-435) which confirms CWD-awareness in both directions.

"""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."""
Expand Down
2 changes: 1 addition & 1 deletion tests/server/test_tools_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading