Files
temp/features/steps/tdd_checkpoint_real_rollback_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

184 lines
6.7 KiB
Python

"""Step definitions for TDD Issue #822 — checkpoint rollback is simulated.
These steps exercise ``CheckpointService.rollback_to_checkpoint()`` against
a real git repository to prove that the method does **not** execute a
``git reset --hard`` and therefore leaves the filesystem unchanged.
On ``master`` (before the fix), ``rollback_to_checkpoint()`` constructs a
``RollbackResult`` with the checkpoint metadata but skips the actual git
operation. Files modified after the checkpoint remain modified after
rollback, which is the bug.
The feature is tagged ``@tdd_expected_fail`` so the assertion failure
(proving the bug) is inverted to a pass by the Behave environment hook.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.checkpoint_service import CheckpointService
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_INITIAL_CONTENT = "initial content\n"
_MODIFIED_CONTENT = "modified after checkpoint\n"
_TRACKED_FILENAME = "tracked.txt"
_NEW_FILENAME = "extra.txt"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run_git(args: list[str], cwd: str) -> subprocess.CompletedProcess[str]:
"""Run a git command and return the completed process."""
return subprocess.run(
["git", *args],
cwd=cwd,
capture_output=True,
text=True,
check=True,
timeout=30,
)
def _get_head_sha(cwd: str) -> str:
"""Return the HEAD commit SHA of a git repository."""
result = _run_git(["rev-parse", "HEAD"], cwd=cwd)
return result.stdout.strip()
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a temporary git workspace with an initial committed file")
def step_create_workspace(context: Context) -> None:
"""Create a temp directory with a git repo containing a tracked file."""
tmpdir = tempfile.mkdtemp(prefix="tdd_checkpoint_822_")
context.workspace_dir = tmpdir
# Register cleanup early to avoid leaks if subsequent steps fail
def _cleanup() -> None:
shutil.rmtree(tmpdir, ignore_errors=True)
context.add_cleanup(_cleanup)
# Initialise git repo
_run_git(["init", "--initial-branch=main"], cwd=tmpdir)
_run_git(["config", "user.email", "test@example.com"], cwd=tmpdir)
_run_git(["config", "user.name", "Test"], cwd=tmpdir)
# Create and commit the tracked file
tracked_path = os.path.join(tmpdir, _TRACKED_FILENAME)
Path(tracked_path).write_text(_INITIAL_CONTENT)
_run_git(["add", _TRACKED_FILENAME], cwd=tmpdir)
_run_git(["commit", "-m", "Initial commit"], cwd=tmpdir)
context.tracked_file_path = tracked_path
context.initial_sha = _get_head_sha(tmpdir)
@given("a checkpoint is created from the current commit")
def step_create_checkpoint(context: Context) -> None:
"""Create a checkpoint referencing the current HEAD commit."""
plan_id_value = "01JBG822CHKPT000PXAN000000"
sandbox_ref = context.initial_sha
service = CheckpointService()
service.register_sandbox(plan_id_value, context.workspace_dir)
checkpoint = service.create_checkpoint(
plan_id=plan_id_value,
sandbox_ref=sandbox_ref,
reason="TDD checkpoint for bug #822",
checkpoint_type="manual",
)
context.checkpoint_service = service
context.checkpoint = checkpoint
context.plan_id = plan_id_value
@given("the tracked file is modified after the checkpoint")
def step_modify_tracked_file(context: Context) -> None:
"""Overwrite the tracked file and commit the change."""
Path(context.tracked_file_path).write_text(_MODIFIED_CONTENT)
_run_git(["add", _TRACKED_FILENAME], cwd=context.workspace_dir)
_run_git(["commit", "-m", "Modify tracked file"], cwd=context.workspace_dir)
@given("a new file is added and committed after the checkpoint")
def step_add_new_file(context: Context) -> None:
"""Create a new file and commit it after the checkpoint."""
new_path = os.path.join(context.workspace_dir, _NEW_FILENAME)
Path(new_path).write_text("new file content\n")
_run_git(["add", _NEW_FILENAME], cwd=context.workspace_dir)
_run_git(["commit", "-m", "Add new file"], cwd=context.workspace_dir)
context.new_file_path = new_path
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I invoke rollback_to_checkpoint targeting the checkpoint")
def step_invoke_rollback(context: Context) -> None:
"""Call rollback_to_checkpoint and store the result."""
service: CheckpointService = context.checkpoint_service
result = service.rollback_to_checkpoint(
plan_id=context.plan_id,
checkpoint_id=context.checkpoint.checkpoint_id,
)
context.rollback_result = result
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("the tracked file content should match the checkpoint state")
def step_assert_file_reverted(context: Context) -> None:
"""Assert that the tracked file has been reverted to its initial content.
This assertion FAILS on master because rollback_to_checkpoint() does
not execute git reset --hard — the file still contains the modified
content. The failure proves bug #822 exists.
"""
actual = Path(context.tracked_file_path).read_text()
assert actual == _INITIAL_CONTENT, (
f"Expected tracked file to be reverted to checkpoint content.\n"
f"Expected: {_INITIAL_CONTENT!r}\n"
f"Actual: {actual!r}\n"
f"Bug #822: rollback_to_checkpoint() did not execute git reset --hard."
)
@then("the new file should not exist in the workspace")
def step_assert_new_file_removed(context: Context) -> None:
"""Assert that the file added after the checkpoint no longer exists.
This assertion FAILS on master because rollback_to_checkpoint() does
not execute git reset --hard — the new file remains on disk. The
failure proves bug #822 exists.
"""
assert not os.path.exists(context.new_file_path), (
f"Expected new file to be removed after rollback.\n"
f"File still exists: {context.new_file_path}\n"
f"Bug #822: rollback_to_checkpoint() did not execute git reset --hard."
)