| |
|
|
| import hashlib |
| import json |
| import os |
| from pathlib import Path |
| import subprocess |
| import tarfile |
| import tempfile |
| import textwrap |
| import unittest |
|
|
|
|
| INSTALL_SCRIPT = Path(__file__).with_name("install.sh") |
| VERSION = "0.142.5" |
| MISMATCH_VERSION = "0.145.0" |
|
|
|
|
| class InstallShTest(unittest.TestCase): |
| def test_metadata_fetch_failure_is_not_reported_as_missing_assets(self) -> None: |
| result, requests = run_installer(VERSION, metadata_failure=True) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}" |
| ], |
| ) |
| self.assertIn( |
| f"Could not fetch GitHub release metadata for Codex {VERSION}", |
| result.stderr, |
| ) |
| self.assertNotIn("Could not find Codex package", result.stderr) |
|
|
| def test_exact_release_opt_out_uses_github_metadata_once(self) -> None: |
| result, requests = run_installer(VERSION, use_mirror=False) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| ], |
| ) |
| self.assertIn(f"Resolved version: {VERSION}", result.stdout) |
|
|
| def test_alpha_hotfix_release_is_valid(self) -> None: |
| version = "0.145.0-alpha.23.1" |
| result, requests = run_installer(version) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{version}", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{version}/codex-package_SHA256SUMS", |
| ], |
| ) |
| self.assertIn(f"Resolved version: {version}", result.stdout) |
|
|
| def test_latest_release_reuses_version_metadata(self) -> None: |
| result, requests = run_installer("latest") |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://api.github.com/repos/openai/codex/releases/latest", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| ], |
| ) |
| self.assertIn(f"Resolved version: {VERSION}", result.stdout) |
|
|
| def test_compact_metadata_is_independent_of_field_order(self) -> None: |
| result, requests = run_installer( |
| "latest", metadata_json=release_metadata(compact=True, reorder=True) |
| ) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://api.github.com/repos/openai/codex/releases/latest", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| ], |
| ) |
| self.assertIn(f"Resolved version: {VERSION}", result.stdout) |
|
|
| def test_json_like_strings_and_nested_fields_do_not_define_assets(self) -> None: |
| result, requests = run_installer( |
| VERSION, metadata_json=legacy_release_metadata_with_decoys() |
| ) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual(len(requests), 2) |
| self.assertIn("/codex-npm-", requests[1]) |
| self.assertNotIn("codex-package_SHA256SUMS", requests[1]) |
|
|
| def test_macos_install_exposes_code_mode_host_beside_codex(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
|
|
| result, _requests = run_installer_in( |
| root, |
| VERSION, |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| install_bin = root / "install-bin" |
| current = root / "codex-home" / "packages" / "standalone" / "current" |
| codex_path = install_bin / "codex" |
| host_path = install_bin / "codex-code-mode-host" |
| self.assertEqual(os.readlink(codex_path), str(current / "bin" / "codex")) |
| self.assertEqual( |
| os.readlink(host_path), |
| str(current / "bin" / "codex-code-mode-host"), |
| ) |
| self.assertTrue(os.access(host_path, os.X_OK)) |
|
|
| def test_releases_latest_installs_verified_package_by_default(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
|
|
| def test_daemon_install_preserves_visible_cli_and_profile(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir).resolve() |
| archive, checksum, metadata = create_package_release(root) |
| options = dict( |
| metadata_json=metadata, |
| archive_path=archive, |
| checksum_path=checksum, |
| force_macos=True, |
| ) |
| pinned, _ = run_installer_in(root, VERSION, **options) |
| self.assertEqual(pinned.returncode, 0, pinned.stderr) |
| visible = root / "install-bin/codex" |
| original = visible.resolve() |
| pending = visible.with_name(".codex.12345") |
| pending.symlink_to(original) |
| profile = root / "home/.profile" |
| profile.write_text("unchanged profile\n") |
|
|
| installed, _ = run_installer_in(root, "latest", daemon_only=True, **options) |
| self.assertEqual(installed.returncode, 0, installed.stderr) |
| daemon = root / "codex-home/packages/app-server-daemon" |
| release_name = f"{VERSION}-aarch64-apple-darwin" |
| self.assertEqual( |
| (daemon / "current").resolve(), daemon / "releases" / release_name |
| ) |
| self.assertEqual((daemon / "auto-update-version").read_text(), release_name) |
| options["daemon_only"] = True |
| local = daemon / "releases/local-development" |
| local.mkdir() |
| (daemon / "current").unlink() |
| (daemon / "current").symlink_to(local) |
| marker = daemon / "auto-update-version" |
| marker.unlink() |
|
|
| scheduled, _ = run_installer_in( |
| root, "latest", update_guard_from_release=local.name, **options |
| ) |
| self.assertEqual(scheduled.returncode, 0, scheduled.stderr) |
| self.assertEqual((daemon / "current").resolve(), local) |
| self.assertFalse(marker.exists()) |
|
|
| raced, _ = run_installer_in( |
| root, |
| "latest", |
| update_guard_from_release="a-different-release", |
| manual_update=True, |
| **options, |
| ) |
| self.assertNotEqual(raced.returncode, 0, raced.stderr) |
| self.assertEqual((daemon / "current").resolve(), local) |
| self.assertFalse(marker.exists()) |
|
|
| binary = daemon / "releases" / release_name / "bin/codex" |
| contents = binary.read_text() |
| binary.write_text( |
| '#!/bin/sh\n[ "$1" = "--version" ] || exit 2\n' |
| f"echo codex-cli {VERSION}\n" |
| ) |
| incompatible, _ = run_installer_in( |
| root, |
| "latest", |
| update_guard_from_release=local.name, |
| manual_update=True, |
| **options, |
| ) |
| self.assertNotEqual(incompatible.returncode, 0) |
| self.assertIn("does not support daemon-owned packages", incompatible.stderr) |
| self.assertEqual((daemon / "current").resolve(), local) |
| self.assertFalse(marker.exists()) |
| binary.write_text(contents) |
|
|
| restored, _ = run_installer_in( |
| root, |
| "latest", |
| update_guard_from_release=local.name, |
| manual_update=True, |
| **options, |
| ) |
| self.assertEqual(restored.returncode, 0, restored.stderr) |
| self.assertEqual( |
| (daemon / "current").resolve(), daemon / "releases" / release_name |
| ) |
| self.assertEqual(marker.read_text(), release_name) |
| self.assertEqual(visible.resolve(), original) |
| self.assertTrue(pending.is_symlink()) |
| self.assertEqual(profile.read_text(), "unchanged profile\n") |
| self.assertFalse( |
| (root / "codex-home/packages/standalone/auto-update-version").exists() |
| ) |
|
|
| def test_explicit_release_pins_even_the_current_latest_version(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive, checksum, metadata = create_package_release(root) |
| options = dict( |
| metadata_json=metadata, |
| archive_path=archive, |
| checksum_path=checksum, |
| force_macos=True, |
| ) |
| marker = root / "codex-home/packages/standalone/auto-update-version" |
| latest, _ = run_installer_in(root, "latest", **options) |
| self.assertEqual(latest.returncode, 0, latest.stderr) |
| release_name = f"{VERSION}-aarch64-apple-darwin" |
| self.assertEqual(marker.read_text(), release_name) |
|
|
| pinned, _ = run_installer_in(root, VERSION, **options) |
| self.assertEqual(pinned.returncode, 0, pinned.stderr) |
| self.assertFalse(marker.exists()) |
|
|
| updater_record = ( |
| root / "codex-home/app-server-daemon/app-server-updater.pid" |
| ) |
| updater_record.parent.mkdir(parents=True) |
| updater_record.write_text( |
| json.dumps( |
| {"pid": os.getpid(), "processStartTime": process_start_time()} |
| ) |
| ) |
| old_updater, _ = run_installer_in( |
| root, "latest", old_updater_parent_pid=os.getpid(), **options |
| ) |
| self.assertEqual(old_updater.returncode, 0, old_updater.stderr) |
| self.assertFalse(marker.exists()) |
|
|
| skipped, _ = run_installer_in( |
| root, "latest", update_guard_from_release=release_name, **options |
| ) |
| self.assertEqual(skipped.returncode, 0, skipped.stderr) |
| self.assertFalse(marker.exists()) |
|
|
| updater_record.write_text( |
| json.dumps({"pid": os.getpid(), "processStartTime": "stale"}) |
| ) |
| latest_again, _ = run_installer_in( |
| root, "latest", old_updater_parent_pid=os.getpid(), **options |
| ) |
| self.assertEqual(latest_again.returncode, 0, latest_again.stderr) |
| self.assertEqual(marker.read_text(), release_name) |
|
|
| managed = ( |
| root |
| / f"codex-home/packages/standalone/releases/{release_name}/bin/codex" |
| ) |
| managed.unlink() |
| guarded, _ = run_installer_in( |
| root, |
| "latest", |
| update_guard_from_release=release_name, |
| **options, |
| ) |
| self.assertEqual(guarded.returncode, 0, guarded.stderr) |
| self.assertTrue(managed.exists()) |
|
|
| def test_uninspectable_legacy_updater_does_not_clear_pin(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive, checksum, metadata = create_package_release(root) |
| options = dict( |
| metadata_json=metadata, |
| archive_path=archive, |
| checksum_path=checksum, |
| force_macos=True, |
| ) |
| pinned, _ = run_installer_in(root, VERSION, **options) |
| self.assertEqual(pinned.returncode, 0, pinned.stderr) |
| updater_record = ( |
| root / "codex-home/app-server-daemon/app-server-updater.pid" |
| ) |
| updater_record.parent.mkdir(parents=True) |
| updater_record.write_text( |
| json.dumps( |
| {"pid": os.getpid(), "processStartTime": process_start_time()} |
| ) |
| ) |
|
|
| attempted, _ = run_installer_in( |
| root, |
| "latest", |
| old_updater_parent_pid=os.getpid(), |
| fail_ps=True, |
| **options, |
| ) |
| self.assertEqual(attempted.returncode, 0, attempted.stderr) |
| self.assertFalse( |
| (root / "codex-home/packages/standalone/auto-update-version").exists() |
| ) |
|
|
| def test_releases_unusable_metadata_falls_back_to_github(self) -> None: |
| unusable_metadata = { |
| "html": "<html>proxy error</html>", |
| "empty": "", |
| "malformed_json": '{"tag_name":', |
| "missing_tag": json.dumps({"assets": []}), |
| "missing_assets": json.dumps( |
| {"tag_name": f"rust-v{VERSION}", "assets": []} |
| ), |
| "invalid_checksum_digest": json.dumps( |
| { |
| "tag_name": f"rust-v{VERSION}", |
| "assets": [ |
| { |
| "name": "codex-package-aarch64-apple-darwin.tar.gz", |
| "digest": "sha256:" + "a" * 64, |
| }, |
| { |
| "name": "codex-package_SHA256SUMS", |
| "digest": "sha256:" + "z" * 64, |
| }, |
| ], |
| } |
| ), |
| "invalid_version": json.dumps({"tag_name": "rust-vinvalid"}), |
| } |
|
|
| for name, releases_metadata_json in unusable_metadata.items(): |
| with self.subTest(metadata=name): |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release( |
| root |
| ) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| releases_metadata_json=releases_metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| "https://api.github.com/repos/openai/codex/releases/latest", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("falling back to GitHub Releases", result.stderr) |
|
|
| def test_releases_exact_metadata_version_mismatch_falls_back_to_github( |
| self, |
| ) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
| releases_metadata = json.loads(metadata_json) |
| releases_metadata["tag_name"] = f"rust-v{MISMATCH_VERSION}" |
|
|
| result, requests = run_installer_in( |
| root, |
| VERSION, |
| metadata_json=metadata_json, |
| releases_metadata_json=json.dumps(releases_metadata), |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| f"https://releases.openai.com/codex/releases/{VERSION}/release.json", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("falling back to GitHub Releases", result.stderr) |
|
|
| def test_releases_asset_download_falls_back_to_github(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| releases_mode="asset_fallback", |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("retrying from GitHub Releases", result.stderr) |
|
|
| def test_releases_corrupt_assets_fall_back_to_github(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| releases_mode="corrupt_assets", |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("checksum did not match expected digest", result.stderr) |
| self.assertIn("retrying from GitHub Releases", result.stderr) |
|
|
| def test_releases_wrong_checksum_digest_uses_github_metadata(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
| mirror_metadata = json.loads(metadata_json) |
| for release_asset in mirror_metadata["assets"]: |
| if release_asset["name"] == "codex-package_SHA256SUMS": |
| release_asset["digest"] = "sha256:" + "0" * 64 |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| releases_metadata_json=json.dumps(mirror_metadata), |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("checksum did not match expected digest", result.stderr) |
|
|
| def test_releases_incomplete_checksum_manifest_falls_back_to_github(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
| mirror_checksum_path = root / "mirror-SHA256SUMS" |
| mirror_checksum_path.write_text( |
| f"{'a' * 64} codex-package-other-platform.tar.gz\n", |
| encoding="utf-8", |
| ) |
| mirror_metadata = json.loads(metadata_json) |
| for release_asset in mirror_metadata["assets"]: |
| if release_asset["name"] == "codex-package_SHA256SUMS": |
| release_asset["digest"] = ( |
| "sha256:" |
| + hashlib.sha256(mirror_checksum_path.read_bytes()).hexdigest() |
| ) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| releases_metadata_json=json.dumps(mirror_metadata), |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| releases_checksum_path=mirror_checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| ) |
|
|
| self.assertEqual(result.returncode, 0, result.stderr) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn("retrying from GitHub Releases", result.stderr) |
|
|
| def test_releases_corrupt_github_fallback_still_fails(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release(root) |
|
|
| result, requests = run_installer_in( |
| root, |
| "latest", |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=None, |
| releases_mode="corrupt_checksum_and_github", |
| ) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| "https://releases.openai.com/codex/channels/latest", |
| f"https://releases.openai.com/codex/releases/{VERSION}/codex-package_SHA256SUMS", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-package_SHA256SUMS", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| ], |
| ) |
| self.assertIn("checksum did not match expected digest", result.stderr) |
|
|
| def test_releases_exact_rejects_wrong_binary_version(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, checksum_path, metadata_json = create_package_release( |
| root, |
| metadata_version=MISMATCH_VERSION, |
| ) |
|
|
| result, requests = run_installer_in( |
| root, |
| MISMATCH_VERSION, |
| metadata_json=metadata_json, |
| archive_path=archive_path, |
| checksum_path=checksum_path, |
| force_macos=True, |
| use_mirror=True, |
| ) |
|
|
| self.assertNotEqual(result.returncode, 0) |
| self.assertEqual( |
| requests, |
| [ |
| f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/release.json", |
| f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/codex-package_SHA256SUMS", |
| f"https://releases.openai.com/codex/releases/{MISMATCH_VERSION}/codex-package-aarch64-apple-darwin.tar.gz", |
| ], |
| ) |
| self.assertIn( |
| f"did not report expected version {MISMATCH_VERSION}", |
| result.stderr, |
| ) |
| self.assertNotIn("installed successfully", result.stdout) |
|
|
| def test_releases_exact_legacy_fallback_reuses_offline_install(self) -> None: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| root = Path(temp_dir) |
| archive_path, metadata_json = create_legacy_release(root) |
|
|
| first_result, first_requests = run_installer_in( |
| root, |
| VERSION, |
| metadata_json=metadata_json, |
| legacy_archive_path=archive_path, |
| force_macos=True, |
| use_mirror=True, |
| releases_mode="channel_failure", |
| ) |
|
|
| self.assertEqual(first_result.returncode, 0, first_result.stderr) |
| self.assertEqual( |
| first_requests, |
| [ |
| f"https://releases.openai.com/codex/releases/{VERSION}/release.json", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| "https://github.com/openai/codex/releases/download/" |
| f"rust-v{VERSION}/codex-npm-darwin-arm64-{VERSION}.tgz", |
| ], |
| ) |
|
|
| (root / "requests.log").unlink() |
| second_result, second_requests = run_installer_in( |
| root, |
| VERSION, |
| metadata_json=metadata_json, |
| force_macos=True, |
| use_mirror=True, |
| releases_mode="channel_failure", |
| ) |
|
|
| self.assertEqual(second_result.returncode, 0, second_result.stderr) |
| self.assertEqual( |
| second_requests, |
| [ |
| f"https://releases.openai.com/codex/releases/{VERSION}/release.json", |
| "https://api.github.com/repos/openai/codex/releases/tags/" |
| f"rust-v{VERSION}", |
| ], |
| ) |
| self.assertNotIn("Downloading Codex CLI", second_result.stdout) |
|
|
|
|
| def run_installer( |
| release: str, |
| *, |
| metadata_failure: bool = False, |
| metadata_json: str | None = None, |
| use_mirror: bool | None = False, |
| ) -> tuple[subprocess.CompletedProcess[str], list[str]]: |
| with tempfile.TemporaryDirectory() as temp_dir: |
| return run_installer_in( |
| Path(temp_dir), |
| release, |
| metadata_failure=metadata_failure, |
| metadata_json=metadata_json, |
| use_mirror=use_mirror, |
| ) |
|
|
|
|
| def run_installer_in( |
| root: Path, |
| release: str, |
| *, |
| metadata_failure: bool = False, |
| metadata_json: str | None = None, |
| releases_metadata_json: str | None = None, |
| archive_path: Path | None = None, |
| checksum_path: Path | None = None, |
| releases_checksum_path: Path | None = None, |
| legacy_archive_path: Path | None = None, |
| force_macos: bool = False, |
| use_mirror: bool | None = False, |
| releases_mode: str = "", |
| update_guard_from_release: str | None = None, |
| old_updater_parent_pid: int | None = None, |
| fail_ps: bool = False, |
| daemon_only: bool = False, |
| manual_update: bool = False, |
| ) -> tuple[subprocess.CompletedProcess[str], list[str]]: |
| bin_dir = root / "bin" |
| bin_dir.mkdir(exist_ok=True) |
| request_log = root / "requests.log" |
| fake_curl = bin_dir / "curl" |
| fake_curl.write_text( |
| textwrap.dedent( |
| """\ |
| #!/bin/sh |
| url="" |
| output="" |
| previous="" |
| for arg in "$@"; do |
| case "$arg" in |
| https://*) url="$arg" ;; |
| esac |
| if [ "$previous" = "-o" ]; then |
| output="$arg" |
| fi |
| previous="$arg" |
| done |
| printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG" |
| |
| case "$url" in |
| https://api.github.com/*) |
| if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then |
| echo "curl: (22) The requested URL returned error: 403" >&2 |
| exit 22 |
| fi |
| printf '%s\n' "$CODEX_TEST_METADATA_JSON" |
| ;; |
| https://releases.openai.com/codex/channels/latest|https://releases.openai.com/codex/releases/*/release.json) |
| if [ "$CODEX_TEST_RELEASES_MODE" = "channel_failure" ]; then |
| exit 22 |
| fi |
| printf '%s\n' "$CODEX_TEST_RELEASES_METADATA_JSON" |
| ;; |
| https://releases.openai.com/codex/releases/*/codex-package_SHA256SUMS) |
| if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then |
| exit 22 |
| fi |
| if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ] || |
| [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then |
| printf '<html>proxy error</html>\n' >"$output" |
| exit 0 |
| fi |
| if [ -n "$CODEX_TEST_RELEASES_CHECKSUM_PATH" ]; then |
| cp "$CODEX_TEST_RELEASES_CHECKSUM_PATH" "$output" |
| else |
| exit 22 |
| fi |
| ;; |
| https://releases.openai.com/codex/releases/*/codex-package-*.tar.gz) |
| if [ "$CODEX_TEST_RELEASES_MODE" = "asset_fallback" ]; then |
| exit 22 |
| fi |
| if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_assets" ]; then |
| printf '<html>proxy error</html>\n' >"$output" |
| exit 0 |
| fi |
| if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then |
| cp "$CODEX_TEST_ARCHIVE_PATH" "$output" |
| else |
| exit 22 |
| fi |
| ;; |
| https://github.com/openai/codex/releases/download/*/codex-package_SHA256SUMS) |
| if [ "$CODEX_TEST_RELEASES_MODE" = "corrupt_checksum_and_github" ]; then |
| printf '<html>proxy error</html>\n' >"$output" |
| exit 0 |
| fi |
| if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then |
| cp "$CODEX_TEST_CHECKSUM_PATH" "$output" |
| else |
| exit 22 |
| fi |
| ;; |
| https://github.com/openai/codex/releases/download/*/codex-package-*.tar.gz) |
| if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then |
| cp "$CODEX_TEST_ARCHIVE_PATH" "$output" |
| else |
| exit 22 |
| fi |
| ;; |
| https://github.com/openai/codex/releases/download/*/codex-npm-*.tgz) |
| if [ -n "$CODEX_TEST_LEGACY_ARCHIVE_PATH" ]; then |
| cp "$CODEX_TEST_LEGACY_ARCHIVE_PATH" "$output" |
| else |
| exit 22 |
| fi |
| ;; |
| *) |
| exit 22 |
| ;; |
| esac |
| """ |
| ), |
| encoding="utf-8", |
| ) |
| fake_curl.chmod(0o755) |
| if force_macos: |
| fake_uname = bin_dir / "uname" |
| fake_uname.write_text( |
| "#!/bin/sh\n" |
| 'case "$1" in\n' |
| " -s) printf 'Darwin\\n' ;;\n" |
| " -m) printf 'arm64\\n' ;;\n" |
| "esac\n", |
| encoding="utf-8", |
| ) |
| fake_uname.chmod(0o755) |
| if old_updater_parent_pid is not None: |
| fake_ps = bin_dir / "ps" |
| fake_ps.write_text( |
| "#!/bin/sh\nexit 1\n" |
| if fail_ps |
| else "#!/bin/sh\n" |
| 'case "$*" in\n' |
| ' *lstart*) printf "S %s\\n" "$CODEX_TEST_PARENT_START" ;;\n' |
| ' *) printf "%s\\n" "$CODEX_TEST_PARENT_PID" ;;\n' |
| "esac\n", |
| encoding="utf-8", |
| ) |
| fake_ps.chmod(0o755) |
|
|
| home = root / "home" |
| home.mkdir(exist_ok=True) |
| env = os.environ.copy() |
| env.update( |
| { |
| "CODEX_HOME": str(root / "codex-home"), |
| "CODEX_INSTALL_DIR": str(root / "install-bin"), |
| "CODEX_NON_INTERACTIVE": "1", |
| "CODEX_RELEASE": release, |
| "CODEX_INSTALL_DAEMON_ONLY": "1" if daemon_only else "0", |
| "CODEX_TEST_ARCHIVE_PATH": str(archive_path or ""), |
| "CODEX_TEST_CHECKSUM_PATH": str(checksum_path or ""), |
| "CODEX_TEST_RELEASES_CHECKSUM_PATH": str( |
| releases_checksum_path or checksum_path or "" |
| ), |
| "CODEX_TEST_LEGACY_ARCHIVE_PATH": str(legacy_archive_path or ""), |
| "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", |
| "CODEX_TEST_METADATA_JSON": ( |
| metadata_json if metadata_json is not None else release_metadata() |
| ), |
| "CODEX_TEST_RELEASES_METADATA_JSON": ( |
| releases_metadata_json |
| if releases_metadata_json is not None |
| else metadata_json |
| if metadata_json is not None |
| else release_metadata() |
| ), |
| "CODEX_TEST_RELEASES_MODE": releases_mode, |
| "CODEX_TEST_REQUEST_LOG": str(request_log), |
| "HOME": str(home), |
| "PATH": f"{bin_dir}:/usr/bin:/bin", |
| "SHELL": "/bin/sh", |
| } |
| ) |
| env["CODEX_INSTALL_IF_CURRENT"] = "1" if manual_update else "0" |
| if update_guard_from_release is None: |
| env.pop("CODEX_INSTALL_IF_LATEST", None) |
| env.pop("CODEX_UPDATE_FROM_RELEASE", None) |
| else: |
| env["CODEX_INSTALL_IF_LATEST"] = "0" if manual_update else "1" |
| env["CODEX_UPDATE_FROM_RELEASE"] = update_guard_from_release |
| if old_updater_parent_pid is not None: |
| env["CODEX_TEST_PARENT_PID"] = str(old_updater_parent_pid) |
| env["CODEX_TEST_PARENT_START"] = process_start_time() |
| if use_mirror is None: |
| env.pop("CODEX_INSTALLER_USE_RELEASES_OPENAI_COM", None) |
| else: |
| env["CODEX_INSTALLER_USE_RELEASES_OPENAI_COM"] = ( |
| "TRUE" if use_mirror else "false" |
| ) |
| result = subprocess.run( |
| ["/bin/sh", str(INSTALL_SCRIPT)], |
| capture_output=True, |
| check=False, |
| env=env, |
| text=True, |
| ) |
| requests = ( |
| request_log.read_text(encoding="utf-8").splitlines() |
| if request_log.exists() |
| else [] |
| ) |
| return result, requests |
|
|
|
|
| def process_start_time() -> str: |
| details = subprocess.check_output( |
| ["ps", "-p", str(os.getpid()), "-o", "stat=", "-o", "lstart="], text=True |
| ).strip() |
| return details.split(maxsplit=1)[1] |
|
|
|
|
| def create_package_release( |
| root: Path, |
| *, |
| metadata_version: str = VERSION, |
| ) -> tuple[Path, Path, str]: |
| package_dir = root / "package" |
| (package_dir / "bin").mkdir(parents=True) |
| (package_dir / "codex-path").mkdir() |
| (package_dir / "codex-package.json").write_text("{}\n", encoding="utf-8") |
| write_executable( |
| package_dir / "bin" / "codex", |
| f"#!/bin/sh\nprintf 'codex-cli {VERSION}\\n'\n", |
| ) |
| write_executable( |
| package_dir / "bin" / "codex-code-mode-host", |
| "#!/bin/sh\nexit 0\n", |
| ) |
| write_executable(package_dir / "codex-path" / "rg", "#!/bin/sh\nexit 0\n") |
|
|
| asset = "codex-package-aarch64-apple-darwin.tar.gz" |
| archive_path = root / asset |
| with tarfile.open(archive_path, "w:gz") as archive: |
| for path in package_dir.iterdir(): |
| archive.add(path, arcname=path.name) |
|
|
| archive_digest = hashlib.sha256(archive_path.read_bytes()).hexdigest() |
| checksum_path = root / "codex-package_SHA256SUMS" |
| checksum_path.write_text(f"{archive_digest} {asset}\n", encoding="utf-8") |
| checksum_digest = hashlib.sha256(checksum_path.read_bytes()).hexdigest() |
| metadata_json = json.dumps( |
| { |
| "assets": [ |
| {"name": asset, "digest": f"sha256:{archive_digest}"}, |
| { |
| "name": "codex-package_SHA256SUMS", |
| "digest": f"sha256:{checksum_digest}", |
| }, |
| ], |
| "tag_name": f"rust-v{metadata_version}", |
| }, |
| indent=2, |
| ) |
| return archive_path, checksum_path, metadata_json |
|
|
|
|
| def create_legacy_release(root: Path) -> tuple[Path, str]: |
| package_dir = root / "legacy-package" |
| vendor_dir = package_dir / "package" / "vendor" / "aarch64-apple-darwin" |
| (vendor_dir / "codex").mkdir(parents=True) |
| (vendor_dir / "path").mkdir() |
| write_executable( |
| vendor_dir / "codex" / "codex", |
| f"#!/bin/sh\nprintf 'codex-cli {VERSION}\\n'\n", |
| ) |
| write_executable(vendor_dir / "path" / "rg", "#!/bin/sh\nexit 0\n") |
|
|
| asset = f"codex-npm-darwin-arm64-{VERSION}.tgz" |
| archive_path = root / asset |
| with tarfile.open(archive_path, "w:gz") as archive: |
| archive.add(package_dir / "package", arcname="package") |
|
|
| archive_digest = hashlib.sha256(archive_path.read_bytes()).hexdigest() |
| metadata_json = json.dumps( |
| { |
| "assets": [{"name": asset, "digest": f"sha256:{archive_digest}"}], |
| "tag_name": f"rust-v{VERSION}", |
| }, |
| indent=2, |
| ) |
| return archive_path, metadata_json |
|
|
|
|
| def write_executable(path: Path, contents: str) -> None: |
| path.write_text(contents, encoding="utf-8") |
| path.chmod(0o755) |
|
|
|
|
| def release_metadata(*, compact: bool = False, reorder: bool = False) -> str: |
| assets = [ |
| asset_metadata( |
| f"codex-package-{target}.tar.gz", |
| f"sha256:{'a' * 64}", |
| reorder=reorder, |
| ) |
| for target in ( |
| "aarch64-apple-darwin", |
| "x86_64-apple-darwin", |
| "aarch64-unknown-linux-musl", |
| "x86_64-unknown-linux-musl", |
| ) |
| ] |
| assets.append( |
| asset_metadata( |
| "codex-package_SHA256SUMS", |
| f"sha256:{'b' * 64}", |
| reorder=reorder, |
| ) |
| ) |
| separators = (",", ":") if compact else None |
| return json.dumps( |
| {"assets": assets, "body": "braces: { } [ ]", "tag_name": f"rust-v{VERSION}"}, |
| indent=None if compact else 2, |
| separators=separators, |
| ) |
|
|
|
|
| def asset_metadata(name: str, digest: str, *, reorder: bool) -> dict[str, str]: |
| if reorder: |
| return {"digest": digest, "name": name} |
| return {"name": name, "digest": digest} |
|
|
|
|
| def legacy_release_metadata_with_decoys() -> str: |
| fake_digest = f"sha256:{'0' * 64}" |
| assets = [ |
| { |
| "metadata": { |
| "name": "codex-package-x86_64-unknown-linux-musl.tar.gz", |
| "digest": fake_digest, |
| }, |
| "digest": f"sha256:{'c' * 64}", |
| "name": f"codex-npm-{target}-{VERSION}.tgz", |
| } |
| for target in ("darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64") |
| ] |
| return json.dumps( |
| { |
| "body": ( |
| f'fake: {{"name":"codex-package_SHA256SUMS","digest":"{fake_digest}"}}' |
| ), |
| "assets": assets, |
| "tag_name": f"rust-v{VERSION}", |
| }, |
| separators=(",", ":"), |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|