From 258ef3a7e7037a6820c174bea71db710e5b50f7e Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 24 May 2026 02:02:22 +0200 Subject: [PATCH] fix(template): replace click.confirm with input() to avoid ^M on Linux terminals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit click.confirm() doesn't strip \r — "y\r" doesn't match "y" and the prompt loops or fails. Replace with raw input().strip().rstrip('\r').lower() which handles both \n and \r\n line endings. Also add --recreate-snapshot flag to skip the interactive prompt entirely (useful for scripted re-provisioning). Update snapshot_exists_skip/recreate tests to monkeypatch builtins.input instead of using CliRunner input=. Co-Authored-By: Claude Sonnet 4.6 --- src/ci_orchestrator/commands/template.py | 10 ++++++- tests/python/test_commands_template.py | 37 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ci_orchestrator/commands/template.py b/src/ci_orchestrator/commands/template.py index 14043bc..52c568c 100644 --- a/src/ci_orchestrator/commands/template.py +++ b/src/ci_orchestrator/commands/template.py @@ -366,6 +366,8 @@ def _run_with_heartbeat( help="Store build-user credentials in the system keyring after provisioning.") @click.option("--credential-target", default="BuildVMGuest", show_default=True, help="Keyring target name for the stored credential.") +@click.option("--recreate-snapshot", is_flag=True, + help="Delete and recreate the snapshot if it already exists (no prompt).") def template_prepare_win( vmx: str, guest_ip: str, @@ -382,6 +384,7 @@ def template_prepare_win( auth: str, store_credential: bool, credential_target: str, + recreate_snapshot: bool, ) -> None: """Provision a Windows template VM via pypsrp (Linux-host compatible). @@ -665,7 +668,12 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' ` existing = backend.list_snapshots(handle) if snapshot in existing: click.echo(f"[prepare-win] Snapshot '{snapshot}' already exists.") - if not click.confirm(f"Delete and recreate '{snapshot}'?", default=False): + if recreate_snapshot: + do_recreate = True + else: + raw = input(f"[prepare-win] Delete and recreate '{snapshot}'? [y/N] ") + do_recreate = raw.strip().rstrip("\r").lower() in ("y", "yes") + if not do_recreate: click.echo("[prepare-win] Keeping existing snapshot. Provisioning complete.") return click.echo(f"[prepare-win] Deleting snapshot '{snapshot}' ...") diff --git a/tests/python/test_commands_template.py b/tests/python/test_commands_template.py index 4b0e77e..09c1733 100644 --- a/tests/python/test_commands_template.py +++ b/tests/python/test_commands_template.py @@ -613,8 +613,8 @@ def test_prepare_win_snapshot_exists_skip( backend.list_snapshots.return_value = ["BaseClean"] transport = _make_transport() _patch_prepare(monkeypatch, backend, transport) + monkeypatch.setattr("builtins.input", lambda _prompt="": "N") - # User declines deletion with "N" result = CliRunner().invoke( cli, [ @@ -624,7 +624,6 @@ def test_prepare_win_snapshot_exists_skip( "--admin-password", "adminpass", "--build-password", "buildpass", ], - input="N\n", ) assert result.exit_code == 0, result.output assert "Keeping existing snapshot" in result.output @@ -645,6 +644,7 @@ def test_prepare_win_snapshot_exists_recreate( monkeypatch.setattr(tmpl_mod, "_wait_tcp", lambda *_: True) monkeypatch.setattr(tmpl_mod, "_wait_winrm", 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: sp_calls.append(cmd) @@ -660,13 +660,44 @@ def test_prepare_win_snapshot_exists_recreate( "--admin-password", "adminpass", "--build-password", "buildpass", ], - input="Y\n", ) assert result.exit_code == 0, result.output assert any("deleteSnapshot" in " ".join(c) for c in sp_calls) assert any("snapshot" in " ".join(c) and "deleteSnapshot" not in " ".join(c) for c in sp_calls) +def test_prepare_win_recreate_snapshot_flag( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """--recreate-snapshot skips the interactive prompt and deletes+recreates.""" + vmx = _setup_prepare_env(tmp_path) + monkeypatch.chdir(tmp_path) + backend = _make_backend([True, False]) + backend.list_snapshots.return_value = ["BaseClean"] + transport = _make_transport() + sp_calls: list[list[str]] = [] + _patch_prepare(monkeypatch, backend, transport) + + def _sp_run(cmd: list[str], **_: object) -> None: + sp_calls.append(cmd) + + monkeypatch.setattr(tmpl_mod.subprocess, "run", _sp_run) + + result = CliRunner().invoke( + cli, + [ + "template", "prepare-win", + "--vmx", str(vmx), + "--ip", "192.168.1.100", + "--admin-password", "adminpass", + "--build-password", "buildpass", + "--recreate-snapshot", + ], + ) + assert result.exit_code == 0, result.output + assert any("deleteSnapshot" in " ".join(c) for c in sp_calls) + + def test_prepare_win_skip_flags( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: