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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,13 @@ Presets are optional defaults under `[tool.interlocks]`. Explicit values in the

```toml
[tool.interlocks]
preset = "baseline" # "baseline" | "strict" | "legacy"
preset = "baseline" # "baseline" | "strict" | "legacy" | "progressive"
```

- `baseline` lowers first-adoption friction: advisory CRAP, relaxed thresholds, mutation off in CI, acceptance off in `check`.
- `strict` is for mature repositories: stronger thresholds, blocking CRAP and mutation, mutation in CI, acceptance in `check`, and required Gherkin coverage.
- `legacy` is for ratcheting existing repositories: very permissive thresholds, advisory gates, mutation off in CI.
- `progressive` is an autopilot ratchet: blocking gates like `strict`, but floors are captured in `.interlocks/baseline.json` and advanced on green main merges. Start permissive; the baseline file drives thresholds upward automatically.

`agent-safe` is intentionally unsupported. If configured, `interlocks doctor` reports it as an unsupported preset instead of resolving agent-specific defaults.

Expand Down
10 changes: 10 additions & 0 deletions tests/features/interlock_cli.feature
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ Feature: interlocks CLI surface area
And the output contains "── Examples"
And the output does not contain "user-global"

# req: cli-presets-parity
Scenario: presets command lists all four presets including progressive
Given I run "interlocks presets"
Then the output contains "── Available Presets"
And the output contains "baseline"
And the output contains "strict"
And the output contains "legacy"
And the output contains "progressive"
And the output contains "autopilot ratchet"

