Skip to content
Open
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
196 changes: 196 additions & 0 deletions tests/unit/test_update_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,201 @@ def test_update_uses_shell_installer_on_unix(
mock_chmod.assert_called_once()


class TestUpdatePlatformHelpers(unittest.TestCase):
"""Tests for platform-detection helper functions."""

def test_is_windows_platform_true_on_win32(self):
with patch.object(update_module.sys, "platform", "win32"):
self.assertTrue(update_module._is_windows_platform())

def test_is_windows_platform_false_on_linux(self):
with patch.object(update_module.sys, "platform", "linux"):
self.assertFalse(update_module._is_windows_platform())

def test_is_windows_platform_false_on_darwin(self):
with patch.object(update_module.sys, "platform", "darwin"):
self.assertFalse(update_module._is_windows_platform())

def test_installer_url_windows(self):
with patch.object(update_module.sys, "platform", "win32"):
url = update_module._get_update_installer_url()
self.assertEqual(url, "https://aka.ms/apm-windows")

def test_installer_url_unix(self):
with patch.object(update_module.sys, "platform", "linux"):
url = update_module._get_update_installer_url()
self.assertEqual(url, "https://aka.ms/apm-unix")

def test_installer_suffix_windows(self):
with patch.object(update_module.sys, "platform", "win32"):
suffix = update_module._get_update_installer_suffix()
self.assertEqual(suffix, ".ps1")

def test_installer_suffix_unix(self):
with patch.object(update_module.sys, "platform", "linux"):
suffix = update_module._get_update_installer_suffix()
self.assertEqual(suffix, ".sh")

def test_manual_update_command_unix(self):
with patch.object(update_module.sys, "platform", "linux"):
command = update_module._get_manual_update_command()
self.assertIn("aka.ms/apm-unix", command)
self.assertIn("curl", command)

def test_installer_run_command_unix_bin_sh_exists(self):
with patch.object(update_module.sys, "platform", "linux"), \
patch.object(update_module.os.path, "exists", return_value=True):
cmd = update_module._get_installer_run_command("/tmp/install.sh")
self.assertEqual(cmd, ["/bin/sh", "/tmp/install.sh"])

def test_installer_run_command_unix_fallback_to_sh(self):
with patch.object(update_module.sys, "platform", "linux"), \
patch.object(update_module.os.path, "exists", return_value=False):
cmd = update_module._get_installer_run_command("/tmp/install.sh")
self.assertEqual(cmd, ["sh", "/tmp/install.sh"])

def test_installer_run_command_windows_powershell_not_found(self):
with patch.object(update_module.sys, "platform", "win32"), \
patch.object(update_module.shutil, "which", return_value=None):
with self.assertRaises(FileNotFoundError):
update_module._get_installer_run_command("/tmp/install.ps1")

def test_installer_run_command_windows_pwsh_fallback(self):
def _which(name):
return "pwsh.exe" if name == "pwsh" else None

with patch.object(update_module.sys, "platform", "win32"), \
patch.object(update_module.shutil, "which", side_effect=_which):
cmd = update_module._get_installer_run_command("/tmp/install.ps1")
self.assertEqual(cmd[0], "pwsh.exe")
self.assertIn("-File", cmd)


class TestUpdateCommandLogic(unittest.TestCase):
"""Tests for the update click command business logic."""

def setUp(self):
self.runner = CliRunner()
Comment on lines +169 to +170
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apm update writes the installer script into get_apm_temp_dir() (env var / user config dependent). These tests invoke the command without pinning that temp dir, so they can fail on developer machines where APM_TEMP_DIR or ~/.apm/config.json points to a non-writable/non-existent location. Consider setting APM_TEMP_DIR to a temporary directory (or otherwise patching get_apm_temp_dir) in setUp so the tests are hermetic across environments.

Copilot uses AI. Check for mistakes.

@patch("apm_cli.commands.update.get_version", return_value="unknown")
def test_update_dev_version_warns_and_returns(self, mock_version):
result = self.runner.invoke(cli, ["update"])
self.assertEqual(result.exit_code, 0)
self.assertIn("development mode", result.output)

@patch("apm_cli.commands.update.get_version", return_value="unknown")
def test_update_dev_version_check_flag_no_reinstall_hint(self, mock_version):
"""When --check is passed with dev version, reinstall hint should be suppressed."""
result = self.runner.invoke(cli, ["update", "--check"])
self.assertEqual(result.exit_code, 0)
self.assertIn("development mode", result.output)
self.assertNotIn("reinstall", result.output)

@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value=None)
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
def test_update_cannot_fetch_latest_exits_1(self, mock_version, mock_latest):
result = self.runner.invoke(cli, ["update"])
self.assertEqual(result.exit_code, 1)
self.assertIn("Unable to fetch latest version", result.output)

