"""Ticket #18: one-time legacy Vocalinux import into the Voculinux locations. Seams under test (all offline, no hardware): - A fresh run with a legacy install present imports ``config.json``, the custom dictionary, and the portal restore token exactly once and logs it; a second run imports nothing. - A fresh run with an existing new configuration never imports, merges, or writes to the legacy location. - A fresh run with no legacy install starts from defaults (nothing imported). - Models are not migrated. """ import json from pathlib import Path from vocalinux import legacy_import from vocalinux.legacy_import import import_legacy_once def _isolate(monkeypatch, tmp_path: Path) -> tuple[Path, Path]: """Point XDG roots at scratch dirs; return (config_root, data_root).""" config_root = tmp_path / "config" data_root = tmp_path / "data" monkeypatch.setenv("XDG_CONFIG_HOME", str(config_root)) monkeypatch.setenv("XDG_DATA_HOME", str(data_root)) return config_root, data_root def _seed_legacy(config_root: Path, data_root: Path) -> dict[str, Path]: """Create a legacy install's payload; return the created files.""" legacy_config = config_root / "vocalinux" legacy_data = data_root / "vocalinux" legacy_config.mkdir(parents=True) legacy_data.mkdir(parents=True) created = {} for name, content in ( ("config.json", '{"general": {"first_run": false}}'), ("dictionary.txt", "Supabase\n"), ("custom-dictionary-corrections.json", json.dumps({"version": 1, "corrections": []})), ): target = legacy_config / name target.write_text(content, encoding="utf-8") created[name] = target token = legacy_data / "remote_desktop_restore_token" token.write_text("legacy-token\n", encoding="utf-8") created["remote_desktop_restore_token"] = token return created def test_fresh_run_with_legacy_install_imports_once(monkeypatch, tmp_path, caplog) -> None: """Legacy config, dictionary, and restore token copy over exactly once.""" config_root, data_root = _isolate(monkeypatch, tmp_path) _seed_legacy(config_root, data_root) with caplog.at_level("INFO", logger="vocalinux.legacy_import"): imported = import_legacy_once() assert len(imported) == 4 assert ( config_root / "voculinux" / "config.json" ).read_text() == '{"general": {"first_run": false}}' assert (config_root / "voculinux" / "dictionary.txt").read_text() == "Supabase\n" assert json.loads( (config_root / "voculinux" / "custom-dictionary-corrections.json").read_text() ) == { "version": 1, "corrections": [], } assert ( data_root / "voculinux" / "remote_desktop_restore_token" ).read_text() == "legacy-token\n" assert any("Legacy Vocalinux import complete" in r.message for r in caplog.records) # A second run imports nothing. assert import_legacy_once() == [] def test_existing_new_config_never_imports_or_touches_legacy(monkeypatch, tmp_path) -> None: """An existing new configuration always wins: no merge, no write-back.""" config_root, data_root = _isolate(monkeypatch, tmp_path) seeded = _seed_legacy(config_root, data_root) before = {name: path.read_bytes() for name, path in seeded.items()} new_config = config_root / "voculinux" new_config.mkdir(parents=True) (new_config / "config.json").write_text('{"general": {"first_run": true}}', encoding="utf-8") assert import_legacy_once() == [] assert (new_config / "config.json").read_text() == '{"general": {"first_run": true}}' assert not (new_config / "dictionary.txt").exists() assert not (data_root / "voculinux").exists() for name, path in seeded.items(): assert path.read_bytes() == before[name], name def test_no_legacy_install_starts_from_defaults(monkeypatch, tmp_path) -> None: """No legacy directories means nothing is imported.""" _isolate(monkeypatch, tmp_path) assert import_legacy_once() == [] def test_models_are_not_migrated(monkeypatch, tmp_path) -> None: """A legacy models directory is left alone; rung files re-download.""" config_root, data_root = _isolate(monkeypatch, tmp_path) _seed_legacy(config_root, data_root) legacy_models = data_root / "vocalinux" / "models" legacy_models.mkdir(parents=True) (legacy_models / "ggml-large-v3-turbo-q8_0.bin").write_bytes(b"fake-model") imported = import_legacy_once() assert not (data_root / "voculinux" / "models").exists() assert {Path(p).name for p in imported} == { "config.json", "dictionary.txt", "custom-dictionary-corrections.json", "remote_desktop_restore_token", } def test_partial_legacy_payload_imports_what_exists(monkeypatch, tmp_path) -> None: """A legacy install with only a config still imports just that file.""" config_root, _data_root = _isolate(monkeypatch, tmp_path) legacy_config = config_root / "vocalinux" legacy_config.mkdir(parents=True) (legacy_config / "config.json").write_text("{}", encoding="utf-8") imported = import_legacy_once() assert imported == [str(config_root / "voculinux" / "config.json")] assert import_legacy_once() == [] def test_new_dictionary_never_overwritten_by_legacy(monkeypatch, tmp_path) -> None: """A new dictionary file is never clobbered even when config is absent.""" config_root, data_root = _isolate(monkeypatch, tmp_path) _seed_legacy(config_root, data_root) new_config = config_root / "voculinux" new_config.mkdir(parents=True) (new_config / "dictionary.txt").write_text("Mine\n", encoding="utf-8") import_legacy_once() # No new config.json exists, so the import ran — but the existing new # dictionary file must survive untouched. assert (new_config / "dictionary.txt").read_text() == "Mine\n" assert legacy_import.CONFIG_FILENAME == "config.json"