diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 69da1da8a..7c19892a0 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -697,6 +697,23 @@ def _validate_and_add_packages_to_apm_yml( return validated_packages, outcome +def _prepare_dry_run_manifest_path( + manifest_path: Path, + *, + dry_run: bool, + user_scope: bool, + has_packages: bool, +): + """Redirect an absent user manifest to temporary storage during previews.""" + if not (dry_run and user_scope and has_packages and not manifest_path.exists()): + return manifest_path, None + + import tempfile + + temp_dir = tempfile.TemporaryDirectory(prefix="apm-dry-run-") + return Path(temp_dir.name) / manifest_path.name, temp_dir + + # --------------------------------------------------------------------------- # MCP CLI helpers (W3 --mcp flag) # --------------------------------------------------------------------------- @@ -1176,6 +1193,7 @@ def install( # noqa: PLR0913 logger = None command_result: InstallResult | None = None transaction: InstallTransaction | None = None + dry_run_manifest_tmp = None from ..install.service import InstallService try: @@ -1423,7 +1441,8 @@ def install( # noqa: PLR0913 scope = InstallScope.USER if global_ else InstallScope.PROJECT if scope is InstallScope.USER: - ensure_user_dirs() + if not dry_run: + ensure_user_dirs() logger.progress("Installing to user scope (~/.apm/)") _scope_warn = warn_unsupported_user_scope() if _scope_warn: @@ -1434,6 +1453,12 @@ def install( # noqa: PLR0913 apm_dir = get_apm_dir(scope) # Display name for messages (short for project scope, full for user scope) manifest_display = str(manifest_path) if scope is InstallScope.USER else APM_YML_FILENAME + manifest_path, dry_run_manifest_tmp = _prepare_dry_run_manifest_path( + manifest_path, + dry_run=dry_run, + user_scope=scope is InstallScope.USER, + has_packages=bool(packages), + ) # Project root for integration (used by both dep and local integration) from ..core.scope import get_deploy_root @@ -1471,7 +1496,10 @@ def install( # noqa: PLR0913 if manifest_targets := manifest_targets_from_target_option(target): config["targets"] = manifest_targets _create_minimal_apm_yml(config, target_path=manifest_path) - logger.success(f"Created {manifest_display}") + if dry_run: + logger.progress(f"Dry run: Would create {manifest_display}") + else: + logger.success(f"Created {manifest_display}") if manifest_targets: logger.progress( f"Targets set: {', '.join(manifest_targets)} (persisted to {manifest_display})" @@ -1645,6 +1673,8 @@ def install( # noqa: PLR0913 _root_redirect.__exit__(None, None, None) if transaction is not None: transaction.__exit__(*sys.exc_info()) + if dry_run_manifest_tmp is not None: + dry_run_manifest_tmp.cleanup() # F5 (#1116): render minimal elapsed-time line on exit paths that # did not already render the full install summary. Best-effort: # never let a render failure mask the original exception/exit. diff --git a/tests/unit/regressions/test_issue_2549_global_dry_run.py b/tests/unit/regressions/test_issue_2549_global_dry_run.py new file mode 100644 index 000000000..6c664a122 --- /dev/null +++ b/tests/unit/regressions/test_issue_2549_global_dry_run.py @@ -0,0 +1,34 @@ +from apm_cli.commands.install import _prepare_dry_run_manifest_path + + +def test_global_dry_run_redirects_absent_manifest_without_creating_user_state(tmp_path): + user_manifest = tmp_path / "home" / ".apm" / "apm.yml" + + preview_manifest, temp_dir = _prepare_dry_run_manifest_path( + user_manifest, + dry_run=True, + user_scope=True, + has_packages=True, + ) + try: + assert preview_manifest != user_manifest + assert preview_manifest.name == "apm.yml" + assert not user_manifest.parent.exists() + finally: + temp_dir.cleanup() + + +def test_existing_user_manifest_is_read_in_place(tmp_path): + user_manifest = tmp_path / ".apm" / "apm.yml" + user_manifest.parent.mkdir(parents=True) + user_manifest.write_text("name: test\n", encoding="utf-8") + + preview_manifest, temp_dir = _prepare_dry_run_manifest_path( + user_manifest, + dry_run=True, + user_scope=True, + has_packages=True, + ) + + assert preview_manifest == user_manifest + assert temp_dir is None