Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion loopx/self_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
Expand Down
49 changes: 42 additions & 7 deletions loopx/windows_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions tests/test_python_ci_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
69 changes: 66 additions & 3 deletions tests/test_windows_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any

import pytest

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -377,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]
Expand Down Expand Up @@ -424,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()
Loading