From a39283bf09aad36e27fd128faad1dda614b6d4de Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:49:37 -0700 Subject: [PATCH] test(docker): assert boot migration keeps .env byte-identical across reboots Adds the #51579 regression test the issue asked for: run the real docker_config_migrate.py boot path twice (host-reboot scenario under --restart unless-stopped) and assert $HERMES_HOME/.env survives byte-for-byte and the second boot is a no-op (no re-migration, no new backup). Exercises real migrate_config + real file I/O via subprocess. --- tests/tools/test_docker_config_migrate.py | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/tools/test_docker_config_migrate.py b/tests/tools/test_docker_config_migrate.py index 05e4f38bcc6..fc9b2531042 100644 --- a/tests/tools/test_docker_config_migrate.py +++ b/tests/tools/test_docker_config_migrate.py @@ -202,3 +202,58 @@ def test_docker_config_migrate_restores_backups_when_version_does_not_advance( assert config_path.read_text(encoding="utf-8") == original_config assert env_path.read_text(encoding="utf-8") == original_env + + +def test_docker_config_migrate_second_boot_preserves_env_byte_for_byte(tmp_path: Path) -> None: + """Regression for #51579: booting ``gateway run`` twice (i.e. a host + reboot under ``--restart unless-stopped``) must not strip or rewrite + ``$HERMES_HOME/.env``. The first boot migrates the stale config and bumps + ``_config_version``; the second boot must be a no-op that leaves ``.env`` + byte-identical to what the user supplied. + + This exercises the real script + real ``migrate_config`` + real file I/O + via subprocess — not mocks — so it covers the actual Docker boot path, + not just the failure-rollback shapes above. + """ + config_path = tmp_path / "config.yaml" + env_path = tmp_path / ".env" + config_path.write_text( + yaml.safe_dump( + { + "_config_version": 11, + "gateway": {"provider": "telegram"}, + } + ), + encoding="utf-8", + ) + original_env = ( + "TELEGRAM_BOT_TOKEN=secret-bot-token\n" + "TELEGRAM_ALLOWED_USERS=123456789\n" + "OPENROUTER_API_KEY=sk-test-provider-key\n" + ) + env_path.write_text(original_env, encoding="utf-8") + env_bytes_before = env_path.read_bytes() + + # ── First boot: stale config migrates, version advances. ── + first = _run_migration(tmp_path) + assert first.returncode == 0, first.stderr + assert "Migrating config schema 11 ->" in first.stdout + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) + assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"] + # The token (and every other credential) must survive the migration. + assert env_path.exists(), ".env must never be deleted by the boot migration" + assert env_path.read_bytes() == env_bytes_before + + config_after_first = config_path.read_bytes() + first_boot_backups = sorted(tmp_path.glob("config.yaml.bak-*")) + + # ── Second boot (host reboot): version is current, must be a no-op. ── + second = _run_migration(tmp_path) + assert second.returncode == 0, second.stderr + assert "Migrating config schema" not in second.stdout + # .env is still present and byte-for-byte identical to the original. + assert env_path.exists() + assert env_path.read_bytes() == env_bytes_before + # config.yaml is untouched by the second boot, and no new backup is made. + assert config_path.read_bytes() == config_after_first + assert sorted(tmp_path.glob("config.yaml.bak-*")) == first_boot_backups