125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""The fork-upstream prompt must never block a non-interactive update (#60240).
|
|
|
|
`_sync_with_upstream_if_needed` asks "Add official repo as 'upstream' remote?"
|
|
on fork checkouts with no upstream remote. In unattended contexts (CI, cron,
|
|
the desktop updater hand-off) stdin is open but nobody answers, so a bare
|
|
``input()`` blocks forever. These tests pin the gate: under ``assume_yes`` or
|
|
a non-TTY stdio pair the prompt is skipped as a decline WITHOUT persisting the
|
|
skip marker or touching git remotes, and both update call sites forward the
|
|
interaction state.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import update_cmd
|
|
|
|
|
|
@pytest.fixture
|
|
def fork_without_upstream(tmp_path):
|
|
with patch.object(
|
|
update_cmd, "_has_upstream_remote", return_value=False
|
|
), patch.object(
|
|
update_cmd, "_should_skip_upstream_prompt", return_value=False
|
|
), patch.object(
|
|
update_cmd, "_add_upstream_remote", return_value=True
|
|
) as add_remote, patch.object(
|
|
update_cmd, "_mark_skip_upstream_prompt"
|
|
) as mark_skip, patch("builtins.input") as stdin_input:
|
|
yield SimpleNamespace(
|
|
cwd=tmp_path,
|
|
add_remote=add_remote,
|
|
mark_skip=mark_skip,
|
|
stdin_input=stdin_input,
|
|
)
|
|
|
|
|
|
def _tty(stdin: bool, stdout: bool):
|
|
return (
|
|
patch.object(update_cmd.sys.stdin, "isatty", return_value=stdin),
|
|
patch.object(update_cmd.sys.stdout, "isatty", return_value=stdout),
|
|
)
|
|
|
|
|
|
class TestUpstreamPromptNonInteractive:
|
|
def test_assume_yes_skips_prompt_without_touching_remotes(
|
|
self, fork_without_upstream, capsys
|
|
):
|
|
p_in, p_out = _tty(True, True)
|
|
with p_in, p_out:
|
|
checked = update_cmd._sync_with_upstream_if_needed(
|
|
["git"], fork_without_upstream.cwd, assume_yes=True
|
|
)
|
|
|
|
assert checked is False
|
|
fork_without_upstream.stdin_input.assert_not_called()
|
|
fork_without_upstream.add_remote.assert_not_called()
|
|
fork_without_upstream.mark_skip.assert_not_called()
|
|
assert "Skipping upstream setup" in capsys.readouterr().out
|
|
|
|
@pytest.mark.parametrize(
|
|
"stdin_tty,stdout_tty", [(False, False), (False, True), (True, False)]
|
|
)
|
|
def test_non_tty_skips_prompt_without_persisting_decline(
|
|
self, fork_without_upstream, capsys, stdin_tty, stdout_tty
|
|
):
|
|
p_in, p_out = _tty(stdin_tty, stdout_tty)
|
|
with p_in, p_out:
|
|
checked = update_cmd._sync_with_upstream_if_needed(
|
|
["git"], fork_without_upstream.cwd
|
|
)
|
|
|
|
assert checked is False
|
|
fork_without_upstream.stdin_input.assert_not_called()
|
|
fork_without_upstream.add_remote.assert_not_called()
|
|
fork_without_upstream.mark_skip.assert_not_called()
|
|
assert "Skipping upstream setup" in capsys.readouterr().out
|
|
|
|
def test_gateway_prompt_routes_through_input_fn(self, fork_without_upstream):
|
|
prompts = []
|
|
|
|
def gw_input(prompt, default=""):
|
|
prompts.append((prompt, default))
|
|
return "n"
|
|
|
|
p_in, p_out = _tty(False, False)
|
|
with p_in, p_out:
|
|
update_cmd._sync_with_upstream_if_needed(
|
|
["git"], fork_without_upstream.cwd, input_fn=gw_input
|
|
)
|
|
|
|
assert prompts == [("Add official repo as 'upstream' remote? [y/N]", "n")]
|
|
fork_without_upstream.stdin_input.assert_not_called()
|
|
fork_without_upstream.add_remote.assert_not_called()
|
|
fork_without_upstream.mark_skip.assert_called_once_with()
|
|
|
|
def test_interactive_decline_still_persists_marker(self, fork_without_upstream):
|
|
fork_without_upstream.stdin_input.return_value = "n"
|
|
p_in, p_out = _tty(True, True)
|
|
with p_in, p_out:
|
|
update_cmd._sync_with_upstream_if_needed(
|
|
["git"], fork_without_upstream.cwd
|
|
)
|
|
|
|
fork_without_upstream.stdin_input.assert_called_once()
|
|
fork_without_upstream.add_remote.assert_not_called()
|
|
fork_without_upstream.mark_skip.assert_called_once_with()
|
|
|
|
def test_interactive_accept_adds_upstream(self, fork_without_upstream):
|
|
fork_without_upstream.stdin_input.return_value = "y"
|
|
with patch.object(
|
|
update_cmd, "_count_commits_between", return_value=-1
|
|
), patch.object(update_cmd.subprocess, "run"):
|
|
p_in, p_out = _tty(True, True)
|
|
with p_in, p_out:
|
|
update_cmd._sync_with_upstream_if_needed(
|
|
["git"], fork_without_upstream.cwd
|
|
)
|
|
|
|
fork_without_upstream.add_remote.assert_called_once_with(
|
|
["git"], fork_without_upstream.cwd
|
|
)
|
|
fork_without_upstream.mark_skip.assert_not_called()
|