Skip to content
Open
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
20 changes: 20 additions & 0 deletions tests/features/interlock_stages.feature
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,23 @@ Feature: interlocks stage commands on a minimal inline project
Then the stage exits 0
And the stage output contains "coverage_min"
And the stage output contains "advanced_from_sha"

# req: stage-check
Scenario: `interlocks check` does not enforce required acceptance when run_acceptance_in_check is false
Given a tmp project with require_acceptance true and run_acceptance_in_check false
When I run "interlocks check" in the tmp project
Then the stage exits 0

# req: stage-check
Scenario: `interlocks check` fails with remediation when acceptance is required in check and features dir is missing
Given a tmp project with require_acceptance true and run_acceptance_in_check true
When I run "interlocks check" in the tmp project
Then the stage exits 1
And the stage output contains "init-acceptance"

# req: stage-ci
Scenario: `interlocks ci` fails with remediation when acceptance is required and features dir is missing
Given a tmp project with require_acceptance true
When I run "interlocks ci" in the tmp project
Then the stage exits 1
And the stage output contains "init-acceptance"
36 changes: 36 additions & 0 deletions tests/step_defs/test_interlock_stages.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,42 @@ def _tmp_project_untracked_markdown(tmp_project: Path) -> None:
(tmp_project / "NOTES.md").write_text("# Notes\n\nNon-Python.\n", encoding="utf-8")


def _make_acceptance_project(tmp_path: Path, extra_config: str) -> Path:
"""Minimal passing project with extra `[tool.interlocks]` config appended."""
project = make_tmp_project(tmp_path)
pyproject = project / "pyproject.toml"
pyproject.write_text(pyproject.read_text(encoding="utf-8") + extra_config, encoding="utf-8")
return project


@given(
"a tmp project with require_acceptance true and run_acceptance_in_check false",
target_fixture="tmp_project",
)
def _tmp_project_require_acceptance_no_check(tmp_path: Path) -> Path:
return _make_acceptance_project(
tmp_path, "require_acceptance = true\nrun_acceptance_in_check = false\n"
)


@given(
"a tmp project with require_acceptance true and run_acceptance_in_check true",
target_fixture="tmp_project",
)
def _tmp_project_require_acceptance_in_check(tmp_path: Path) -> Path:
return _make_acceptance_project(
tmp_path, "require_acceptance = true\nrun_acceptance_in_check = true\n"
)


@given(
"a tmp project with require_acceptance true",
target_fixture="tmp_project",
)
def _tmp_project_require_acceptance(tmp_path: Path) -> Path:
return _make_acceptance_project(tmp_path, "require_acceptance = true\n")


@given(
"a tmp project on the progressive preset with a recorded baseline floor",
target_fixture="tmp_project",
Expand Down
26 changes: 26 additions & 0 deletions tests/tasks/test_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,32 @@ def test_task_acceptance_behave_branch(tmp_project: Path, monkeypatch: pytest.Mo
assert "behave" in task.cmd


def test_cmd_acceptance_disabled_warns_and_exits_zero(
tmp_project: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""`acceptance_runner = 'off'` → skip nudge, no run(), exit 0."""
from interlocks.config import clear_cache
from interlocks.tasks import acceptance as mod

(tmp_project / "pyproject.toml").write_text(
_PYPROJECT + '\n[tool.interlocks]\nacceptance_runner = "off"\n',
encoding="utf-8",
)
monkeypatch.chdir(tmp_project)
clear_cache()

called: list[object] = []
monkeypatch.setattr(mod, "run", called.append)

mod.cmd_acceptance()

out = capsys.readouterr().out
assert "disabled" in out
assert called == []


def test_cmd_acceptance_optional_missing_warns_and_exits_zero(
tmp_project: Path,
monkeypatch: pytest.MonkeyPatch,
Expand Down
79 changes: 79 additions & 0 deletions tests/test_acceptance_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from interlocks.acceptance_status import (
AcceptanceStatus,
classify_acceptance,
classify_acceptance_with_details,
count_scenarios,
feature_files,
remediation_message,
Expand Down Expand Up @@ -221,3 +222,81 @@ def test_required_acceptance_failure_task_exits_with_remediation(tmp_path: Path)
assert "no `.feature` files" in payload
assert "features" in payload
assert "sys.exit(1)" in payload


# ─────────────── AcceptanceClassification.is_required_failure ───────────────


def test_is_required_failure_true_for_missing_features_dir(tmp_path: Path) -> None:
cfg = _cfg(tmp_path, features_dir=None, require_acceptance=True)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.MISSING_FEATURES_DIR
assert classification.is_required_failure is True


def test_is_required_failure_true_for_missing_feature_files(tmp_path: Path) -> None:
features = tmp_path / "tests" / "features"
features.mkdir(parents=True)
cfg = _cfg(tmp_path, features_dir=features, require_acceptance=True)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.MISSING_FEATURE_FILES
assert classification.is_required_failure is True


def test_is_required_failure_true_for_missing_scenarios(tmp_path: Path) -> None:
features = tmp_path / "tests" / "features"
_write_feature(features / "stub.feature", "Feature: stub\n # no scenarios\n")
cfg = _cfg(tmp_path, features_dir=features, require_acceptance=True)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.MISSING_SCENARIOS
assert classification.is_required_failure is True


def test_is_required_failure_false_for_optional_missing(tmp_path: Path) -> None:
cfg = _cfg(tmp_path, features_dir=None, require_acceptance=False)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.OPTIONAL_MISSING
assert classification.is_required_failure is False


def test_is_required_failure_false_for_disabled(tmp_path: Path) -> None:
cfg = _cfg(tmp_path, acceptance_runner="off", features_dir=None)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.DISABLED
assert classification.is_required_failure is False


def test_is_required_failure_false_for_runnable(tmp_path: Path) -> None:
features = tmp_path / "tests" / "features"
_write_feature(
features / "ok.feature",
"Feature: ok\n Scenario: a thing works\n Given precondition\n",
)
cfg = _cfg(tmp_path, features_dir=features, require_acceptance=False)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.RUNNABLE
assert classification.is_required_failure is False
Comment on lines +230 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new test suite for is_required_failure covers most statuses but omits AcceptanceStatus.MISSING_BEHAVIOR_COVERAGE. Since this status is also defined as a required failure in interlocks/acceptance_status.py (line 53), it should be included to ensure complete coverage of the property's logic. While setting up this state is more involved than the others, it is important for verifying the enforcement semantics mentioned in the PR title.



def test_is_required_failure_true_for_missing_behavior_coverage(tmp_path: Path) -> None:
features = tmp_path / "tests" / "features"
_write_feature(
features / "ok.feature",
"Feature: ok\n Scenario: a thing works\n Given precondition\n",
)
# Naming the project "interlocks" activates INTERLOCKS_REGISTRY; a feature
# file with no `# req:` markers leaves every behavior uncovered.
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "interlocks"\nversion = "0.0.0"\n',
encoding="utf-8",
)
clear_cache()
cfg = replace(
load_config(tmp_path),
project_root=tmp_path,
features_dir=features,
require_acceptance=True,
)
classification = classify_acceptance_with_details(cfg)
assert classification.status is AcceptanceStatus.MISSING_BEHAVIOR_COVERAGE
assert classification.is_required_failure is True
Loading