From ab37fed8b8f6048b490c0370bd23a4909fd9a15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Gait=C3=A1n-Villamizar?= Date: Mon, 11 May 2026 12:36:51 +0200 Subject: [PATCH 1/2] test(acceptance): probe require_acceptance, run_acceptance_in_check, and stage-enforcement semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/test_acceptance_status.py: add 6 explicit is_required_failure tests covering MISSING_FEATURES_DIR, MISSING_FEATURE_FILES, MISSING_SCENARIOS (true) and OPTIONAL_MISSING, DISABLED, RUNNABLE (false) - tests/tasks/test_acceptance.py: add test for cmd_acceptance with DISABLED runner - tests/features/interlock_stages.feature: 3 new scenarios — check skips enforcement when run_acceptance_in_check=false, check gates when both flags true, ci gates when require_acceptance=true and features dir is absent - tests/step_defs/test_interlock_stages.py: step defs for the new scenarios with a shared _make_acceptance_project helper to avoid pyproject read/append/write duplication --- tests/features/interlock_stages.feature | 20 +++++++++ tests/step_defs/test_interlock_stages.py | 36 ++++++++++++++++ tests/tasks/test_acceptance.py | 26 +++++++++++ tests/test_acceptance_status.py | 55 ++++++++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/tests/features/interlock_stages.feature b/tests/features/interlock_stages.feature index b7dd6b3..d75775d 100644 --- a/tests/features/interlock_stages.feature +++ b/tests/features/interlock_stages.feature @@ -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" diff --git a/tests/step_defs/test_interlock_stages.py b/tests/step_defs/test_interlock_stages.py index e0de514..67413ca 100644 --- a/tests/step_defs/test_interlock_stages.py +++ b/tests/step_defs/test_interlock_stages.py @@ -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", diff --git a/tests/tasks/test_acceptance.py b/tests/tasks/test_acceptance.py index 25f5141..c731fdd 100644 --- a/tests/tasks/test_acceptance.py +++ b/tests/tasks/test_acceptance.py @@ -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, diff --git a/tests/test_acceptance_status.py b/tests/test_acceptance_status.py index 7eb37d2..d343fa3 100644 --- a/tests/test_acceptance_status.py +++ b/tests/test_acceptance_status.py @@ -8,6 +8,7 @@ from interlocks.acceptance_status import ( AcceptanceStatus, classify_acceptance, + classify_acceptance_with_details, count_scenarios, feature_files, remediation_message, @@ -221,3 +222,57 @@ 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 From e0e4bc7197a9a6f669123421756b8c6915bdad47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Gait=C3=A1n-Villamizar?= Date: Mon, 11 May 2026 12:44:38 +0200 Subject: [PATCH 2/2] test(acceptance): add is_required_failure probe for MISSING_BEHAVIOR_COVERAGE Completes the is_required_failure test matrix by covering the MISSING_BEHAVIOR_COVERAGE status, which was flagged as missing in the Gemini code review. Uses the same project-name trick as the existing classify test: naming the project "interlocks" activates INTERLOCKS_REGISTRY so any feature file without `# req:` markers triggers incomplete coverage. --- tests/test_acceptance_status.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_acceptance_status.py b/tests/test_acceptance_status.py index d343fa3..fdd0ef1 100644 --- a/tests/test_acceptance_status.py +++ b/tests/test_acceptance_status.py @@ -276,3 +276,27 @@ def test_is_required_failure_false_for_runnable(tmp_path: Path) -> None: classification = classify_acceptance_with_details(cfg) assert classification.status is AcceptanceStatus.RUNNABLE assert classification.is_required_failure is False + + +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