The fork takes its own name everywhere a user can see it: binaries (voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus namespace (com.voculinux.Voculinux, no alias for the old name), config/data/cache dirs, single-instance lock, IBus engine identity, tray icons, resource prefixes, VOCULINUX_* environment names, and the update-checker user agent. The internal Python module stays vocalinux and the distribution metadata keeps the upstream URLs as provenance. On first run a previous Vocalinux install's config.json, custom dictionary, and portal restore token are copied once into the new locations (new config always wins, no merge, no write-back); models re-resolve through the pinned-checksum download path. An offline naming-sweep test proves no old-name strings remain on user-visible surfaces outside the allowlist. Refs #18
148 lines
5.8 KiB
Python
148 lines
5.8 KiB
Python
"""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"
|