fix(template): use sys.stdin.readline() for snapshot confirm prompt

input() with a prompt string on some terminals (zsh + Linux Mint) echoes
^M and the prompt doesn't flush before reading, causing the prompt to
appear stuck or not accept input.

Replace with explicit sys.stdout.write()+flush() + sys.stdin.readline()
which guarantees the prompt is flushed before blocking on stdin, and
readline() + .strip() handles both \n and \r\n line endings correctly.

Tests updated to use CliRunner(input=) which populates sys.stdin inside
the runner context (monkeypatching sys.stdin.readline directly is ignored
by CliRunner which replaces sys.stdin entirely).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:07:36 +02:00
parent 258ef3a7e7
commit 5d3214d10c
2 changed files with 7 additions and 6 deletions
+5 -2
View File
@@ -60,6 +60,7 @@ import json
import shutil import shutil
import socket import socket
import subprocess import subprocess
import sys
import threading import threading
import time import time
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -671,8 +672,10 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' `
if recreate_snapshot: if recreate_snapshot:
do_recreate = True do_recreate = True
else: else:
raw = input(f"[prepare-win] Delete and recreate '{snapshot}'? [y/N] ") sys.stdout.write(f"[prepare-win] Delete and recreate '{snapshot}'? [y/N] ")
do_recreate = raw.strip().rstrip("\r").lower() in ("y", "yes") sys.stdout.flush()
raw = sys.stdin.readline()
do_recreate = raw.strip().lower() in ("y", "yes")
if not do_recreate: if not do_recreate:
click.echo("[prepare-win] Keeping existing snapshot. Provisioning complete.") click.echo("[prepare-win] Keeping existing snapshot. Provisioning complete.")
return return
+2 -4
View File
@@ -613,8 +613,6 @@ def test_prepare_win_snapshot_exists_skip(
backend.list_snapshots.return_value = ["BaseClean"] backend.list_snapshots.return_value = ["BaseClean"]
transport = _make_transport() transport = _make_transport()
_patch_prepare(monkeypatch, backend, transport) _patch_prepare(monkeypatch, backend, transport)
monkeypatch.setattr("builtins.input", lambda _prompt="": "N")
result = CliRunner().invoke( result = CliRunner().invoke(
cli, cli,
[ [
@@ -624,6 +622,7 @@ def test_prepare_win_snapshot_exists_skip(
"--admin-password", "adminpass", "--admin-password", "adminpass",
"--build-password", "buildpass", "--build-password", "buildpass",
], ],
input="N\n",
) )
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
assert "Keeping existing snapshot" in result.output assert "Keeping existing snapshot" in result.output
@@ -644,8 +643,6 @@ def test_prepare_win_snapshot_exists_recreate(
monkeypatch.setattr(tmpl_mod, "_wait_tcp", lambda *_: True) monkeypatch.setattr(tmpl_mod, "_wait_tcp", lambda *_: True)
monkeypatch.setattr(tmpl_mod, "_wait_winrm", lambda *_: None) monkeypatch.setattr(tmpl_mod, "_wait_winrm", lambda *_: None)
monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _: None) monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "Y")
def _sp_run(cmd: list[str], **_: object) -> None: def _sp_run(cmd: list[str], **_: object) -> None:
sp_calls.append(cmd) sp_calls.append(cmd)
@@ -660,6 +657,7 @@ def test_prepare_win_snapshot_exists_recreate(
"--admin-password", "adminpass", "--admin-password", "adminpass",
"--build-password", "buildpass", "--build-password", "buildpass",
], ],
input="Y\n",
) )
assert result.exit_code == 0, result.output assert result.exit_code == 0, result.output
assert any("deleteSnapshot" in " ".join(c) for c in sp_calls) assert any("deleteSnapshot" in " ".join(c) for c in sp_calls)