From 8bd7f4cbd244af2848895d15d3e3075b71a9a969 Mon Sep 17 00:00:00 2001 From: Jonathan Singer Date: Fri, 2 Oct 2026 11:32:55 -0400 Subject: [PATCH] refactor: use one auto-fix setting and infer fix delivery --- strix/core/runner.py | 14 +------------- strix/fix/scan.py | 6 ++---- strix/interface/cli.py | 1 - strix/interface/tui/runtime.py | 1 - tests/test_runner_teardown.py | 15 ++++++++------- tests/test_scan_fixes.py | 11 +++-------- 6 files changed, 14 insertions(+), 34 deletions(-) diff --git a/strix/core/runner.py b/strix/core/runner.py index c892d1dc..3c4c4340 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -500,22 +500,11 @@ async def run_strix_scan( ) report_state = get_global_report_state() - auto_fix_enabled = ( - scan_config.get( - "auto_fix_enabled", - scan_config.get("one_click_fixes_enabled", True), - ) - is not False - ) - local_fix_branches_enabled = ( - scan_config.get("local_fix_branches_enabled") is True and fix_sink is None - ) if ( - auto_fix_enabled + scan_config.get("auto_fix_enabled", True) is not False and scan_config.get("mode") != "pr_review" and report_state is not None and local_sources - and (not interactive or local_fix_branches_enabled) ): fixes = ScanFixes( session=sandbox_session, @@ -527,7 +516,6 @@ async def run_strix_scan( report_state=report_state, event_sink=event_sink, sink=fix_sink, - publish_local_branches=local_fix_branches_enabled, ) report_state.defer_completion = True diff --git a/strix/fix/scan.py b/strix/fix/scan.py index 5e4859eb..060b0971 100644 --- a/strix/fix/scan.py +++ b/strix/fix/scan.py @@ -54,7 +54,6 @@ class ScanFixes: report_state: Any, event_sink: Any = None, sink: FixSink | None = None, - publish_local_branches: bool = False, ) -> None: self.session, self.coordinator = session, coordinator self.scan_id, self.directory = scan_id, state_dir / "fixes" @@ -77,7 +76,6 @@ class ScanFixes: if s.get("source_path") } self.hooks, self.event_sink, self.sink = hooks, event_sink, sink - self.publish_local_branches = publish_local_branches self.report_state = report_state self.tasks: dict[str, asyncio.Task[Any]] = {} self.dispatches: set[asyncio.Task[Any]] = set() @@ -152,7 +150,7 @@ class ScanFixes: ) except Exception as error: # noqa: BLE001 - report launch failure to the scan await self._cancel_active(finding_id) - if candidate is not None and self.publish_local_branches: + if candidate is not None and self.sink is None: record = self.records.setdefault(finding_id, {}) record.update(digest=candidate.digest(), status="stopped", reason=str(error)) self._save() @@ -431,7 +429,7 @@ class ScanFixes: await self._reconcile() self.closed = True await asyncio.gather(*self.tasks.values(), return_exceptions=True) - if not self.publish_local_branches: + if self.sink is not None: return [], [] branches: list[dict[str, str]] = [] diff --git a/strix/interface/cli.py b/strix/interface/cli.py index e61ab8b5..45b3e134 100644 --- a/strix/interface/cli.py +++ b/strix/interface/cli.py @@ -94,7 +94,6 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915 "scan_mode": scan_mode, "non_interactive": bool(getattr(args, "non_interactive", False)), "auto_fix_enabled": bool(args.auto_fix), - "local_fix_branches_enabled": bool(args.auto_fix), "local_sources": getattr(args, "local_sources", None) or [], "workspace_files": getattr(args, "workspace_files", None) or [], "scope_mode": getattr(args, "scope_mode", "auto"), diff --git a/strix/interface/tui/runtime.py b/strix/interface/tui/runtime.py index 30c28b19..e6000a8b 100644 --- a/strix/interface/tui/runtime.py +++ b/strix/interface/tui/runtime.py @@ -92,7 +92,6 @@ class GoTuiRuntime: "scan_mode": self.args.scan_mode, "non_interactive": False, "auto_fix_enabled": bool(self.args.auto_fix), - "local_fix_branches_enabled": bool(self.args.auto_fix), "local_sources": self.args.local_sources or [], "workspace_files": getattr(self.args, "workspace_files", None) or [], "scope_mode": self.args.scope_mode, diff --git a/tests/test_runner_teardown.py b/tests/test_runner_teardown.py index 3aa9ad16..e9fc5810 100644 --- a/tests/test_runner_teardown.py +++ b/tests/test_runner_teardown.py @@ -106,7 +106,12 @@ async def test_a_live_child_is_settled_before_sessions_close( @pytest.mark.asyncio @pytest.mark.parametrize( "interactive,local_branches,is_resume", - [(False, False, False), (False, True, False), (True, True, False), (True, True, True)], + [ + (interactive, local_branches, resume) + for interactive in (False, True) + for local_branches in (False, True) + for resume in ((False, True) if interactive else (False,)) + ], ) async def test_assessment_publishes_before_fixes_end_and_sandbox_teardown( monkeypatch: pytest.MonkeyPatch, @@ -135,7 +140,6 @@ async def test_assessment_publishes_before_fixes_end_and_sandbox_teardown( class Fixes: def __init__(self, **options: Any) -> None: - assert options["publish_local_branches"] is local_branches assert (options["sink"] is not None) is not local_branches def start(self, *_: Any) -> None: @@ -183,7 +187,6 @@ async def test_assessment_publishes_before_fixes_end_and_sandbox_teardown( scan_config={ "targets": [], "scan_mode": "deep", - "local_fix_branches_enabled": local_branches, }, scan_id="scan", image="image", @@ -202,17 +205,15 @@ async def test_assessment_publishes_before_fixes_end_and_sandbox_teardown( @pytest.mark.parametrize( "fix_config", [ - {"one_click_fixes_enabled": False}, + {"auto_fix_enabled": False}, {"mode": "pr_review"}, { "mode": "pr_review", - "one_click_fixes_enabled": True, "auto_fix_enabled": True, - "local_fix_branches_enabled": True, }, ], ) -async def test_disabled_one_click_fixes_skips_fix_runtime( +async def test_disabled_auto_fix_skips_fix_runtime( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, interactive: bool, diff --git a/tests/test_scan_fixes.py b/tests/test_scan_fixes.py index 4ba10c9d..86b8dd40 100644 --- a/tests/test_scan_fixes.py +++ b/tests/test_scan_fixes.py @@ -127,7 +127,9 @@ async def test_native_parallel_fixes_deliver_patches_and_preserve_scan_source( assert first["success"] and second["success"], (first, second) duplicate = await delegate(context) assert duplicate["agent_id"] == first["agent_id"] - await fixes.wait() + branches, errors = await fixes.wait() + assert branches == errors == [] + assert _git(source, "branch", "--list", "strix/fix-*") == "" assert len(models) == 2 assert all(record["status"] == "done" for record in fixes.records.values()), fixes.records assert all(Path(record["artifact"]).exists() for record in fixes.records.values()) @@ -150,7 +152,6 @@ async def test_ready_fix_creates_local_branch_without_changing_checkout( tmp_path, monkeypatch, interactive ): fixes, _, source, _, _, context, sessions = setup(tmp_path, interactive=interactive) - fixes.publish_local_branches = True original_head = _git(source, "rev-parse", "HEAD") original_branch = _git(source, "branch", "--show-current") monkeypatch.setattr( @@ -205,7 +206,6 @@ async def test_delegation_errors_reach_reporting_agent_before_any_model_call(tmp @pytest.mark.asyncio async def test_blocked_native_child_has_no_patch(tmp_path, monkeypatch): fixes, _, _, _, _, context, sessions = setup(tmp_path) - fixes.publish_local_branches = True monkeypatch.setattr( scan_module, "_run_config", @@ -233,7 +233,6 @@ async def test_cancelled_verification_is_reported_without_publishing_a_branch( tmp_path, monkeypatch, withdrawn ): fixes, _, source, _, reports, context, sessions = setup(tmp_path, interactive=True) - fixes.publish_local_branches = True reviewing = asyncio.Event() async def verify(*_args): @@ -272,7 +271,6 @@ async def test_cancelled_verification_is_reported_without_publishing_a_branch( @pytest.mark.parametrize("status", ["running", "stopped", "failed", "done"]) async def test_wait_omits_withdrawn_records_but_keeps_current_fix_errors(tmp_path, status): fixes, _, source, _, _, _, _ = setup(tmp_path) - fixes.publish_local_branches = True digest = fixes._finding("finding")[1].digest() fixes.records = { "finding": {"digest": digest, "status": "stopped", "reason": "Tests failed"}, @@ -294,7 +292,6 @@ async def test_wait_omits_withdrawn_records_but_keeps_current_fix_errors(tmp_pat @pytest.mark.asyncio async def test_dispatch_failure_is_reported_before_an_agent_exists(tmp_path, monkeypatch): fixes, _, _, _, _, context, _ = setup(tmp_path) - fixes.publish_local_branches = True fixes._parent_ctx = context.context monkeypatch.setattr(fixes, "spawn", AsyncMock(side_effect=RuntimeError("Source unavailable"))) await fixes._dispatch("finding") @@ -306,7 +303,6 @@ async def test_dispatch_failure_is_reported_before_an_agent_exists(tmp_path, mon @pytest.mark.asyncio async def test_revised_candidate_does_not_publish_stale_ready_artifact(tmp_path, monkeypatch): fixes, report, source, _, _, _, _ = setup(tmp_path) - fixes.publish_local_branches = True original_digest = fixes._finding("finding")[1].digest() artifact = tmp_path / "state/fixes/stale/prepared-fix.zip" artifact.parent.mkdir(parents=True) @@ -330,7 +326,6 @@ async def test_revised_candidate_does_not_publish_stale_ready_artifact(tmp_path, @pytest.mark.asyncio async def test_local_branch_failure_is_persisted_and_returned(tmp_path): fixes, _, _, _, _, _, _ = setup(tmp_path) - fixes.publish_local_branches = True digest = fixes._finding("finding")[1].digest() artifact = tmp_path / "state/fixes/broken/prepared-fix.zip" artifact.parent.mkdir(parents=True)