# req: cli-evaluate-guidance
Scenario: Evaluate gap guidance includes closure command
Given I run "interlocks evaluate" on a project with a traceability gap
Expand Down
98 changes: 97 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from interlocks.config import (
CONFIG_KEYS,
InterlockConfig,
Preset,
clear_cache,
load_config,
preset_defaults,
Expand Down Expand Up @@ -143,6 +144,7 @@ def test_cmd_presets_prints_options_and_copyable_config(
assert "baseline" in out
assert "strict" in out
assert "legacy" in out
assert "progressive" in out
assert "── Next Steps" in out
assert "Set a project preset with the CLI:" in out
assert "interlocks presets set baseline" in out
Expand Down Expand Up @@ -281,7 +283,101 @@ def test_cmd_presets_rejects_unknown_preset(
assert exc.value.code == 1
out = capsys.readouterr().out
assert "unsupported preset: agent-safe" in out
assert "expected baseline|strict|legacy" in out
assert "expected baseline|strict|legacy|progressive" in out


@pytest.mark.parametrize(
("preset", "expected_fragment"),
[
("baseline", "advisory CRAP"),
("strict", "mature repo"),
("legacy", "ratcheting"),
("progressive", "autopilot ratchet"),
],
)
def test_cmd_presets_all_four_listed_with_descriptions(
capsys: pytest.CaptureFixture[str],
preset: str,
expected_fragment: str,
) -> None:
"""All four presets appear in `interlocks presets` output with their descriptions."""
cmd_presets()
out = capsys.readouterr().out
assert preset in out, f"preset {preset!r} missing from presets output"
assert expected_fragment in out, (
f"description fragment {expected_fragment!r} missing for preset {preset!r}"
)


@pytest.mark.parametrize(
("preset", "key", "expected"),
[
# baseline: advisory gates, mutation off
("baseline", "enforce_crap", False),
("baseline", "run_mutation_in_ci", False),
("baseline", "mutation_ci_mode", "off"),
("baseline", "run_acceptance_in_check", False),
("baseline", "coverage_min", 70),
# strict: all blocking gates on, mutation incremental
("strict", "enforce_crap", True),
("strict", "enforce_behavior_attribution", True),
("strict", "enforce_mutation", True),
("strict", "run_mutation_in_ci", True),
("strict", "mutation_ci_mode", "incremental"),
("strict", "run_acceptance_in_check", True),
("strict", "require_acceptance", True),
("strict", "coverage_min", 90),
Comment on lines +321 to +329

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 strict preset explicitly enables enforce_behavior_attribution. It should be included in this parity test to ensure the preset is correctly configured.

Suggested change
# strict: all blocking gates on, mutation incremental
("strict", "enforce_crap", True),
("strict", "enforce_mutation", True),
("strict", "run_mutation_in_ci", True),
("strict", "mutation_ci_mode", "incremental"),
("strict", "run_acceptance_in_check", True),
("strict", "require_acceptance", True),
("strict", "coverage_min", 90),
# strict: all blocking gates on, mutation incremental
("strict", "enforce_crap", True),
("strict", "enforce_behavior_attribution", True),
("strict", "enforce_mutation", True),
("strict", "run_mutation_in_ci", True),
("strict", "mutation_ci_mode", "incremental"),
("strict", "run_acceptance_in_check", True),
("strict", "require_acceptance", True),
("strict", "coverage_min", 90),

# legacy: very permissive, advisory only
("legacy", "enforce_crap", False),
("legacy", "run_mutation_in_ci", False),
("legacy", "coverage_min", 0),
("legacy", "mutation_ci_mode", "off"),
# progressive: blocking gates on, permissive floors (ratcheted at runtime)
("progressive", "enforce_crap", True),
("progressive", "enforce_behavior_attribution", True),
("progressive", "enforce_mutation", True),
("progressive", "run_mutation_in_ci", True),
("progressive", "mutation_ci_mode", "incremental"),
("progressive", "run_acceptance_in_check", True),
("progressive", "require_acceptance", True),
("progressive", "coverage_min", 0), # floor; ratcheted by baseline.json
Comment on lines +335 to +343

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 progressive preset also explicitly enables enforce_behavior_attribution. Including it here ensures full coverage of the preset's intended blocking behavior.

        # progressive: blocking gates on, permissive floors (ratcheted at runtime)
        ("progressive", "enforce_crap", True),
        ("progressive", "enforce_behavior_attribution", True),
        ("progressive", "enforce_mutation", True),
        ("progressive", "run_mutation_in_ci", True),
        ("progressive", "mutation_ci_mode", "incremental"),
        ("progressive", "run_acceptance_in_check", True),
        ("progressive", "require_acceptance", True),
        ("progressive", "coverage_min", 0),  # floor; ratcheted by baseline.json

],
)
def test_preset_defaults_key_values(preset: Preset, key: str, expected: object) -> None:
"""``preset_defaults()`` returns the documented gate values for each preset."""
defaults = preset_defaults(preset)
assert defaults[key] == expected, (
f"preset {preset!r}: expected {key}={expected!r}, got {defaults[key]!r}"
)


def test_progressive_preset_enables_blocking_gates_when_configured(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
clean_config_cache: None,
) -> None:
"""``preset = progressive`` wires blocking gates (CRAP, mutation, acceptance)."""
_setup_project_with_interlocks(tmp_path, monkeypatch, 'preset = "progressive"')

cmd_presets()

out = capsys.readouterr().out
assert re.search(r"^\s*preset\s+progressive\s*$", out, re.MULTILINE), out
assert re.search(r"^\s*enforce_crap\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
assert re.search(
r"^\s*enforce_behavior_attribution\s+True \(preset-derived\)\s*$", out, re.MULTILINE
), out
assert re.search(r"^\s*enforce_mutation\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
assert re.search(r"^\s*run_mutation_in_ci\s+True \(preset-derived\)\s*$", out, re.MULTILINE), (
out
)
assert re.search(
r"^\s*run_acceptance_in_check\s+True \(preset-derived\)\s*$", out, re.MULTILINE
), out
assert re.search(r"^\s*require_acceptance\s+True \(preset-derived\)\s*$", out, re.MULTILINE), (
out
)
Comment on lines +366 to +380

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

This integration test verifies that the progressive preset correctly wires blocking gates. It should also verify enforce_behavior_attribution and run_acceptance_in_check, which are key parts of the preset's configuration.

    assert re.search(r"^\s*preset\s+progressive\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*enforce_crap\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*enforce_behavior_attribution\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*enforce_mutation\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*run_mutation_in_ci\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*run_acceptance_in_check\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out
    assert re.search(r"^\s*require_acceptance\s+True \(preset-derived\)\s*$", out, re.MULTILINE), out



def test_cmd_presets_shorthand_rejects_extra_args(
Expand Down
Loading