@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.0.0")
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
def test_update_already_on_latest(self, mock_version, mock_latest):
result = self.runner.invoke(cli, ["update"])
self.assertEqual(result.exit_code, 0)
self.assertIn("latest version", result.output)

@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0")
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
def test_update_check_flag_shows_available_no_install(self, mock_version, mock_latest):
result = self.runner.invoke(cli, ["update", "--check"])
self.assertEqual(result.exit_code, 0)
self.assertIn("1.0.0", result.output)
self.assertIn("1.1.0", result.output)

@patch("requests.get")
@patch("subprocess.run")
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
@patch("apm_cli.commands.update.os.chmod")
@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0")
def test_update_installer_failure_exits_1(
self, mock_latest, mock_chmod, mock_version, mock_run, mock_get
):
mock_response = Mock()
mock_response.text = "echo install"
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
mock_run.return_value = Mock(returncode=1)

with patch.object(update_module.sys, "platform", "linux"), \
patch("apm_cli.commands.update.os.path.exists", return_value=True):
result = self.runner.invoke(cli, ["update"])

self.assertEqual(result.exit_code, 1)
self.assertIn("Installation failed", result.output)

@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0")
def test_update_requests_not_available_exits_1(self, mock_latest, mock_version):
"""When requests library is missing, exit with clear message."""
import builtins
real_import = builtins.__import__

def mock_import(name, *args, **kwargs):
if name == "requests":
raise ImportError("No module named 'requests'")
return real_import(name, *args, **kwargs)

with patch("builtins.__import__", side_effect=mock_import), \
patch.object(update_module.sys, "platform", "linux"):
result = self.runner.invoke(cli, ["update"])

self.assertEqual(result.exit_code, 1)
self.assertIn("requests", result.output)

@patch("requests.get")
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
@patch("apm_cli.commands.update.os.chmod")
@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0")
def test_update_network_error_exits_1(self, mock_latest, mock_chmod, mock_version, mock_get):
mock_get.side_effect = Exception("Network error")

with patch.object(update_module.sys, "platform", "linux"):
result = self.runner.invoke(cli, ["update"])

self.assertEqual(result.exit_code, 1)
self.assertIn("Update failed", result.output)

@patch("requests.get")
@patch("subprocess.run")
@patch("apm_cli.commands.update.get_version", return_value="1.0.0")
@patch("apm_cli.commands.update.os.chmod")
@patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0")
def test_update_temp_file_cleanup_on_success(
self, mock_latest, mock_chmod, mock_version, mock_run, mock_get
):
"""Verify temporary script is deleted after successful install."""
deleted_paths = []
original_unlink = update_module.os.unlink

def tracking_unlink(path):
deleted_paths.append(path)
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original_unlink is assigned but never used, and tracking_unlink does not call through to the real os.unlink. That means this test will leave the temporary installer script on disk, which can pollute the configured/system temp directory over repeated runs. Consider calling the original unlink inside the tracking wrapper (or avoid patching unlink and assert cleanup via filesystem state in an isolated temp dir).

Suggested change
deleted_paths.append(path)
deleted_paths.append(path)
original_unlink(path)

Copilot uses AI. Check for mistakes.

mock_response = Mock()
mock_response.text = "echo install"
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
mock_run.return_value = Mock(returncode=0)

with patch.object(update_module.sys, "platform", "linux"), \
patch("apm_cli.commands.update.os.path.exists", return_value=True), \
patch.object(update_module.os, "unlink", side_effect=tracking_unlink):
result = self.runner.invoke(cli, ["update"])

self.assertEqual(result.exit_code, 0)
self.assertEqual(len(deleted_paths), 1)
self.assertTrue(deleted_paths[0].endswith(".sh"))


if __name__ == "__main__":
unittest.main()
Loading