diff --git a/src/apm_cli/deps/lockfile.py b/src/apm_cli/deps/lockfile.py index dc939287d..439ce9bac 100644 --- a/src/apm_cli/deps/lockfile.py +++ b/src/apm_cli/deps/lockfile.py @@ -799,9 +799,13 @@ def to_yaml(self) -> str: # since the flat fields remain the source of truth in YAML. _self_dep = self.dependencies.pop(_SELF_KEY, None) try: + # ``generated_at`` is deliberately NOT written (issue #2572): a + # per-run timestamp in the lockfile is a constant source of merge + # conflicts across unrelated branches. The field is still parsed + # from legacy lockfiles so ``apm lock export`` keeps its timestamp + # fallback for existing projects. data: dict[str, Any] = { "lockfile_version": emit_version, - "generated_at": self.generated_at, } if self.apm_version: data["apm_version"] = self.apm_version @@ -1012,8 +1016,8 @@ def save(self, path: Path) -> None: def is_semantically_equivalent(self, other: LockFile) -> bool: """Return True if *other* has the same deps, MCP/LSP servers, and configs. - Ignores ``generated_at`` and ``apm_version`` so that a no-change - install does not dirty the lockfile. + Ignores ``apm_version`` so that a no-change install does not dirty + the lockfile. """ if self.lockfile_version != other.lockfile_version: return False diff --git a/src/apm_cli/integration/mcp_integrator.py b/src/apm_cli/integration/mcp_integrator.py index 24dd05aa8..216987b34 100644 --- a/src/apm_cli/integration/mcp_integrator.py +++ b/src/apm_cli/integration/mcp_integrator.py @@ -19,7 +19,6 @@ import shutil import warnings from collections.abc import MutableMapping -from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING @@ -850,7 +849,9 @@ def update_lockfile( ): _log.debug("MCP lockfile unchanged -- skipping write") return - lockfile.generated_at = datetime.now(timezone.utc).isoformat() + # ``generated_at`` is not stamped anymore (issue #2572): the field + # is no longer written to the lockfile, so setting it here would + # be dead state. lockfile.save(lock_path) except Exception as exc: _log.debug( diff --git a/tests/unit/install/test_mcp_lockfile_determinism.py b/tests/unit/install/test_mcp_lockfile_determinism.py index 4cb20d322..00e889291 100644 --- a/tests/unit/install/test_mcp_lockfile_determinism.py +++ b/tests/unit/install/test_mcp_lockfile_determinism.py @@ -135,10 +135,7 @@ def _run_lockfile_phase_and_mcp_persist( ctx.package_types = {dep_key: package_type} _FixedDatetime.instant = instant - with ( - patch("apm_cli.deps.lockfile.datetime", _FixedDatetime), - patch("apm_cli.integration.mcp_integrator.datetime", _FixedDatetime), - ): + with patch("apm_cli.deps.lockfile.datetime", _FixedDatetime): LockfileBuilder(ctx).build_and_save() mcp_deps = package.get_mcp_dependencies() MCPIntegrator.update_lockfile( @@ -239,7 +236,9 @@ def test_unchanged_local_instructions_do_not_rewrite_lockfile(tmp_path: Path) -> first_bytes = lock_path.read_bytes() first_lock = LockFile.read(lock_path) assert first_lock is not None - assert first_lock.generated_at == first_instant.isoformat() + # ``generated_at`` must not be written anymore (#2572): a per-run timestamp + # in the lockfile is a constant source of merge conflicts. + assert "generated_at" not in lock_path.read_text() assert first_lock.local_deployed_files == [".github/instructions/local.instructions.md"] _run_lockfile_phase_and_local_persist(tmp_path, second_instant) @@ -247,7 +246,6 @@ def test_unchanged_local_instructions_do_not_rewrite_lockfile(tmp_path: Path) -> second_lock = LockFile.read(lock_path) assert second_lock is not None - assert second_lock.generated_at == first_lock.generated_at assert second_lock.local_deployed_file_hashes == first_lock.local_deployed_file_hashes assert second_bytes == first_bytes @@ -263,14 +261,13 @@ def test_unchanged_mcp_dependencies_do_not_rewrite_lockfile(tmp_path: Path) -> N first_bytes = lock_path.read_bytes() first_lock = LockFile.read(lock_path) assert first_lock is not None - assert first_lock.generated_at == first_instant.isoformat() + assert "generated_at" not in lock_path.read_text() _run_lockfile_phase_and_mcp_persist(tmp_path, package, second_instant) second_bytes = lock_path.read_bytes() second_lock = LockFile.read(lock_path) assert second_lock is not None - assert second_lock.generated_at == first_lock.generated_at assert second_bytes == first_bytes @@ -288,7 +285,7 @@ def test_unchanged_mcp_target_servers_do_not_rewrite_lockfile(tmp_path: Path) -> first_bytes = lock_path.read_bytes() first_lock = LockFile.read(lock_path) assert first_lock is not None - assert first_lock.generated_at == first_instant.isoformat() + assert "generated_at" not in lock_path.read_text() assert first_lock.mcp_target_servers == target_servers second_context = _run_lockfile_phase_and_mcp_persist( @@ -298,7 +295,6 @@ def test_unchanged_mcp_target_servers_do_not_rewrite_lockfile(tmp_path: Path) -> second_lock = LockFile.read(lock_path) assert second_lock is not None - assert second_lock.generated_at == first_lock.generated_at assert second_lock.mcp_target_servers == target_servers assert second_bytes == first_bytes deployment_rows = { @@ -350,7 +346,7 @@ def track_changed_write(lockfile: LockFile, path: Path) -> None: changed_lock = LockFile.read(lock_path) assert changed_lock is not None assert changed_writes == [lock_path] - assert changed_lock.generated_at == second_instant.isoformat() + assert "generated_at" not in lock_path.read_text() assert changed_lock.mcp_target_servers == changed_targets converged_writes: list[Path] = [] @@ -454,7 +450,7 @@ def track_repair(lockfile: LockFile, path: Path) -> None: repaired = LockFile.read(lock_path) assert repaired is not None assert repair_writes == [lock_path] - assert repaired.generated_at == second_instant.isoformat() + assert "generated_at" not in lock_path.read_text() assert repaired.mcp_config_provenance == {} repaired_bytes = lock_path.read_bytes() @@ -640,7 +636,7 @@ def test_changed_mcp_dependencies_update_lockfile(tmp_path: Path) -> None: second_lock = LockFile.read(lock_path) assert second_lock is not None - assert second_lock.generated_at == second_instant.isoformat() + assert "generated_at" not in lock_path.read_text() assert second_lock.mcp_servers == ["github"] assert second_lock.mcp_configs == { "github": { @@ -664,14 +660,13 @@ def test_unchanged_lsp_dependencies_do_not_rewrite_lockfile(tmp_path: Path) -> N first_bytes = lock_path.read_bytes() first_lock = LockFile.read(lock_path) assert first_lock is not None - assert first_lock.generated_at == first_instant.isoformat() + assert "generated_at" not in lock_path.read_text() _run_lockfile_phase_and_lsp_persist(tmp_path, package, second_instant) second_bytes = lock_path.read_bytes() second_lock = LockFile.read(lock_path) assert second_lock is not None - assert second_lock.generated_at == first_lock.generated_at assert second_lock.lsp_servers == first_lock.lsp_servers assert second_lock.lsp_configs == first_lock.lsp_configs assert second_bytes == first_bytes