"""Regression tests for SEA-aware detection in uclaude_updater.py. Background: before this fix, find_cli_js() searched ONLY for `cli.js` files. After upstream switched to SEA layout (v2.1.114+), `cli.js` no longer exists — there's only `bin/claude.exe`. As a result: - find_cli_js() returned None - get_installed_version() returned (None, None) - is_patched() returned (False, [3 markers]) — false negative - cmd_check showed "Claude Code: not installed" right after install These tests pin down the SEA-aware behaviour so it cannot regress. """ from __future__ import annotations import importlib.util import json import os import sys from pathlib import Path from unittest.mock import patch as mock_patch ROOT = Path(__file__).resolve().parent.parent spec = importlib.util.spec_from_file_location( "uclaude_updater", ROOT / "uclaude_updater.py" ) uu = importlib.util.module_from_spec(spec) sys.modules["uclaude_updater"] = uu spec.loader.exec_module(uu) def _make_sea_install(npm_root: Path, version: str = "2.1.119", patched: bool = True, nested: bool = False) -> Path: """Create a fake SEA install layout under npm_root. nested=True → also nest a copy under npm_root/@anthropic-ai/claude-code/node_modules/@anthropic-ai/claude-code/ matching how npm SEA wrapper packages get unpacked. """ pkg = npm_root / "@anthropic-ai" / "claude-code" if nested: pkg = pkg / "node_modules" / "@anthropic-ai" / "claude-code" bin_dir = pkg / "bin" bin_dir.mkdir(parents=True) payload = b"\x7fELF" + (b"X" * 4096) if patched: payload += b"/*ae1_models_filter_patched*/" + (b"Y" * 1024) payload += b"/*bypass_permissions_prompt*/" + (b"Z" * 512) binary = bin_dir / "claude.exe" binary.write_bytes(payload) os.chmod(binary, 0o755) (pkg / "cli-wrapper.cjs").write_text("// wrapper\n") (pkg / "package.json").write_text(json.dumps({"version": version})) return binary def test_find_claude_artifact_finds_sea_binary(tmp_path): """SEA install: bin/claude.exe must be discovered when no cli.js.""" npm_root = tmp_path / "npm_global" npm_root.mkdir() binary = _make_sea_install(npm_root) with mock_patch("subprocess.run") as mrun: mrun.return_value = type("R", (), { "returncode": 0, "stdout": str(npm_root), "stderr": "", })() with mock_patch("os.path.isfile", side_effect=lambda p: Path(p).is_file()): with mock_patch.object(uu, "IS_WINDOWS", False): got = uu.find_claude_artifact() assert got is not None assert Path(got).name == "claude.exe" def test_find_claude_artifact_finds_nested_sea(tmp_path): """Nested layout: @anthropic-ai/claude-code/node_modules/@anthropic-ai/claude-code/bin/claude.exe""" npm_root = tmp_path / "npm_global" npm_root.mkdir() binary = _make_sea_install(npm_root, nested=True) with mock_patch("subprocess.run") as mrun: # which claude → realpath returns the nested binary def _run(cmd, **kw): class R: pass r = R() r.returncode = 0 r.stderr = "" if cmd[:1] == ["which"]: r.stdout = str(binary) else: r.stdout = str(npm_root) return r mrun.side_effect = _run with mock_patch.object(uu, "IS_WINDOWS", False): got = uu.find_claude_artifact() assert got is not None assert Path(got).resolve() == binary.resolve() def test_is_patched_sea_recognises_marker_in_binary(tmp_path): """SEA: is_patched scans binary as bytes, not text.""" npm_root = tmp_path / "npm" npm_root.mkdir() binary = _make_sea_install(npm_root, patched=True) patched, missing = uu.is_patched(str(binary)) assert patched is True assert missing == [] def test_is_patched_sea_unpatched_returns_false(tmp_path): """SEA without ae1/bypass markers must report not patched.""" npm_root = tmp_path / "npm" npm_root.mkdir() binary = _make_sea_install(npm_root, patched=False) patched, missing = uu.is_patched(str(binary)) assert patched is False # Both SEA markers should be reported missing assert any("ae1" in m for m in missing) assert any("bypass" in m for m in missing) def test_is_patched_legacy_cli_js_still_works(tmp_path): """Legacy cli.js path: text-based scan unchanged.""" cli_js = tmp_path / "cli.js" cli_js.write_text( "// __CLAUDE_SETTINGS__\n" "function x(){return!!(/*bypass_permissions_prompt*/false)}\n" "/* root check removed by patcher */\n" ) patched, missing = uu.is_patched(str(cli_js)) assert patched is True assert missing == [] def test_is_patched_returns_false_on_missing_file(): patched, missing = uu.is_patched("/nonexistent/path") assert patched is False assert len(missing) > 0 def test_get_installed_version_sea_reads_package_json(tmp_path): """SEA: version comes from package.json next to bin/.""" npm_root = tmp_path / "npm" npm_root.mkdir() binary = _make_sea_install(npm_root, version="2.1.119") with mock_patch.object(uu, "find_claude_artifact", return_value=str(binary)): v, p = uu.get_installed_version() assert v == "2.1.119" assert Path(p).name == "claude.exe" def test_get_installed_version_returns_none_when_missing(): with mock_patch.object(uu, "find_claude_artifact", return_value=None): v, p = uu.get_installed_version() assert v is None assert p is None def test_find_all_cli_js_returns_sea_artifact(tmp_path): """Multi-install detector must also surface SEA binaries.""" npm_root = tmp_path / "npm" npm_root.mkdir() binary = _make_sea_install(npm_root) with mock_patch("subprocess.run") as mrun: mrun.return_value = type("R", (), { "returncode": 0, "stdout": str(npm_root), "stderr": "", })() with mock_patch.object(uu, "IS_WINDOWS", False): paths = uu.find_all_cli_js() sea_paths = [p for p in paths if p.endswith("claude.exe")] assert len(sea_paths) >= 1