CUDA toolkit detection also checks CUDA_HOME, CUDA_PATH and /usr/local/cuda*, and the backend order names only backends the installed build compiled in. An explicit IBus pin on niri is honoured with one warning. The update checker queries this fork's Gitea releases and packaged installs make no network request. The one-time legacy import repoints a migrated dictionary path at the copied file.
280 lines
12 KiB
Python
280 lines
12 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
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
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"
|
|
|
|
|
|
#: A fixed old timestamp, so any write to the legacy file moves its mtime visibly.
|
|
_OLD_MTIME_NS = 1_000_000_000_000_000_000
|
|
|
|
|
|
def _import_with_stored_dictionary_path(
|
|
monkeypatch, config_root: Path, data_root: Path, stored_path: str
|
|
) -> Path:
|
|
"""Seed a legacy install whose config stores ``stored_path``, then import it.
|
|
|
|
Returns the legacy dictionary file. The ConfigManager paths are pointed at
|
|
the copied configuration, so nothing outside the scratch directories is read
|
|
or written.
|
|
"""
|
|
seeded = _seed_legacy(config_root, data_root)
|
|
seeded["config.json"].write_text(
|
|
json.dumps({"dictionary": {"enabled": True, "file_path": stored_path}}),
|
|
encoding="utf-8",
|
|
)
|
|
import_legacy_once()
|
|
_point_config_manager_at(monkeypatch, config_root / "voculinux")
|
|
return seeded["dictionary.txt"]
|
|
|
|
|
|
def _point_config_manager_at(monkeypatch, new_config_dir: Path) -> None:
|
|
"""Make ConfigManager read and write only the given (scratch) configuration dir."""
|
|
from vocalinux.ui import config_manager
|
|
|
|
monkeypatch.setattr(config_manager, "CONFIG_DIR", str(new_config_dir))
|
|
monkeypatch.setattr(config_manager, "CONFIG_FILE", str(new_config_dir / "config.json"))
|
|
|
|
|
|
def _add_term_through_migrated_config(monkeypatch, new_config_dir: Path, term: str) -> None:
|
|
"""Add a dictionary term exactly as the app does, using the migrated configuration."""
|
|
_point_config_manager_at(monkeypatch, new_config_dir)
|
|
from vocalinux.custom_dictionary import CustomDictionaryManager
|
|
from vocalinux.ui.config_manager import ConfigManager
|
|
|
|
assert CustomDictionaryManager(ConfigManager()).add_term(term) is True
|
|
|
|
|
|
@pytest.mark.parametrize("form", ["absolute-legacy", "tilde-legacy"])
|
|
def test_migrated_dictionary_edit_never_writes_the_legacy_file(monkeypatch, tmp_path, form) -> None:
|
|
"""The copied config points the dictionary at the copy, so an edit leaves legacy alone."""
|
|
if form == "tilde-legacy":
|
|
# The tilde form names the legacy file only when XDG_CONFIG_HOME is ~/.config.
|
|
home = tmp_path / "home"
|
|
monkeypatch.setenv("HOME", str(home))
|
|
config_root = home / ".config"
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(config_root))
|
|
data_root = tmp_path / "data"
|
|
monkeypatch.setenv("XDG_DATA_HOME", str(data_root))
|
|
stored = "~/.config/vocalinux/dictionary.txt"
|
|
else:
|
|
config_root, data_root = _isolate(monkeypatch, tmp_path)
|
|
stored = str(config_root / "vocalinux" / "dictionary.txt")
|
|
legacy_terms = _import_with_stored_dictionary_path(monkeypatch, config_root, data_root, stored)
|
|
os.utime(legacy_terms, ns=(_OLD_MTIME_NS, _OLD_MTIME_NS))
|
|
legacy_bytes = legacy_terms.read_bytes()
|
|
legacy_mtime = legacy_terms.stat().st_mtime_ns
|
|
|
|
from vocalinux.custom_dictionary import CustomDictionaryManager
|
|
from vocalinux.ui.config_manager import ConfigManager
|
|
|
|
dictionary = CustomDictionaryManager(ConfigManager())
|
|
assert dictionary.add_term("Kubernetes") is True
|
|
|
|
assert legacy_terms.read_bytes() == legacy_bytes
|
|
assert legacy_terms.stat().st_mtime_ns == legacy_mtime
|
|
new_terms = config_root / "voculinux" / "dictionary.txt"
|
|
assert "Kubernetes" in new_terms.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_dictionary_path_outside_the_legacy_directory_is_kept(monkeypatch, tmp_path) -> None:
|
|
"""A path the user chose outside the legacy directory is not rewritten."""
|
|
chosen = tmp_path / "shared" / "terms.txt"
|
|
chosen.parent.mkdir(parents=True)
|
|
chosen.write_text("Chosen\n", encoding="utf-8")
|
|
config_root, data_root = _isolate(monkeypatch, tmp_path)
|
|
_import_with_stored_dictionary_path(monkeypatch, config_root, data_root, str(chosen))
|
|
|
|
migrated = json.loads((config_root / "voculinux" / "config.json").read_text(encoding="utf-8"))
|
|
assert migrated["dictionary"]["file_path"] == str(chosen)
|
|
assert chosen.read_text(encoding="utf-8") == "Chosen\n"
|
|
|
|
|
|
def test_absent_legacy_dictionary_edit_lands_in_the_new_file(monkeypatch, tmp_path) -> None:
|
|
"""The default dictionary path was saved but the file was never created."""
|
|
config_root, _data_root = _isolate(monkeypatch, tmp_path)
|
|
legacy_config = config_root / "vocalinux"
|
|
legacy_config.mkdir(parents=True)
|
|
legacy_terms = legacy_config / "dictionary.txt"
|
|
(legacy_config / "config.json").write_text(
|
|
json.dumps({"dictionary": {"enabled": True, "file_path": str(legacy_terms)}}),
|
|
encoding="utf-8",
|
|
)
|
|
import_legacy_once()
|
|
|
|
_add_term_through_migrated_config(monkeypatch, config_root / "voculinux", "Kubernetes")
|
|
|
|
assert not legacy_terms.exists()
|
|
assert "Kubernetes" in (config_root / "voculinux" / "dictionary.txt").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
|
|
def test_existing_new_dictionary_takes_the_edit_and_legacy_stays_put(monkeypatch, tmp_path) -> None:
|
|
"""A new dictionary already present wins; the legacy file is never written."""
|
|
config_root, data_root = _isolate(monkeypatch, tmp_path)
|
|
seeded = _seed_legacy(config_root, data_root)
|
|
legacy_terms = seeded["dictionary.txt"]
|
|
seeded["config.json"].write_text(
|
|
json.dumps({"dictionary": {"enabled": True, "file_path": str(legacy_terms)}}),
|
|
encoding="utf-8",
|
|
)
|
|
new_config_dir = config_root / "voculinux"
|
|
new_config_dir.mkdir(parents=True)
|
|
(new_config_dir / "dictionary.txt").write_text("Mine\n", encoding="utf-8")
|
|
os.utime(legacy_terms, ns=(_OLD_MTIME_NS, _OLD_MTIME_NS))
|
|
legacy_bytes = legacy_terms.read_bytes()
|
|
legacy_mtime = legacy_terms.stat().st_mtime_ns
|
|
import_legacy_once()
|
|
|
|
_add_term_through_migrated_config(monkeypatch, new_config_dir, "Kubernetes")
|
|
|
|
assert legacy_terms.read_bytes() == legacy_bytes
|
|
assert legacy_terms.stat().st_mtime_ns == legacy_mtime
|
|
assert (new_config_dir / "dictionary.txt").read_text(encoding="utf-8") == "Mine\nKubernetes\n"
|