refactor: use one auto-fix setting and infer fix delivery

This commit is contained in:
Jonathan Singer 2026-10-02 11:32:55 -04:00
parent 9edb2ae00c
commit 8bd7f4cbd2
6 changed files with 14 additions and 34 deletions

View file

@ -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

View file

@ -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]] = []

View file

@ -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"),

View file

@ -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,

View file

@ -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,

View file

@ -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)