From 64eb757855299f043e55bc247ac47f939a40e583 Mon Sep 17 00:00:00 2001 From: "duanjialing.777" Date: Sat, 26 Sep 2026 21:43:42 +0800 Subject: [PATCH 1/3] fix(update): harden authority upgrade subprocesses Pass the candidate release environment to the Windows pre-activation upgrade. Pin UTF-8 for authority upgrade output and retain stderr in failures. Signed-off-by: duanjialing.777 --- loopx/self_update.py | 3 +- loopx/windows_install.py | 49 ++++++++++++++++++++++++---- tests/test_windows_install.py | 61 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/loopx/self_update.py b/loopx/self_update.py index 140a67db4d..3eb8cf7d0e 100644 --- a/loopx/self_update.py +++ b/loopx/self_update.py @@ -1232,7 +1232,8 @@ def execute_rollback_plan( compatible = subprocess.run( [str(target_script), "--format", "json", "authority-archive", "upgrade", "--all-known", "--require-current"], - capture_output=True, text=True, timeout=timeout_seconds, + capture_output=True, text=True, encoding="utf-8", errors="replace", + timeout=timeout_seconds, ) if compatible.returncode != 0: raise RuntimeError("Rollback target cannot read current authority formats. " diff --git a/loopx/windows_install.py b/loopx/windows_install.py index 86a943e8d1..a6b0110940 100644 --- a/loopx/windows_install.py +++ b/loopx/windows_install.py @@ -159,6 +159,44 @@ def _validate_candidate( ) +def _upgrade_authority_archive( + release_root: Path, + *, + python: Path, + skills_dir: Path, +) -> None: + env = dict(os.environ) + env["LOOPX_RELEASE_ROOT"] = str(release_root) + env["CODEX_HOME"] = str(skills_dir.parent) + env["PYTHONDONTWRITEBYTECODE"] = "1" + result = subprocess.run( + _entry_command( + release_root, + python, + [ + "--format", + "json", + "authority-archive", + "upgrade", + "--all-known", + "--execute", + ], + ), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + env=env, + timeout=600, + ) + if result.returncode != 0: + raise RuntimeError( + "Authority format upgrade failed before launcher activation; " + "backups and candidate retained. " + f"stdout={result.stdout[-2000:]!r}, stderr={result.stderr[-2000:]!r}" + ) + + def _atomic_json(path: Path, payload: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( @@ -328,14 +366,11 @@ def install_windows( shutil.rmtree(temporary, ignore_errors=True) raise - upgrade = subprocess.run( - _entry_command(release_root, python, ["--format", "json", "authority-archive", "upgrade", - "--all-known", "--execute"]), - capture_output=True, text=True, timeout=600, + _upgrade_authority_archive( + release_root, + python=python, + skills_dir=skills_dir, ) - if upgrade.returncode != 0: - raise RuntimeError("Authority format upgrade failed before launcher activation; " - "backups and candidate retained. " + upgrade.stdout[-2000:]) launcher = bin_dir / "loopx.ps1" pointer = install_root / "current-release.json" diff --git a/tests/test_windows_install.py b/tests/test_windows_install.py index 774c3abd50..0af436cd8a 100644 --- a/tests/test_windows_install.py +++ b/tests/test_windows_install.py @@ -6,6 +6,7 @@ import shutil import subprocess import sys +from typing import Any import pytest @@ -104,6 +105,66 @@ def test_chat_bundle_preflight_preserves_stdout_with_legacy_pointer( assert captured.err.splitlines() == ["bundle progress"] +def test_authority_upgrade_runs_from_candidate_release( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + release_root = tmp_path / "releases" / "candidate" + skills_dir = tmp_path / "codex" / "skills" + observed_command: list[str] = [] + observed_env: dict[str, str] = {} + + def run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + observed_command.extend(command) + observed_env.update(kwargs["env"]) + return subprocess.CompletedProcess(command, 0, stdout="", stderr="") + + monkeypatch.setattr(windows_install.subprocess, "run", run) + + windows_install._upgrade_authority_archive( + release_root, + python=Path(sys.executable), + skills_dir=skills_dir, + ) + + assert observed_command == [ + sys.executable, + "-I", + str(release_root / "scripts" / "loopx_entry.py"), + "--format", + "json", + "authority-archive", + "upgrade", + "--all-known", + "--execute", + ] + assert observed_env["LOOPX_RELEASE_ROOT"] == str(release_root) + assert observed_env["CODEX_HOME"] == str(skills_dir.parent) + assert observed_env["PYTHONDONTWRITEBYTECODE"] == "1" + + +def test_authority_upgrade_failure_reports_stderr( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + def run(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess( + command, + 2, + stdout="", + stderr="loopx runtime error: LOOPX_RELEASE_ROOT is not set", + ) + + monkeypatch.setattr(windows_install.subprocess, "run", run) + + with pytest.raises(RuntimeError, match="LOOPX_RELEASE_ROOT is not set"): + windows_install._upgrade_authority_archive( + tmp_path / "release", + python=Path(sys.executable), + skills_dir=tmp_path / "codex" / "skills", + ) + + @pytest.mark.skipif(os.name != "nt", reason="native Windows installer regression") def test_windows_installer_promotes_release_and_runs_doctor(tmp_path: Path) -> None: pwsh = shutil.which("pwsh") From ff15b311a569c0fbe40e282f3a6f96b69cff2e7a Mon Sep 17 00:00:00 2001 From: "duanjialing.777" Date: Sat, 26 Sep 2026 22:16:57 +0800 Subject: [PATCH 2/3] test(windows): retain upgraded recovery candidate Signed-off-by: duanjialing.777 --- tests/test_windows_install.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_windows_install.py b/tests/test_windows_install.py index 0af436cd8a..971b17a5d9 100644 --- a/tests/test_windows_install.py +++ b/tests/test_windows_install.py @@ -2,10 +2,10 @@ import json import os -from pathlib import Path import shutil import subprocess import sys +from pathlib import Path from typing import Any import pytest @@ -438,7 +438,7 @@ def reject_candidate(*args: object, **kwargs: object) -> None: @pytest.mark.skipif(os.name != "nt", reason="native Windows installer regression") -def test_windows_installer_rolls_back_late_user_surface_failure( +def test_windows_installer_rolls_back_user_surfaces_and_retains_upgraded_candidate( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: repo_root = Path(__file__).resolve().parents[1] @@ -485,4 +485,6 @@ def fail_after_skill_write( '{"release_id":"known-good"}\n' ) assert existing_skill.read_text(encoding="utf-8") == "# known-good skill\n" - assert not (install_root / "releases" / "rejected-late").exists() + retained_candidate = install_root / "releases" / "rejected-late" + assert retained_candidate.is_dir() + assert (retained_candidate / "scripts" / "loopx_entry.py").is_file() From 50078ca8ac2c6b5fcb16a21941253cb0d075c07a Mon Sep 17 00:00:00 2001 From: "duanjialing.777" Date: Sat, 26 Sep 2026 23:06:36 +0800 Subject: [PATCH 3/3] fix(ci): drop retired Windows test path Signed-off-by: duanjialing.777 --- .github/workflows/python-tests.yml | 1 - tests/test_python_ci_workflow.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index d445a11ed7..5d685b0997 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -645,7 +645,6 @@ jobs: tests/test_doctor_install_freshness.py tests/test_file_lock.py tests/test_file_lock_cross_process.py - tests/control_plane/test_coordination_file_provider.py tests/control_plane/test_effect_runtime_integration.py tests/control_plane/test_local_authority_shadow_outbox.py tests/test_self_update_runtime_activation.py diff --git a/tests/test_python_ci_workflow.py b/tests/test_python_ci_workflow.py index 44de1661c2..a036debc6b 100644 --- a/tests/test_python_ci_workflow.py +++ b/tests/test_python_ci_workflow.py @@ -239,6 +239,21 @@ def test_windows_lane_rebuilds_the_frontend_without_a_usable_python3() -> None: ) +def test_windows_lifecycle_suite_references_existing_tests() -> None: + job = WORKFLOW.split(" windows-powershell:\n", 1)[1].split( + " presentation:\n", 1, + )[0] + step = job.split("name: Run native Windows lifecycle tests", 1)[1].split( + "\n\n - name:", 1, + )[0] + test_paths = re.findall(r"^\s+(tests/\S+\.py)\s*$", step, re.MULTILINE) + + assert test_paths + assert [ + path for path in test_paths if not (WORKFLOW_ROOT / path).is_file() + ] == [] + + def test_four_shards_execute_each_test_once_and_merge_portable_coverage( tmp_path: Path, ) -> None: