diff --git a/src/github_agent_bridge/autoupdate.py b/src/github_agent_bridge/autoupdate.py index 6478d20..eb6e330 100644 --- a/src/github_agent_bridge/autoupdate.py +++ b/src/github_agent_bridge/autoupdate.py @@ -900,6 +900,7 @@ def plan_update( def record_update_plan(db: str | Path, plan: dict[str, Any]) -> dict[str, Any]: queue = JobQueue(db) + previous = load_update_state(queue) state = { "updated_at": utc_now(), "installed_version": plan["installed_version"], @@ -918,5 +919,18 @@ def record_update_plan(db: str | Path, plan: dict[str, Any]) -> dict[str, Any]: "service_plan": plan.get("service_plan", {}), "warnings": plan.get("warnings", []), } + if plan["decision"] == "noop" and previous.get("executor_reload_pending"): + # A refresh can observe the newly installed version before the + # completion pass has restarted the executor. Keep the deferred action + # actionable instead of replacing it with an up-to-date noop plan. + for key in ( + "decision", + "executor_reload_pending", + "blocked_reason", + "classification", + "service_plan", + ): + if key in previous: + state[key] = previous[key] save_update_state(queue, state) return state diff --git a/tests/test_autoupdate.py b/tests/test_autoupdate.py index ede8436..754657f 100644 --- a/tests/test_autoupdate.py +++ b/tests/test_autoupdate.py @@ -345,6 +345,48 @@ def test_executor_update_records_pending_reload_when_jobs_are_active(tmp_path, m assert load_update_state(q)["service_plan"]["deferred"][0]["command"] == "restart" +def test_noop_refresh_preserves_pending_executor_reload(tmp_path, monkeypatch): + monkeypatch.setattr("github_agent_bridge.actors.github_actor_details_for_context", lambda ctx, *, gh_bin="gh": None) + db = tmp_path / "bridge.sqlite3" + q = JobQueue(db) + job_id = enqueue_job(q) + + deferred_plan = plan_update( + db, + repo_dir=tmp_path, + installed_version="1.2.3", + runner=release_runner("v1.2.4", ["src/github_agent_bridge/executor.py"]), + ) + record_update_plan(db, deferred_plan) + q.finish(job_id, "done", "finished") + + noop_plan = plan_update( + db, + repo_dir=tmp_path, + installed_version="1.2.4", + runner=release_runner("v1.2.4", []), + ) + state = record_update_plan(db, noop_plan) + + assert noop_plan["decision"] == "noop" + assert state["installed_version"] == "1.2.4" + assert state["decision"] == "stage_defer_executor_reload" + assert state["executor_reload_pending"] is True + assert [action["unit"] for action in state["service_plan"]["deferred"]] == [ + "github-agent-bridge.service" + ] + calls: list[list[str]] = [] + completion = complete_pending_reload( + db, + systemctl_bin="systemctl-test", + runner=lambda args, cwd: calls.append(list(args)) or completed("ok"), + ) + + assert completion["completed"] is True + assert calls == [["systemctl-test", "--user", "restart", "github-agent-bridge.service"]] + assert load_update_state(q)["executor_reload_pending"] is False + + def test_migration_update_is_deferred_while_jobs_are_active(tmp_path, monkeypatch): monkeypatch.setattr("github_agent_bridge.actors.github_actor_details_for_context", lambda ctx, *, gh_bin="gh": None) db = tmp_path / "bridge.sqlite3"