from pathlib import Path from unittest.mock import patch import pytest from triggershell.bootstrap import ( BootstrapError, check_node, ensure_dependencies_installed, load_dotenv_for_config, needs_build, record_build_stamp, ) def _completed(stdout: str = "", returncode: int = 0): class Result: pass result = Result() result.stdout = stdout result.returncode = returncode return result def test_check_node_missing_raises() -> None: with patch("triggershell.bootstrap.shutil.which", return_value=None): with pytest.raises(BootstrapError, match="not found"): check_node() def test_check_node_too_old_raises() -> None: with patch("triggershell.bootstrap.shutil.which", return_value="/usr/bin/node"): with patch("triggershell.bootstrap.subprocess.run", return_value=_completed("v18.0.0\n")): with pytest.raises(BootstrapError, match="18"): check_node() def test_check_node_ok() -> None: with patch("triggershell.bootstrap.shutil.which", return_value="/usr/bin/node"): with patch("triggershell.bootstrap.subprocess.run", return_value=_completed("v20.11.0\n")): assert check_node() == "20.11.0" def test_ensure_dependencies_installed_skips_when_stamp_matches(tmp_path: Path) -> None: app_dir = tmp_path / "app" app_dir.mkdir() lockfile = app_dir / "pnpm-lock.yaml" lockfile.write_text("lockfile contents") node_modules = app_dir / "node_modules" node_modules.mkdir() import hashlib stamp = node_modules / ".triggershell-install-stamp" stamp.write_text(hashlib.sha256(lockfile.read_bytes()).hexdigest()) with patch("triggershell.bootstrap.subprocess.run") as run: ensure_dependencies_installed(app_dir) run.assert_not_called() def test_ensure_dependencies_installed_runs_when_stale(tmp_path: Path) -> None: app_dir = tmp_path / "app" app_dir.mkdir() (app_dir / "pnpm-lock.yaml").write_text("v1") with patch("triggershell.bootstrap.subprocess.run", return_value=_completed(returncode=0)) as run: ensure_dependencies_installed(app_dir) run.assert_called_once() def test_needs_build_true_when_no_stamp(tmp_path: Path) -> None: app_dir = tmp_path / "app" (app_dir / "src").mkdir(parents=True) config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") assert needs_build(app_dir, config_path) is True def test_needs_build_false_after_stamp_recorded(tmp_path: Path) -> None: app_dir = tmp_path / "app" (app_dir / "src").mkdir(parents=True) config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") record_build_stamp(app_dir, config_path) assert needs_build(app_dir, config_path) is False def test_needs_build_true_after_config_touched(tmp_path: Path) -> None: import os import time app_dir = tmp_path / "app" (app_dir / "src").mkdir(parents=True) config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") record_build_stamp(app_dir, config_path) assert needs_build(app_dir, config_path) is False time.sleep(0.01) config_path.write_text("scripts: []\n# touched") future = time.time() + 5 os.utime(config_path, (future, future)) assert needs_build(app_dir, config_path) is True def test_load_dotenv_for_config_noop_when_missing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import os monkeypatch.delenv("TRIGGERSHELL_SESSION_SECRET", raising=False) config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") load_dotenv_for_config(config_path) assert "TRIGGERSHELL_SESSION_SECRET" not in os.environ def test_load_dotenv_for_config_loads_values(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import os monkeypatch.delenv("TRIGGERSHELL_SESSION_SECRET", raising=False) config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") (tmp_path / ".env").write_text("TRIGGERSHELL_SESSION_SECRET=from-dotenv-file\n") load_dotenv_for_config(config_path) assert os.environ["TRIGGERSHELL_SESSION_SECRET"] == "from-dotenv-file" def test_load_dotenv_for_config_does_not_override_existing_env( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("TRIGGERSHELL_SESSION_SECRET", "from-shell") config_path = tmp_path / "triggershell.yml" config_path.write_text("scripts: []") (tmp_path / ".env").write_text("TRIGGERSHELL_SESSION_SECRET=from-dotenv-file\n") load_dotenv_for_config(config_path) import os assert os.environ["TRIGGERSHELL_SESSION_SECRET"] == "from-shell"