| """GitHub push pure-builder tests β argv shape, idempotency, secret hygiene.""" |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import pytest |
|
|
| from mindxtrain.deploy.github_push import ( |
| GithubConfig, |
| bootstrap_steps, |
| missing_env, |
| remote_url, |
| required_env, |
| status_missing, |
| status_target, |
| write_sha_file, |
| ) |
|
|
|
|
| def _cfg(**overrides: str) -> GithubConfig: |
| base = { |
| "token": "ghp_TESTTOKEN", |
| "repo": "professor-codephreak/mindXtrain", |
| "branch": "main", |
| "author_name": "mindXtrain bot", |
| "author_email": "noreply@pythai.net", |
| } |
| base.update(overrides) |
| return GithubConfig(**base) |
|
|
|
|
| def test_required_env_lists_token_and_repo() -> None: |
| assert "GITHUB_TOKEN" in required_env() |
| assert "GITHUB_REPO" in required_env() |
|
|
|
|
| def test_missing_env_returns_unset_keys() -> None: |
| miss = missing_env({"GITHUB_TOKEN": "x"}) |
| assert "GITHUB_REPO" in miss |
| assert "GITHUB_TOKEN" not in miss |
| full = missing_env({"GITHUB_TOKEN": "x", "GITHUB_REPO": "o/r"}) |
| assert full == [] |
|
|
|
|
| def test_status_target_uses_repo_env() -> None: |
| assert status_target({"GITHUB_REPO": "owner/repo"}) == "owner/repo" |
| assert status_target({}) == "" |
|
|
|
|
| def test_remote_url_is_https() -> None: |
| assert remote_url("owner/repo") == "https://github.com/owner/repo.git" |
|
|
|
|
| def test_bootstrap_steps_emits_all_phases() -> None: |
| steps = bootstrap_steps(_cfg()) |
| labels = [s.label for s in steps] |
| |
| |
| |
| assert labels == [ |
| "probe-git", |
| "git-init", |
| "probe-repo", |
| "gh-create", |
| "probe-remote", |
| "git-remote-add", |
| "git-add", |
| "probe-stage", |
| "git-commit", |
| "git-push", |
| "head-sha", |
| ] |
|
|
|
|
| def test_token_never_appears_in_argv() -> None: |
| """Secret hygiene: $GH_TOKEN is in env, never in argv. Catches a regression |
| where a future refactor inlines the token into a credential URL.""" |
| steps = bootstrap_steps(_cfg(token="ghp_SUPERSECRET")) |
| for step in steps: |
| for arg in step.cmd: |
| assert "ghp_SUPERSECRET" not in arg, f"token leaked into {step.label}: {arg!r}" |
|
|
|
|
| def test_token_is_in_env_for_push_step() -> None: |
| steps = bootstrap_steps(_cfg(token="ghp_SUPERSECRET")) |
| push = next(s for s in steps if s.label == "git-push") |
| assert push.env["GH_TOKEN"] == "ghp_SUPERSECRET" |
| assert push.env["GITHUB_TOKEN"] == "ghp_SUPERSECRET" |
|
|
|
|
| def test_force_with_lease_only_when_force_true() -> None: |
| soft = bootstrap_steps(_cfg(), force=False) |
| push_soft = next(s for s in soft if s.label == "git-push") |
| assert "--force-with-lease" not in push_soft.cmd |
|
|
| hard = bootstrap_steps(_cfg(), force=True) |
| push_hard = next(s for s in hard if s.label == "git-push") |
| assert "--force-with-lease" in push_hard.cmd |
|
|
|
|
| def test_commit_uses_dash_c_flags_to_avoid_global_gitconfig() -> None: |
| """The commit step injects user.email/name via `git -c β¦` so we never |
| write to ~/.gitconfig (would surprise the user). Pin the contract.""" |
| steps = bootstrap_steps(_cfg(author_email="ci@x.com", author_name="ci")) |
| commit = next(s for s in steps if s.label == "git-commit") |
| assert "-c" in commit.cmd |
| assert "user.email=ci@x.com" in commit.cmd |
| assert "user.name=ci" in commit.cmd |
|
|
|
|
| def test_push_uses_credential_helper_not_url_token() -> None: |
| """The push command must pass auth via credential.helper, not by |
| embedding the token into the remote URL (which would land in `.git/config` |
| or `git reflog`).""" |
| steps = bootstrap_steps(_cfg(token="ghp_SUPERSECRET")) |
| push = next(s for s in steps if s.label == "git-push") |
| |
| assert not any("@github.com" in arg and "x-access-token" in arg for arg in push.cmd) |
| |
| helper = [arg for arg in push.cmd if "credential.helper=" in arg] |
| assert helper, "git-push step is missing the credential.helper inline config" |
|
|
|
|
| def test_predicate_wiring_for_conditional_steps() -> None: |
| steps = {s.label: s for s in bootstrap_steps(_cfg())} |
| assert steps["git-init"].predicate_step == "probe-git" |
| assert steps["gh-create"].predicate_step == "probe-repo" |
| assert steps["gh-create"].predicate_rc_in == (1,) |
| assert steps["git-commit"].predicate_step == "probe-stage" |
| assert steps["git-commit"].predicate_rc_in == (1,) |
| |
| for label in ("probe-git", "probe-repo", "probe-remote", "probe-stage"): |
| assert steps[label].allow_failure, f"{label} must allow failure" |
|
|
|
|
| def test_capture_stdout_on_head_sha_step() -> None: |
| steps = bootstrap_steps(_cfg()) |
| sha = next(s for s in steps if s.label == "head-sha") |
| assert sha.capture_stdout, "head-sha must capture stdout for manifest.git_sha" |
|
|
|
|
| def test_write_sha_file_persists_to_disk(tmp_path: Path) -> None: |
| target = write_sha_file(tmp_path, "abc123\n") |
| assert target == tmp_path / "git_sha.txt" |
| assert target.read_text().strip() == "abc123" |
|
|
|
|
| def test_status_missing_includes_missing_binaries(monkeypatch: pytest.MonkeyPatch) -> None: |
| """If gh is not on PATH, status_missing surfaces it so the UI can disable |
| the button + show 'install gh'.""" |
| monkeypatch.setattr("shutil.which", lambda _b: None) |
| miss = status_missing({"GITHUB_TOKEN": "x", "GITHUB_REPO": "o/r"}) |
| assert "git" in miss |
| assert "gh" in miss |
|
|