"""Step definitions for TDD Issue #980 — skill add cross-process persistence. These steps capture bug #980 by exercising the *real* CLI code path across independent process-like invocations. The existing persistence tests (``skill_add_persist.feature``) verify round-trip within the same Python process — they create two ``SkillService`` instances sharing the same in-memory SQLAlchemy session factory. That approach cannot detect the cross-process regression introduced by PR #640. This test: 1. Points the CLI at the per-scenario SQLite database (already set up by ``before_scenario`` with all migrations applied). 2. Runs ``agents skill add --config --format json`` via the CLI app with a fresh DI container (simulating process 1). 3. Resets the DI container and runs ``agents skill list --format json`` (simulating process 2) — the SkillService is rebuilt from scratch. 4. Asserts the skill appears in the listing. The ``@tdd_expected_fail`` tag on the feature inverts the assertion failure to a CI pass while the bug is unfixed. The root cause is that ``_build_skill_service`` creates a standalone engine that connects to the database but the ``skills`` table does not exist in databases initialised by ``agents init`` (the Alembic migration for skills was never added, or the skill service bypasses migration-managed tables). """ from __future__ import annotations import contextlib import os import tempfile from behave import given, then, when from behave.runner import Context from typer.testing import CliRunner from cleveragents.application.container import reset_container from cleveragents.cli.main import app as root_app runner = CliRunner() _SKILL_YAML = """\ name: local/tdd-cross-process description: "TDD cross-process persistence test skill" tools: - name: builtin/read_file """ _SKILL_NAME = "local/tdd-cross-process" # --------------------------------------------------------------------------- # Given # --------------------------------------------------------------------------- @given("a cross-process skill persistence environment") def step_cross_process_env(context: Context) -> None: """Write a skill YAML config file for use in subsequent steps. The per-scenario database is already set up by ``before_scenario`` in ``environment.py`` (with migrations applied via the template-DB fast path). We only need to create the skill YAML and ensure the DI container starts from a clean state. """ # Write skill YAML config to a temp file fd, config_path = tempfile.mkstemp(suffix=".yaml", prefix="tdd_skill_") with os.fdopen(fd, "w") as fh: fh.write(_SKILL_YAML) context.tdd_skill_config_path = config_path # Register cleanup for the temp config file def _cleanup() -> None: with contextlib.suppress(OSError): os.unlink(config_path) reset_container() context.add_cleanup(_cleanup) # Start with a clean container reset_container() # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- @when("I add a skill via a CLI subprocess") def step_add_skill_subprocess(context: Context) -> None: """Run ``agents skill add --config --format json``. Simulates CLI process 1. After this call, the DI container and its in-memory SkillService hold the newly added skill. The skill *should* also be persisted to the SQLite database so that process 2 can see it. """ result = runner.invoke( root_app, [ "skill", "add", "--config", context.tdd_skill_config_path, "--format", "json", ], ) context.tdd_skill_add_output = result.output context.tdd_skill_add_exit_code = result.exit_code @when("I list skills via a separate CLI subprocess") def step_list_skills_subprocess(context: Context) -> None: """Run ``agents skill list --format json`` with a fresh DI container. The DI container is reset before the call to simulate a completely independent CLI process — this is the cross-process boundary that exposes bug #980. The new SkillService instance must load skills from the database to find the skill added in the previous step. """ # Simulate cross-process boundary: fresh container = fresh SkillService reset_container() result = runner.invoke( root_app, ["skill", "list", "--format", "json"], ) context.tdd_skill_list_output = result.output context.tdd_skill_list_exit_code = result.exit_code @when("I show the skill via a separate CLI subprocess") def step_show_skill_subprocess(context: Context) -> None: """Run ``agents skill show --format json`` with a fresh container. The DI container is reset before the call to simulate a completely independent CLI process. """ # Simulate cross-process boundary reset_container() result = runner.invoke( root_app, ["skill", "show", _SKILL_NAME, "--format", "json"], ) context.tdd_skill_show_output = result.output context.tdd_skill_show_exit_code = result.exit_code # --------------------------------------------------------------------------- # Then # --------------------------------------------------------------------------- @then("the cross-process skill list should contain the added skill") def step_assert_skill_in_list(context: Context) -> None: """Assert that the skill added in one process is visible in another. Checks the output of ``skill list`` for the skill name. This assertion will FAIL while bug #980 is present because the second process creates a fresh SkillService that does not find the skill in the database (the ``skills`` table is missing or was never populated due to the DB-fallback in ``_build_skill_service``). The ``@tdd_expected_fail`` tag inverts this failure to a CI pass. """ list_output = context.tdd_skill_list_output assert _SKILL_NAME in list_output, ( f"Expected skill '{_SKILL_NAME}' in cross-process skill list output " f"but it was not found.\n" f"Add exit code: {context.tdd_skill_add_exit_code}\n" f"Add output:\n{context.tdd_skill_add_output}\n" f"List exit code: {context.tdd_skill_list_exit_code}\n" f"List output:\n{list_output}" ) @then("the cross-process skill show output should contain the skill name") def step_assert_skill_show_contains_name(context: Context) -> None: """Assert that ``skill show`` in a separate process finds the skill. This assertion will FAIL while bug #980 is present because the second process cannot find the skill in its fresh service instance. """ show_output = context.tdd_skill_show_output show_exit = context.tdd_skill_show_exit_code assert show_exit == 0 and _SKILL_NAME in show_output, ( f"Expected skill '{_SKILL_NAME}' visible via cross-process " f"'skill show' but it was not found.\n" f"Show exit code: {show_exit}\n" f"Show output:\n{show_output}\n" f"Add exit code: {context.tdd_skill_add_exit_code}\n" f"Add output:\n{context.tdd_skill_add_output}" )