Files
temp/features/steps/tdd_skill_add_regression_steps.py
hurui200320 1878998b7a refactor(testing): rename tdd_bug/tdd_bug_N tags to tdd_issue/tdd_issue_N
Rename the TDD tag system from tdd_bug/tdd_bug_<N> to tdd_issue/tdd_issue_<N>
across the entire codebase. The tdd_expected_fail tag is unchanged.

The TDD expected-failure workflow is not limited to bug fixes — it applies
equally to any issue type (features, tasks, refactors). The _bug suffix was
misleading and narrowed the perceived scope. The new _issue suffix accurately
reflects that the TDD tagging system applies to any Forgejo issue.

Changes span 92 files:
- features/environment.py: validate_tdd_tags(), should_invert_result(), and
  apply_tdd_inversion() updated — regex, variables, error messages
- robot/tdd_expected_fail_listener.py: _validate_tdd_tags(), _should_invert_result(),
  start_test(), end_test() updated consistently
- 33 Behave .feature files: all @tdd_bug/@tdd_bug_<N> tags renamed
- 29 Robot .robot files: all tdd_bug/tdd_bug_<N> tags renamed
- 3 Robot fixture files renamed (tdd_bug_alone, tdd_missing_tdd_bug,
  tdd_expected_fail_missing_bug_n) with content and references updated
- Tag validation tests and helpers updated (function names, command dispatch
  keys, output strings, fixture references)
- CONTRIBUTING.md: section renamed from 'TDD Bug Test Tags' to
  'TDD Issue Test Tags', all tag references and examples updated
- noxfile.py: comment references updated
- Step definition files, mock helpers, and benchmark files: docstring
  references updated

ISSUES CLOSED: #965
2026-03-27 05:58:35 +00:00

200 lines
7.1 KiB
Python

"""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 <file> --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 <file> --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 <name> --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}"
)