|
| 1 | +"""Unit tests for the E2E harness's Copilot CLI platform-package resolution. |
| 2 | +
|
| 3 | +Regression coverage for github/copilot-sdk#2103: the harness used to return the |
| 4 | +first ``@github/copilot-*`` directory in alphabetical order instead of the package |
| 5 | +built for the current platform. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +from pathlib import Path |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from copilot._cli_version import get_npm_platform |
| 15 | +from e2e.testharness import context |
| 16 | + |
| 17 | + |
| 18 | +def _make_package(github_modules: Path, name: str) -> Path: |
| 19 | + """Create ``<github_modules>/<name>/index.js`` and return the entrypoint path.""" |
| 20 | + package_dir = github_modules / name |
| 21 | + package_dir.mkdir(parents=True, exist_ok=True) |
| 22 | + index = package_dir / "index.js" |
| 23 | + index.write_text("// fake CLI entrypoint\n") |
| 24 | + return index |
| 25 | + |
| 26 | + |
| 27 | +class TestCliPlatformPackageNames: |
| 28 | + def test_non_linux_platform_yields_single_candidate(self): |
| 29 | + assert context._cli_platform_package_names("darwin-arm64") == ["copilot-darwin-arm64"] |
| 30 | + |
| 31 | + def test_windows_platform_yields_single_candidate(self): |
| 32 | + assert context._cli_platform_package_names("win32-x64") == ["copilot-win32-x64"] |
| 33 | + |
| 34 | + def test_glibc_linux_also_considers_musl_variant(self): |
| 35 | + assert context._cli_platform_package_names("linux-x64") == [ |
| 36 | + "copilot-linux-x64", |
| 37 | + "copilot-linuxmusl-x64", |
| 38 | + ] |
| 39 | + |
| 40 | + def test_musl_linux_prefers_musl_then_falls_back_to_glibc(self): |
| 41 | + assert context._cli_platform_package_names("linuxmusl-arm64") == [ |
| 42 | + "copilot-linuxmusl-arm64", |
| 43 | + "copilot-linux-arm64", |
| 44 | + ] |
| 45 | + |
| 46 | + def test_defaults_to_current_host_platform(self): |
| 47 | + assert context._cli_platform_package_names()[0] == f"copilot-{get_npm_platform()}" |
| 48 | + |
| 49 | + |
| 50 | +class TestFindCliInNodeModules: |
| 51 | + def test_skips_alphabetically_earlier_foreign_package(self, tmp_path): |
| 52 | + # The #2103 regression: "aardvark" sorts before every real platform name. |
| 53 | + _make_package(tmp_path, "copilot-aardvark-x64") |
| 54 | + expected = _make_package(tmp_path, "copilot-darwin-arm64") |
| 55 | + found = context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) |
| 56 | + assert found == str(expected.resolve()) |
| 57 | + |
| 58 | + def test_returns_none_when_no_candidate_is_installed(self, tmp_path): |
| 59 | + _make_package(tmp_path, "copilot-win32-x64") |
| 60 | + assert context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) is None |
| 61 | + |
| 62 | + def test_ignores_non_platform_copilot_packages(self, tmp_path): |
| 63 | + _make_package(tmp_path, "copilot-language-server") |
| 64 | + assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None |
| 65 | + |
| 66 | + def test_prefers_earlier_candidate_when_both_libc_variants_exist(self, tmp_path): |
| 67 | + expected = _make_package(tmp_path, "copilot-linuxmusl-x64") |
| 68 | + _make_package(tmp_path, "copilot-linux-x64") |
| 69 | + found = context._find_cli_in_node_modules( |
| 70 | + tmp_path, ["copilot-linuxmusl-x64", "copilot-linux-x64"] |
| 71 | + ) |
| 72 | + assert found == str(expected.resolve()) |
| 73 | + |
| 74 | + def test_returns_none_when_package_dir_has_no_index_js(self, tmp_path): |
| 75 | + (tmp_path / "copilot-linux-x64").mkdir() |
| 76 | + assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None |
| 77 | + |
| 78 | + def test_returns_none_when_github_modules_is_absent(self, tmp_path): |
| 79 | + missing = tmp_path / "missing" |
| 80 | + assert context._find_cli_in_node_modules(missing, ["copilot-linux-x64"]) is None |
| 81 | + |
| 82 | + |
| 83 | +class TestInstalledCliPackageNames: |
| 84 | + def test_lists_platform_directories_sorted(self, tmp_path): |
| 85 | + _make_package(tmp_path, "copilot-win32-x64") |
| 86 | + _make_package(tmp_path, "copilot-darwin-arm64") |
| 87 | + (tmp_path / "not-copilot").mkdir() |
| 88 | + assert context._installed_cli_package_names(tmp_path) == [ |
| 89 | + "copilot-darwin-arm64", |
| 90 | + "copilot-win32-x64", |
| 91 | + ] |
| 92 | + |
| 93 | + def test_returns_empty_when_directory_is_absent(self, tmp_path): |
| 94 | + assert context._installed_cli_package_names(tmp_path / "missing") == [] |
| 95 | + |
| 96 | + |
| 97 | +class TestGetCliPathForTests: |
| 98 | + def test_env_var_takes_precedence(self, tmp_path, monkeypatch): |
| 99 | + cli = tmp_path / "custom-cli.js" |
| 100 | + cli.write_text("// custom entrypoint\n") |
| 101 | + monkeypatch.setenv("COPILOT_CLI_PATH", str(cli)) |
| 102 | + assert context.get_cli_path_for_tests() == str(cli.resolve()) |
| 103 | + |
| 104 | + def test_error_names_the_packages_tried_and_the_remedy(self, monkeypatch): |
| 105 | + monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) |
| 106 | + monkeypatch.setattr( |
| 107 | + context, "_cli_platform_package_names", lambda *_: ["copilot-linux-x64"] |
| 108 | + ) |
| 109 | + monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) |
| 110 | + with pytest.raises(RuntimeError) as excinfo: |
| 111 | + context.get_cli_path_for_tests() |
| 112 | + message = str(excinfo.value) |
| 113 | + assert "copilot-linux-x64" in message |
| 114 | + assert "npm install" in message |
| 115 | + assert "COPILOT_CLI_PATH" in message |
| 116 | + |
| 117 | + def test_error_names_the_searched_directory(self, monkeypatch): |
| 118 | + monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) |
| 119 | + seen: list[Path] = [] |
| 120 | + |
| 121 | + def fake_find(github_modules, package_names): |
| 122 | + seen.append(github_modules) |
| 123 | + return None |
| 124 | + |
| 125 | + monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) |
| 126 | + monkeypatch.setattr(context, "_find_cli_in_node_modules", fake_find) |
| 127 | + with pytest.raises(RuntimeError) as excinfo: |
| 128 | + context.get_cli_path_for_tests() |
| 129 | + assert seen, "get_cli_path_for_tests must consult _find_cli_in_node_modules" |
| 130 | + assert seen[0].name == "@github" |
| 131 | + assert seen[0].parent.name == "node_modules" |
| 132 | + assert seen[0].parent.parent.name == "nodejs" |
| 133 | + assert str(seen[0]) in str(excinfo.value) |
| 134 | + |
| 135 | + def test_error_lists_the_packages_actually_installed(self, monkeypatch): |
| 136 | + monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) |
| 137 | + monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) |
| 138 | + monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) |
| 139 | + monkeypatch.setattr( |
| 140 | + context, "_installed_cli_package_names", lambda *_: ["copilot-darwin-arm64"] |
| 141 | + ) |
| 142 | + with pytest.raises(RuntimeError) as excinfo: |
| 143 | + context.get_cli_path_for_tests() |
| 144 | + message = str(excinfo.value) |
| 145 | + assert "present: copilot-darwin-arm64" in message |
| 146 | + assert "copilot-nope-x64" in message |
0 commit comments