feat(identity): rename user-visible identity to voculinux with one-time legacy import

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
This commit is contained in:
soubarnak
2026-11-06 01:29:39 +05:30
parent c565e6d946
commit ea7be5f111
251 changed files with 2449 additions and 1991 deletions
+7 -7
View File
@@ -19,11 +19,11 @@ import pytest
os.environ["PYTEST_RUNNING"] = "1"
# Isolate the whole test process from the developer's real desktop session
# and Vocalinux state, mirroring a fresh CI runner:
# and Voculinux state, mirroring a fresh CI runner:
# * XDG base dirs point at a throwaway directory, so locally downloaded
# models (~/.local/share/vocalinux/models) and the real config file
# models (~/.local/share/voculinux/models) and the real config file
# never leak into tests. This must happen at conftest import time:
# vocalinux modules freeze paths like MODELS_DIR at first import during
# voculinux modules freeze paths like MODELS_DIR at first import during
# collection, before any fixture could run. The directory is shared for
# the whole session on purpose — some tests rely on directories that
# earlier tests created, exactly as they do on CI with the runner's HOME.
@@ -31,7 +31,7 @@ os.environ["PYTEST_RUNNING"] = "1"
# session flips IBus/keyboard code paths (issue #478) that CI never sees.
# Tests that need specific values set them via patch.dict/monkeypatch, which
# layer on top of this.
_XDG_ISOLATION_DIR = tempfile.mkdtemp(prefix="vocalinux-pytest-xdg-")
_XDG_ISOLATION_DIR = tempfile.mkdtemp(prefix="voculinux-pytest-xdg-")
os.environ["XDG_DATA_HOME"] = os.path.join(_XDG_ISOLATION_DIR, "data")
os.environ["XDG_CONFIG_HOME"] = os.path.join(_XDG_ISOLATION_DIR, "config")
os.environ["XDG_CACHE_HOME"] = os.path.join(_XDG_ISOLATION_DIR, "cache")
@@ -53,7 +53,7 @@ try:
except ImportError:
_real_numpy = None
else:
sys._vocalinux_real_numpy = _real_numpy
sys._voculinux_real_numpy = _real_numpy
# And the same for zipfile: test_recognition_manager.py and
# test_recognition_manager_download_buffer.py assign a MagicMock to
@@ -62,7 +62,7 @@ else:
# reachable for the ones that need it.
import zipfile as _real_zipfile # noqa: E402
sys._vocalinux_real_zipfile = _real_zipfile
sys._voculinux_real_zipfile = _real_zipfile
# Prevent specific known-blocking daemon threads from starting during tests.
# Source code in ibus_engine.py and evdev_backend.py spawns daemon threads
@@ -105,7 +105,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")
# Always mock GI/GTK modules before any test files are collected.
# On CI, real gi (PyGObject) is installed via "pip install PyGObject" and
# module-level imports in vocalinux source files (ibus_engine, tray_indicator,
# module-level imports in voculinux source files (ibus_engine, tray_indicator,
# settings_dialog, etc.) will trigger GTK initialization or IBus daemon
# connections that hang indefinitely in headless environments.
# pytest-timeout cannot interrupt these C-level blocking calls.
+22 -22
View File
@@ -1,6 +1,6 @@
# Distribution Testing Checklist
Use this checklist when testing Vocalinux on a new Linux distribution.
Use this checklist when testing Voculinux on a new Linux distribution.
## Pre-Installation
@@ -51,22 +51,22 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
- [ ] Check for errors during installation
- [ ] Verify virtual environment was created
```bash
ls -la ~/.local/share/vocalinux/venv/
ls -la ~/.local/share/voculinux/venv/
```
- [ ] Check wrapper scripts exist
```bash
ls -la ~/.local/bin/vocalinux*
ls -la ~/.local/bin/voculinux*
```
- [ ] Check desktop entry was created
```bash
ls -la ~/.local/share/applications/vocalinux.desktop
ls -la ~/.local/share/applications/voculinux.desktop
```
- [ ] Check icons were installed
```bash
ls -la ~/.local/share/icons/hicolor/scalable/apps/vocalinux*
ls -la ~/.local/share/icons/hicolor/scalable/apps/voculinux*
```
## Post-Installation
@@ -74,7 +74,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
### Launch from Terminal
- [ ] Launch from terminal
```bash
~/.local/bin/vocalinux-gui
~/.local/bin/voculinux-gui
```
- [ ] Check for startup errors
@@ -154,7 +154,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
- [ ] Settings persist after restart
- [ ] Config file is created correctly
```bash
cat ~/.config/vocalinux/config.json
cat ~/.config/voculinux/config.json
```
### Model Download (if using Whisper)
@@ -162,7 +162,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
- [ ] Download progress is shown
- [ ] Model is saved to correct location
```bash
ls -la ~/.local/share/vocalinux/models/
ls -la ~/.local/share/voculinux/models/
```
### Error Handling
@@ -179,7 +179,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
- [ ] Memory usage is reasonable
```bash
# Check memory usage
ps aux | grep vocalinux
ps aux | grep voculinux
```
- [ ] CPU usage is reasonable during idle
@@ -190,17 +190,17 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
- [ ] Application can be quit cleanly
- [ ] No processes left running
```bash
ps aux | grep vocalinux
ps aux | grep voculinux
```
- [ ] Uninstallation works
```bash
bash ~/.local/share/vocalinux/uninstall.sh # if exists
bash ~/.local/share/voculinux/uninstall.sh # if exists
# or manual cleanup
rm -rf ~/.local/share/vocalinux
rm -rf ~/.config/vocalinux
rm ~/.local/share/applications/vocalinux.desktop
rm ~/.local/bin/vocalinux*
rm -rf ~/.local/share/voculinux
rm -rf ~/.config/voculinux
rm ~/.local/share/applications/voculinux.desktop
rm ~/.local/bin/voculinux*
```
## Known Issues
@@ -262,7 +262,7 @@ After completing the checklist:
3. **Attach logs** if there were errors:
- Installer output
- Application output from terminal
- Journal logs: `journalctl --user -u vocalinux -b`
- Journal logs: `journalctl --user -u voculinux -b`
4. **Mention any workarounds** you found
@@ -293,13 +293,13 @@ pactl list sources short # PulseAudio
arecord -l # ALSA
# Check for running processes
ps aux | grep vocalinux
ps aux | grep voculinux
# Check memory usage
ps aux | grep vocalinux | awk '{print $6}'
ps aux | grep voculinux | awk '{print $6}'
# Check logs
journalctl --user -b | grep vocalinux
journalctl --user -b | grep voculinux
# Run dependency checker
bash scripts/check-system-deps.sh
@@ -308,7 +308,7 @@ bash scripts/check-system-deps.sh
bash -n install.sh
# Verify installation
ls -la ~/.local/bin/vocalinux*
ls -la ~/.local/share/vocalinux/
ls -la ~/.config/vocalinux/
ls -la ~/.local/bin/voculinux*
ls -la ~/.local/share/voculinux/
ls -la ~/.config/voculinux/
```
+41 -26
View File
@@ -7,7 +7,7 @@ Seams under test (per spec Testing Decisions):
the rename ticket must flip (or deliberately keep) without network access.
No user-visible behaviour, name, path, or string changes: all asserted values
are the pre-rename Vocalinux/vocalinux literals.
are the pre-rename Voculinux/voculinux literals.
"""
import ast
@@ -26,27 +26,27 @@ def _load_identity():
def test_central_values_are_current_names():
"""Shared definition still carries the pre-rename literals."""
identity = _load_identity()
assert identity.APP_NAME == "Vocalinux"
assert identity.APP_ID == "vocalinux"
assert identity.APP_DIR_NAME == "vocalinux"
assert identity.CLI_PROG == "vocalinux"
assert identity.BUS_NAME == "com.vocalinux.Vocalinux"
assert identity.OBJECT_PATH == "/com/vocalinux/Vocalinux"
assert identity.INTERFACE_NAME == "com.vocalinux.Vocalinux"
assert identity.DESKTOP_FILENAME == "vocalinux.desktop"
assert identity.DESKTOP_ICON_NAME == "vocalinux"
assert identity.ENGINE_NAME == "vocalinux"
assert identity.ENGINE_LONGNAME == "Vocalinux"
assert identity.COMPONENT_NAME == "org.freedesktop.IBus.Vocalinux"
assert identity.IBUS_DIR_NAME == "vocalinux-ibus"
assert identity.UPDATE_CHECKER_USER_AGENT == "Vocalinux-UpdateChecker"
assert identity.RESOURCE_DIR_NAME == "vocalinux"
assert identity.VERIFICATION_STAMP_NAME == ".vocalinux_verified"
assert identity.LOG_FILE_PREFIX == "vocalinux_logs_"
assert identity.PORTAL_TOKEN_PREFIX == "vocalinux_"
assert identity.ENV_FORCE_BACKEND == "VOCALINUX_FORCE_BACKEND"
assert identity.ENV_INJECT_MODIFIER_WAIT == "VOCALINUX_INJECT_MODIFIER_WAIT"
assert identity.ENV_YDOTOOL_KEY_DELAY == "VOCALINUX_YDOTOOL_KEY_DELAY"
assert identity.APP_NAME == "Voculinux"
assert identity.APP_ID == "voculinux"
assert identity.APP_DIR_NAME == "voculinux"
assert identity.CLI_PROG == "voculinux"
assert identity.BUS_NAME == "com.voculinux.Voculinux"
assert identity.OBJECT_PATH == "/com/voculinux/Voculinux"
assert identity.INTERFACE_NAME == "com.voculinux.Voculinux"
assert identity.DESKTOP_FILENAME == "voculinux.desktop"
assert identity.DESKTOP_ICON_NAME == "voculinux"
assert identity.ENGINE_NAME == "voculinux"
assert identity.ENGINE_LONGNAME == "Voculinux"
assert identity.COMPONENT_NAME == "org.freedesktop.IBus.Voculinux"
assert identity.IBUS_DIR_NAME == "voculinux-ibus"
assert identity.UPDATE_CHECKER_USER_AGENT == "Voculinux-UpdateChecker"
assert identity.RESOURCE_DIR_NAME == "voculinux"
assert identity.VERIFICATION_STAMP_NAME == ".voculinux_verified"
assert identity.LOG_FILE_PREFIX == "voculinux_logs_"
assert identity.PORTAL_TOKEN_PREFIX == "voculinux_"
assert identity.ENV_FORCE_BACKEND == "VOCULINUX_FORCE_BACKEND"
assert identity.ENV_INJECT_MODIFIER_WAIT == "VOCULINUX_INJECT_MODIFIER_WAIT"
assert identity.ENV_YDOTOOL_KEY_DELAY == "VOCULINUX_YDOTOOL_KEY_DELAY"
def _imports_app_identity(path: Path) -> bool:
@@ -108,13 +108,13 @@ def test_source_pinned_literals_reserved_for_rename():
"""
about = (SRC_ROOT / "ui" / "settings_about.py").read_text(encoding="utf-8")
for literal in (
'VOCALINUX_SITE_URL = "https://vocalinux.com"',
'title="Vocalinux"',
'get_icon_path("vocalinux")',
'VOCULINUX_SITE_URL = "https://voculinux.com"',
'title="Voculinux"',
'get_icon_path("voculinux")',
):
assert literal in about, literal
pad = (SRC_ROOT / "ui" / "dictation_pad.py").read_text(encoding="utf-8")
assert 'window.set_title("Vocalinux Dictation Pad")' in pad
assert 'window.set_title("Voculinux Dictation Pad")' in pad
def test_rename_inventory_files_exist():
@@ -134,3 +134,18 @@ def test_retained_inventory_records_internal_names():
import vocalinux
assert vocalinux.__name__ == "vocalinux"
def test_legacy_names_recorded_and_unclaimed():
"""The old install identity is recorded for the one-time import only.
No compatibility alias: the legacy D-Bus name differs from the claimed
one, and no source claims it.
"""
identity = _load_identity()
assert identity.LEGACY_APP_DIR_NAME == "vocalinux"
assert identity.LEGACY_APP_DIR_NAME != identity.APP_DIR_NAME
assert identity.LEGACY_BUS_NAME == "com.vocalinux.Vocalinux"
assert identity.LEGACY_BUS_NAME != identity.BUS_NAME
service = (SRC_ROOT / "dbus_service.py").read_text(encoding="utf-8")
assert identity.LEGACY_BUS_NAME not in service
+14 -14
View File
@@ -1,6 +1,6 @@
"""The AppImage reuses its pywhispercpp Vulkan build only when the pins match.
The compile does not depend on the Vocalinux commit. The cache id is the base
The compile does not depend on the Voculinux commit. The cache id is the base
image, tool_checksums.txt, the pinned cmake and ninja, the Vulkan flags, and
the pywhispercpp pin. It is not the git SHA and not the Actions run id. A tree
without libggml-vulkan.so is a CPU wheel and is not cached.
@@ -39,8 +39,8 @@ def _run(args: list[str], env: dict[str, str] | None = None) -> subprocess.Compl
def _cache_id(pins: Path, install: Path, extra: dict[str, str] | None = None) -> str:
env = os.environ.copy()
env["VOCALINUX_PINS"] = str(pins)
env["VOCALINUX_INSTALL_SH"] = str(install)
env["VOCULINUX_PINS"] = str(pins)
env["VOCULINUX_INSTALL_SH"] = str(install)
if extra:
env.update(extra)
result = _run(["bash", str(ID_SCRIPT)], env=env)
@@ -103,7 +103,7 @@ def test_native_cache_id_tracks_build_tools_and_compile_flags(tmp_path: Path) ->
tools = tmp_path / "appimage-tools.txt"
tools_text = TOOLS.read_text(encoding="utf-8")
tools.write_text(tools_text.replace("cmake", "cmake-other", 1), encoding="utf-8")
changed_tools = _cache_id(PINS, INSTALL_SH, extra={"VOCALINUX_APPIMAGE_TOOLS": str(tools)})
changed_tools = _cache_id(PINS, INSTALL_SH, extra={"VOCULINUX_APPIMAGE_TOOLS": str(tools)})
assert changed_tools != baseline
commented = tmp_path / "build-comment.sh"
@@ -111,7 +111,7 @@ def test_native_cache_id_tracks_build_tools_and_compile_flags(tmp_path: Path) ->
commented.write_text(
build_text.replace("# GPU:", "# GPU: unchanged flags\n# GPU:", 1), encoding="utf-8"
)
assert _cache_id(PINS, INSTALL_SH, extra={"VOCALINUX_BUILD_SH": str(commented)}) == baseline
assert _cache_id(PINS, INSTALL_SH, extra={"VOCULINUX_BUILD_SH": str(commented)}) == baseline
flagged = tmp_path / "build-flags.sh"
flagged.write_text(
@@ -122,7 +122,7 @@ def test_native_cache_id_tracks_build_tools_and_compile_flags(tmp_path: Path) ->
),
encoding="utf-8",
)
assert _cache_id(PINS, INSTALL_SH, extra={"VOCALINUX_BUILD_SH": str(flagged)}) != baseline
assert _cache_id(PINS, INSTALL_SH, extra={"VOCULINUX_BUILD_SH": str(flagged)}) != baseline
def test_native_cache_id_does_not_read_the_commit_or_the_run() -> None:
@@ -307,11 +307,11 @@ def test_a_failed_restore_runs_the_compile_command(tmp_path: Path) -> None:
def test_native_cache_dir_binds_the_arch_to_that_id(tmp_path: Path) -> None:
env = os.environ.copy()
env["VOCALINUX_APPIMAGE_CACHE"] = str(tmp_path / "cache")
env.pop("VOCALINUX_PINS", None)
env.pop("VOCALINUX_INSTALL_SH", None)
env.pop("VOCALINUX_APPIMAGE_TOOLS", None)
env.pop("VOCALINUX_BUILD_SH", None)
env["VOCULINUX_APPIMAGE_CACHE"] = str(tmp_path / "cache")
env.pop("VOCULINUX_PINS", None)
env.pop("VOCULINUX_INSTALL_SH", None)
env.pop("VOCULINUX_APPIMAGE_TOOLS", None)
env.pop("VOCULINUX_BUILD_SH", None)
result = _source("pywhispercpp_native_cache_dir x86_64", env=env)
assert result.returncode == 0, result.stderr
expected = _cache_id(PINS, INSTALL_SH)
@@ -331,7 +331,7 @@ def test_build_reuses_the_cached_vulkan_tree_before_compiling() -> None:
match = re.search(r"\nrebuild_pywhispercpp_vulkan\(\) \{(.+?)\n\}\n", text, re.S)
assert match, "rebuild_pywhispercpp_vulkan disappeared"
body = match.group(1)
assert body.index("VOCALINUX_APPIMAGE_SKIP_VULKAN") < body.index("pywhispercpp_restore_or_run")
assert body.index("VOCULINUX_APPIMAGE_SKIP_VULKAN") < body.index("pywhispercpp_restore_or_run")
assert "GGML_VULKAN=1" not in body
assert "_compile_pywhispercpp_vulkan" in body
compile_fn = re.search(r"\n_compile_pywhispercpp_vulkan\(\) \{(.+?)\n\}\n", text, re.S)
@@ -349,7 +349,7 @@ def test_appimage_workflows_share_one_native_cache_key() -> None:
keys = []
for name in APPIMAGE_WORKFLOWS:
text = (WORKFLOWS / name).read_text(encoding="utf-8")
found = re.findall(r"path: ~/.cache/vocalinux-appimage\n\s+key: (.+)\n", text)
found = re.findall(r"path: ~/.cache/voculinux-appimage\n\s+key: (.+)\n", text)
assert found, name
assert all(key == NATIVE_KEY for key in found), found
for token in ("github.sha", "github.run_id", "github.run_number", "github.run_attempt"):
@@ -366,7 +366,7 @@ def test_appimage_workflows_share_one_native_cache_key() -> None:
for token in ("github.sha", "github.run_id", "github.run_number", "github.run_attempt"):
assert token not in step, step
assert "packaging/appimage/docker-build.sh" in text
assert 'VOCALINUX_APPIMAGE_REQUIRE_VULKAN: "1"' in text
assert 'VOCULINUX_APPIMAGE_REQUIRE_VULKAN: "1"' in text
assert "aarch64" in text
keys.extend(found)
assert len(keys) == 5
+4 -4
View File
@@ -113,7 +113,7 @@ def test_running_from_appimage_follows_appdir(monkeypatch):
monkeypatch.delenv("APPDIR", raising=False)
assert not running_from_appimage()
monkeypatch.setenv("APPDIR", "/tmp/.mount_vocalinux")
monkeypatch.setenv("APPDIR", "/tmp/.mount_voculinux")
assert running_from_appimage()
@@ -185,8 +185,8 @@ def test_appimage_build_rebuilds_pywhispercpp_with_vulkan():
text = BUILD_SH.read_text()
assert "GGML_VULKAN=1" in text
assert "libggml-vulkan" in text
assert "VOCALINUX_APPIMAGE_REQUIRE_VULKAN" in text
assert "VOCALINUX_APPIMAGE_SKIP_VULKAN" in text
assert "VOCULINUX_APPIMAGE_REQUIRE_VULKAN" in text
assert "VOCULINUX_APPIMAGE_SKIP_VULKAN" in text
assert "rebuild_pywhispercpp_vulkan" in text
assert 'CC="${CC:-gcc}"' in text
assert 'CXX="${CXX:-g++}"' in text
@@ -345,7 +345,7 @@ def test_vulkan_shaders_get_the_compiler_the_base_image_lacks():
for workflow in APPIMAGE_WORKFLOWS:
content = (WORKFLOWS / workflow).read_text(encoding="utf-8")
assert (
"vocalinux-appimage" in content
"voculinux-appimage" in content
), f"{workflow} does not cache the built glslc; every run rebuilds it"
+12 -12
View File
@@ -44,9 +44,9 @@ def test_local_gate_isolates_outputs_and_retains_failed_diagnostics(
checkout = tmp_path / "checkout"
scripts = checkout / "scripts"
scripts.mkdir(parents=True)
template = checkout / "packaging/aur/vocalinux"
template = checkout / "packaging/aur/voculinux"
template.mkdir(parents=True)
shutil.copy2(ROOT / "packaging/aur/vocalinux/PKGBUILD", template / "PKGBUILD")
shutil.copy2(ROOT / "packaging/aur/voculinux/PKGBUILD", template / "PKGBUILD")
shutil.copy2(ROOT / "scripts/prepare_aur_source.py", scripts / "prepare_aur_source.py")
# Keep the real cleanup policy: shared dist/ would reject these outputs.
@@ -78,22 +78,22 @@ def test_local_gate_isolates_outputs_and_retains_failed_diagnostics(
if os.environ['AUR_GATE_TEST_FAILURE'] == 'build':
sys.exit(31)
args.output.mkdir(parents=True, exist_ok=True)
with tarfile.open(args.output / 'vocalinux-0.18.4.tar.gz', 'w:gz') as archive:
metadata = b'Name: vocalinux\\nVersion: 0.18.4\\n'
member = tarfile.TarInfo('vocalinux-0.18.4/PKG-INFO')
with tarfile.open(args.output / 'voculinux-0.18.4.tar.gz', 'w:gz') as archive:
metadata = b'Name: voculinux\\nVersion: 0.18.4\\n'
member = tarfile.TarInfo('voculinux-0.18.4/PKG-INFO')
member.size = len(metadata)
archive.addfile(member, io.BytesIO(metadata))
(args.output / 'vocalinux-0.18.4-py3-none-any.whl').write_text('new wheel')
(args.output / 'voculinux-0.18.4-py3-none-any.whl').write_text('new wheel')
"""))
# Existing packaging outputs, wheel diagnostics and dev tooling are not ours.
protected = {}
for name in (
"dist/vocalinux-0.18.4-py3-none-any.whl",
"dist/vocalinux-0.18.4.tar.gz",
"dist/Vocalinux-0.18.4-x86_64.AppImage",
"dist/vocalinux_0.18.4_amd64.deb",
"dist/vocalinux-0.18.4.x86_64.rpm",
"dist/voculinux-0.18.4-py3-none-any.whl",
"dist/voculinux-0.18.4.tar.gz",
"dist/Voculinux-0.18.4-x86_64.AppImage",
"dist/voculinux_0.18.4_amd64.deb",
"dist/voculinux-0.18.4.x86_64.rpm",
"build/reproducible-wheel/canonical.log",
".venv/keep.txt",
):
@@ -133,7 +133,7 @@ def test_local_gate_isolates_outputs_and_retains_failed_diagnostics(
source = next(arg.removesuffix(':/aur-source:ro') for arg in sys.argv
if arg.endswith(':/aur-source:ro'))
assert (Path(source) / 'PKGBUILD').is_file()
assert (Path(source) / 'vocalinux-0.18.4.tar.gz').is_file()
assert (Path(source) / 'voculinux-0.18.4.tar.gz').is_file()
assert 'AUR_SOURCE=/aur-source' in sys.argv
with open(os.environ['AUR_GATE_TEST_LOG'], 'a') as log:
log.write(json.dumps(['arch', source]) + '\\n')
+22 -22
View File
@@ -11,8 +11,8 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
AUR = REPO_ROOT / "packaging" / "aur"
PKGBUILD = AUR / "vocalinux" / "PKGBUILD"
BIN_DIR = AUR / "vocalinux-bin"
PKGBUILD = AUR / "voculinux" / "PKGBUILD"
BIN_DIR = AUR / "voculinux-bin"
BIN_PKGBUILD = BIN_DIR / "PKGBUILD"
GATE_SH = AUR / "build-test.sh"
WORKFLOWS = REPO_ROOT / ".github" / "workflows"
@@ -190,7 +190,7 @@ def test_the_smoke_walks_the_pinned_deps():
assert "requirements/runtime.txt" in script
assert "--require-hashes" in script
assert "pacman -U" in script and "vocalinux --version" in script
assert "pacman -U" in script and "voculinux --version" in script
# Arch name -> PyPI name for the export the gate installs from.
pypi_names = {"python-pywhispercpp": "pywhispercpp", "python-pynput": "pynput"}
for arch_name, pypi_name in pypi_names.items():
@@ -369,7 +369,7 @@ def test_bin_package_ships_the_release_appimage_per_arch() -> None:
text = _bin_pkgbuild()
assert "arch=('x86_64' 'aarch64')" in text
for arch in ("x86_64", "aarch64"):
asset = f"Vocalinux-${{_tag}}-{arch}.AppImage"
asset = f"Voculinux-${{_tag}}-{arch}.AppImage"
assert (
f'"{asset}::https://github.com/VocaHQ/vocalinux/releases/download/'
f"v${{_tag}}/{asset}" in text
@@ -383,19 +383,19 @@ def test_bin_package_installs_the_appimage_intact_and_runnable() -> None:
text = _bin_pkgbuild()
assert "options=('!strip')" in text
assert "'fuse2'" in text
assert 'install -Dm755 "Vocalinux-${_tag}-${CARCH}.AppImage"' in text
assert 'install -Dm755 "Voculinux-${_tag}-${CARCH}.AppImage"' in text
assert '"/opt/${pkgname}/' in text
assert "ln -s" in text and '"${pkgdir}/usr/bin/vocalinux"' in text
assert "ln -s" in text and '"${pkgdir}/usr/bin/voculinux"' in text
def test_bin_package_conflicts_with_the_source_and_git_packages() -> None:
"""All three packages ship /usr/bin/vocalinux; provides= lets a depends on
'vocalinux' resolve to the -bin install."""
"""All three packages ship /usr/bin/voculinux; provides= lets a depends on
'voculinux' resolve to the -bin install."""
text = _bin_pkgbuild()
assert "provides=('vocalinux')" in text
assert "provides=('voculinux')" in text
conflicts = re.search(r"conflicts=\(([^)]*)\)", text)
assert conflicts, "vocalinux-bin has no conflicts=()"
for name in ("'vocalinux'", "'vocalinux-git'"):
assert conflicts, "voculinux-bin has no conflicts=()"
for name in ("'voculinux'", "'voculinux-git'"):
assert name in conflicts.group(1)
@@ -405,8 +405,8 @@ def test_bin_pkgbuild_bundled_files_match_the_repo_copies() -> None:
outside the bundle). Guarded byte-for-byte so a fix to a canonical file
cannot leave the -bin package shipping the stale one."""
for name, canonical in (
("vocalinux.desktop", REPO_ROOT / "vocalinux.desktop"),
("vocalinux.svg", REPO_ROOT / "resources" / "icons" / "scalable" / "vocalinux.svg"),
("voculinux.desktop", REPO_ROOT / "voculinux.desktop"),
("voculinux.svg", REPO_ROOT / "resources" / "icons" / "scalable" / "voculinux.svg"),
("LICENSE", REPO_ROOT / "LICENSE"),
):
bundled = BIN_DIR / name
@@ -415,10 +415,10 @@ def test_bin_pkgbuild_bundled_files_match_the_repo_copies() -> None:
f"{name} drifted from {canonical.relative_to(REPO_ROOT)};" " sync the bundled copy"
)
assert f"'{name}'" in _bin_pkgbuild(), f"{name} not in source=()"
desktop = (BIN_DIR / "vocalinux.desktop").read_text(encoding="utf-8")
desktop = (BIN_DIR / "voculinux.desktop").read_text(encoding="utf-8")
# The names package() links into /usr/bin and installs under hicolor.
assert re.search(r"^Exec=vocalinux$", desktop, re.M)
assert re.search(r"^Icon=vocalinux$", desktop, re.M)
assert re.search(r"^Exec=voculinux$", desktop, re.M)
assert re.search(r"^Icon=voculinux$", desktop, re.M)
def test_bin_package_asks_the_host_only_for_what_the_bundle_lacks() -> None:
@@ -436,7 +436,7 @@ def test_bin_package_asks_the_host_only_for_what_the_bundle_lacks() -> None:
assert "'wtype'" in depends.group(1)
lines = text.splitlines()
start = next((i for i, line in enumerate(lines) if line.startswith("optdepends=(")), None)
assert start is not None, "vocalinux-bin has no optdepends=()"
assert start is not None, "voculinux-bin has no optdepends=()"
end = next(i for i in range(start + 1, len(lines)) if lines[i].strip() == ")")
optdepends = "\n".join(lines[start + 1 : end])
for tool in ("ibus", "xclip", "wl-clipboard", "ydotool"):
@@ -445,7 +445,7 @@ def test_bin_package_asks_the_host_only_for_what_the_bundle_lacks() -> None:
def test_bin_pkgbuild_tracks_the_release_tag_like_the_source_one() -> None:
"""Same pkgver/_tag split as packaging/aur/vocalinux, real digests for
"""Same pkgver/_tag split as packaging/aur/voculinux, real digests for
the published AppImages, and the release workflow must bump and publish
both — a -bin left on the last tag would serve the previous release's
AppImage."""
@@ -455,14 +455,14 @@ def test_bin_pkgbuild_tracks_the_release_tag_like_the_source_one() -> None:
# Local builds must verify payloads against real sums, not SKIP.
assert "'SKIP'" not in text
release = RELEASE.read_text(encoding="utf-8")
assert "packaging/aur/vocalinux-bin/PKGBUILD" in release, (
assert "packaging/aur/voculinux-bin/PKGBUILD" in release, (
"release.yml bumps the source PKGBUILD only; the -bin package would"
" keep installing the previous release's AppImage"
)
# The publish step's own updpkgsums only refreshes the x86_64 array, so
# release.yml pins both per-arch digests itself right before publishing.
assert re.search(r"for arch in x86_64 aarch64[\s\S]*sha256sums_\$\{arch\}", release), (
"release.yml must pin per-arch AppImage digests for vocalinux-bin — "
"release.yml must pin per-arch AppImage digests for voculinux-bin — "
"the publish action's updpkgsums only covers the runner's arch"
)
# The aarch64 AppImage lands in a later job; the -bin publish must wait
@@ -472,12 +472,12 @@ def test_bin_pkgbuild_tracks_the_release_tag_like_the_source_one() -> None:
), "publish-aur-bin must need build-appimage-arm64 so the aarch64 asset exists"
# The -bin PKGBUILD names local sources; a lone-PKGBUILD publish leaves
# users unable to build. asset_dir mirrors the whole directory to AUR.
assert "asset_dir: packaging/aur/vocalinux-bin" in release
assert "asset_dir: packaging/aur/voculinux-bin" in release
def test_the_gate_builds_the_bin_package_too() -> None:
"""The -bin package needs build validation as much as the source one:
the gate stubs the release AppImage and runs makepkg on it."""
script = GATE_SH.read_text(encoding="utf-8")
assert "vocalinux-bin" in script
assert "voculinux-bin" in script
assert "--skipchecksums" in script
+1 -1
View File
@@ -678,7 +678,7 @@ class TestAutoPauseConfigDefaults(unittest.TestCase):
from vocalinux.ui.config_manager import ConfigManager
with tempfile.TemporaryDirectory() as tmp:
config_dir = os.path.join(tmp, "vocalinux")
config_dir = os.path.join(tmp, "voculinux")
config_file = os.path.join(config_dir, "config.json")
os.makedirs(config_dir, exist_ok=True)
+11 -11
View File
@@ -15,10 +15,10 @@ class TestAutostartManager(unittest.TestCase):
def test_get_exec_command_prefers_installed_command(self):
with patch(
"vocalinux.ui.autostart_manager.shutil.which",
return_value="/usr/bin/vocalinux",
return_value="/usr/bin/voculinux",
):
command = autostart_manager.get_exec_command()
self.assertEqual(command, "/usr/bin/vocalinux --start-minimized")
self.assertEqual(command, "/usr/bin/voculinux --start-minimized")
def test_get_exec_command_falls_back_to_python_module(self):
with (
@@ -31,22 +31,22 @@ class TestAutostartManager(unittest.TestCase):
def test_get_exec_command_flatpak_uses_flatpak_run(self):
# Inside a Flatpak the host session manager must use the flatpak launcher,
# which is always on PATH, rather than /app/bin/vocalinux.
with patch.dict("os.environ", {"FLATPAK_ID": "com.vocalinux.Vocalinux"}, clear=False):
# which is always on PATH, rather than /app/bin/voculinux.
with patch.dict("os.environ", {"FLATPAK_ID": "com.voculinux.Voculinux"}, clear=False):
command = autostart_manager.get_exec_command()
self.assertEqual(command, "flatpak run com.vocalinux.Vocalinux --start-minimized")
self.assertEqual(command, "flatpak run com.voculinux.Voculinux --start-minimized")
def test_enable_autostart_flatpak_writes_flatpak_run_exec(self):
with tempfile.TemporaryDirectory() as tmp_dir:
with patch.dict(
"os.environ",
{"XDG_CONFIG_HOME": tmp_dir, "FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"XDG_CONFIG_HOME": tmp_dir, "FLATPAK_ID": "com.voculinux.Voculinux"},
clear=False,
):
self.assertTrue(autostart_manager.enable_autostart())
desktop_file = Path(tmp_dir) / "autostart" / "vocalinux.desktop"
desktop_file = Path(tmp_dir) / "autostart" / "voculinux.desktop"
content = desktop_file.read_text(encoding="utf-8")
self.assertIn("Exec=flatpak run com.vocalinux.Vocalinux --start-minimized", content)
self.assertIn("Exec=flatpak run com.voculinux.Voculinux --start-minimized", content)
def test_enable_disable_autostart_creates_and_removes_entry(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -54,16 +54,16 @@ class TestAutostartManager(unittest.TestCase):
patch.dict("os.environ", {"XDG_CONFIG_HOME": tmp_dir}, clear=False),
patch(
"vocalinux.ui.autostart_manager.shutil.which",
return_value="/usr/bin/vocalinux",
return_value="/usr/bin/voculinux",
),
):
enabled = autostart_manager.enable_autostart()
self.assertTrue(enabled)
desktop_file = Path(tmp_dir) / "autostart" / "vocalinux.desktop"
desktop_file = Path(tmp_dir) / "autostart" / "voculinux.desktop"
self.assertTrue(desktop_file.exists())
content = desktop_file.read_text(encoding="utf-8")
self.assertIn("Exec=/usr/bin/vocalinux --start-minimized", content)
self.assertIn("Exec=/usr/bin/voculinux --start-minimized", content)
disabled = autostart_manager.disable_autostart()
self.assertTrue(disabled)
+18 -18
View File
@@ -34,8 +34,8 @@ class TestAutostartManagerExtra:
from vocalinux.ui import autostart_manager
with tempfile.TemporaryDirectory() as tmp_dir:
autostart_file = Path(tmp_dir) / "vocalinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Vocalinux")
autostart_file = Path(tmp_dir) / "voculinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Voculinux")
with patch.object(autostart_manager, "get_autostart_file", return_value=autostart_file):
assert autostart_manager.is_autostart_enabled() is True
@@ -45,7 +45,7 @@ class TestAutostartManagerExtra:
from vocalinux.ui import autostart_manager
with tempfile.TemporaryDirectory() as tmp_dir:
autostart_file = Path(tmp_dir) / "vocalinux.desktop"
autostart_file = Path(tmp_dir) / "voculinux.desktop"
with patch.object(autostart_manager, "get_autostart_file", return_value=autostart_file):
assert autostart_manager.is_autostart_enabled() is False
@@ -59,7 +59,7 @@ class TestAutostartManagerExtra:
readonly_dir = Path(tmp_dir) / "readonly"
readonly_dir.mkdir()
autostart_file = readonly_dir / "vocalinux.desktop"
autostart_file = readonly_dir / "voculinux.desktop"
readonly_dir.chmod(0o444) # Remove write permission
try:
@@ -72,7 +72,7 @@ class TestAutostartManagerExtra:
with patch.object(
autostart_manager,
"get_exec_command",
return_value="vocalinux --start-minimized",
return_value="voculinux --start-minimized",
):
result = autostart_manager.enable_autostart()
assert result is False
@@ -87,23 +87,23 @@ class TestAutostartManagerExtra:
with tempfile.TemporaryDirectory() as tmp_dir:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": tmp_dir}, clear=False):
with patch(
"vocalinux.ui.autostart_manager.shutil.which", return_value="/usr/bin/vocalinux"
"vocalinux.ui.autostart_manager.shutil.which", return_value="/usr/bin/voculinux"
):
result = autostart_manager.enable_autostart()
assert result is True
autostart_file = Path(tmp_dir) / "autostart" / "vocalinux.desktop"
autostart_file = Path(tmp_dir) / "autostart" / "voculinux.desktop"
assert autostart_file.exists()
content = autostart_file.read_text(encoding="utf-8")
assert "[Desktop Entry]" in content
assert "vocalinux" in content
assert "voculinux" in content
def test_disable_autostart_file_not_found(self):
"""Test disable_autostart succeeds even if file doesn't exist."""
from vocalinux.ui import autostart_manager
with tempfile.TemporaryDirectory() as tmp_dir:
autostart_file = Path(tmp_dir) / "vocalinux.desktop"
autostart_file = Path(tmp_dir) / "voculinux.desktop"
with patch.object(autostart_manager, "get_autostart_file", return_value=autostart_file):
result = autostart_manager.disable_autostart()
@@ -114,8 +114,8 @@ class TestAutostartManagerExtra:
from vocalinux.ui import autostart_manager
with tempfile.TemporaryDirectory() as tmp_dir:
autostart_file = Path(tmp_dir) / "vocalinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Vocalinux")
autostart_file = Path(tmp_dir) / "voculinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Voculinux")
# Create a mock that will raise a permission error on unlink()
mock_file = MagicMock()
@@ -133,12 +133,12 @@ class TestAutostartManagerExtra:
with tempfile.TemporaryDirectory() as tmp_dir:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": tmp_dir}, clear=False):
with patch(
"vocalinux.ui.autostart_manager.shutil.which", return_value="/usr/bin/vocalinux"
"vocalinux.ui.autostart_manager.shutil.which", return_value="/usr/bin/voculinux"
):
result = autostart_manager.set_autostart(True)
assert result is True
autostart_file = Path(tmp_dir) / "autostart" / "vocalinux.desktop"
autostart_file = Path(tmp_dir) / "autostart" / "voculinux.desktop"
assert autostart_file.exists()
def test_set_autostart_enabled_false(self):
@@ -149,8 +149,8 @@ class TestAutostartManagerExtra:
# First create the file
autostart_dir = Path(tmp_dir) / "autostart"
autostart_dir.mkdir(parents=True)
autostart_file = autostart_dir / "vocalinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Vocalinux")
autostart_file = autostart_dir / "voculinux.desktop"
autostart_file.write_text("[Desktop Entry]\nName=Voculinux")
with patch.dict(os.environ, {"XDG_CONFIG_HOME": tmp_dir}, clear=False):
result = autostart_manager.set_autostart(False)
@@ -164,10 +164,10 @@ class TestAutostartManagerExtra:
with patch("vocalinux.ui.autostart_manager.shutil.which", return_value=None):
with patch("vocalinux.ui.autostart_manager.sys.frozen", True, create=True):
with patch(
"vocalinux.ui.autostart_manager.sys.executable", "/usr/local/bin/vocalinux-bin"
"vocalinux.ui.autostart_manager.sys.executable", "/usr/local/bin/voculinux-bin"
):
command = autostart_manager.get_exec_command()
assert "/usr/local/bin/vocalinux-bin --start-minimized" in command
assert "/usr/local/bin/voculinux-bin --start-minimized" in command
def test_get_autostart_dir_without_xdg_config_home(self):
"""Test get_autostart_dir uses default location when XDG_CONFIG_HOME is not set."""
@@ -186,5 +186,5 @@ class TestAutostartManagerExtra:
with tempfile.TemporaryDirectory() as tmp_dir:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": tmp_dir}, clear=False):
autostart_file = autostart_manager.get_autostart_file()
expected = Path(tmp_dir) / "autostart" / "vocalinux.desktop"
expected = Path(tmp_dir) / "autostart" / "voculinux.desktop"
assert autostart_file == expected
+3 -3
View File
@@ -116,7 +116,7 @@ def test_overflow_does_not_reset_deadline(stream: MagicMock, clock: SimpleNamesp
def test_portaudio_chunk_preserves_normalization(
stream: MagicMock, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
stream.read.return_value = b"\x64\x00\x64\x00" * 1024
source = capture.PortAudioCaptureSource()
source.stream = stream
@@ -133,7 +133,7 @@ def audio_test(monkeypatch: pytest.MonkeyPatch, stream: MagicMock) -> tuple[Magi
"""Supply one negotiated stream without touching PortAudio or the desktop."""
audio = MagicMock()
audio.get_default_input_device_info.return_value = {"name": "Test Mic", "index": 3}
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
monkeypatch.setitem(sys.modules, "pyaudio", MagicMock(PyAudio=lambda: audio, paInt16=8))
monkeypatch.setattr(capture, "_open_capture_stream", lambda *_args: (1, 16000, stream))
return audio, stream
@@ -246,7 +246,7 @@ def test_pipewire_select_is_sliced_and_cancellable(monkeypatch: pytest.MonkeyPat
def test_pipewire_test_partial_failure_cleans_up(
monkeypatch: pytest.MonkeyPatch, failure: str
) -> None:
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
source = MagicMock(sample_rate=16000, _CHUNK_FRAMES=1024)
source.read_chunk.side_effect = [
b"\x01\x00" * 1024,
+1 -1
View File
@@ -17,7 +17,7 @@ from vocalinux.speech_recognition import recognition_manager as rm
def manager(monkeypatch: pytest.MonkeyPatch) -> Generator[rm.SpeechRecognitionManager, None, None]:
monkeypatch.setattr(rm.SpeechRecognitionManager, "_init_vosk", lambda self: None)
monkeypatch.setattr(rm, "load_silero_vad", lambda: None)
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
for name in ("play_start_sound", "play_stop_sound", "play_error_sound"):
monkeypatch.setattr(rm, name, Mock())
# Close this patch before the autouse notification patch exits.
+1 -1
View File
@@ -26,7 +26,7 @@ def manager(monkeypatch: pytest.MonkeyPatch) -> Generator[rm.SpeechRecognitionMa
monkeypatch.setattr(rm.SpeechRecognitionManager, "_init_vosk", lambda self: None)
monkeypatch.setattr(rm, "load_silero_vad", lambda: None)
monkeypatch.setattr(rm.SpeechRecognitionManager, "_download_selected_model_if_missing", Mock())
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
for name in ("play_start_sound", "play_stop_sound", "play_error_sound"):
monkeypatch.setattr(rm, name, Mock())
monkeypatch.setattr(rm, "_CAPTURE_JOIN_SECONDS", 0.02)
+2 -2
View File
@@ -32,7 +32,7 @@ class TestConfigManager(unittest.TestCase):
"""Set up test environment."""
# Create a temporary directory for configuration
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/vocalinux")
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/voculinux")
_ensure_test_config_dir(self.temp_config_dir)
self.temp_config_file = os.path.join(self.temp_config_dir, "config.json")
@@ -891,7 +891,7 @@ class TestTypedAccessors(unittest.TestCase):
def test_whispercpp_advanced_persistence(self):
# This class skips ConfigManager.__init__, so unlike TestConfigManager
# nothing has redirected CONFIG_FILE: without the patches below this
# save_config() overwrites the developer's real ~/.config/vocalinux/
# save_config() overwrites the developer's real ~/.config/voculinux/
# config.json with the synthetic fixture from setUp.
with tempfile.TemporaryDirectory() as temp_dir:
temp_file = os.path.join(temp_dir, "config.json")
+7 -5
View File
@@ -21,9 +21,11 @@ def get_repo_root() -> Path:
# Check for common repo indicators
if (parent / ".git").exists() or (parent / "install.sh").exists():
return parent
# Also check if we're in a 'vocalinux' subdirectory (CI structure)
if parent.name == "vocalinux" and (parent.parent / "vocalinux" / "install.sh").exists():
return parent.parent / "vocalinux"
# Also check if we're in a checkout-named subdirectory (CI structure)
if parent.name in {"vocalinux", "voculinux"}:
for sibling in ("voculinux", "vocalinux"):
if (parent.parent / sibling / "install.sh").exists():
return parent.parent / sibling
# Fallback: use parent of tests directory
return current.parent.parent.parent
@@ -98,10 +100,10 @@ class TestCrossDistroCompatibility:
if desktop_start == -1:
pytest.skip("install_desktop_entry function not found")
desktop_section = install_sh_content[desktop_start : desktop_start + 1000]
if "sed -i" in desktop_section and "vocalinux.desktop" in desktop_section:
if "sed -i" in desktop_section and "voculinux.desktop" in desktop_section:
assert (
"/usr/lib/girepository-1.0"
not in desktop_section.split("sed -i")[1].split("vocalinux.desktop")[0]
not in desktop_section.split("sed -i")[1].split("voculinux.desktop")[0]
), "Desktop entry should not hardcode /usr/lib/girepository-1.0 path"
def test_detect_typelib_path_with_pkgconfig(self, install_sh_content):
+27 -27
View File
@@ -91,10 +91,10 @@ def test_terms_are_live_reloaded_and_scanner_friendly(tmp_path: Path, monkeypatc
"""The UTF-8 line file accepts comments and applies an external replacement."""
manager = manager_at(tmp_path, monkeypatch, FakeConfig({"dictionary": {"enabled": True}}))
terms_file = tmp_path / TERMS_FILENAME
terms_file.write_text("# scanner comment\nVocaLinux\nvocalinux\nSupabase\n", encoding="utf-8")
terms_file.write_text("# scanner comment\nVoculinux\nvoculinux\nSupabase\n", encoding="utf-8")
assert manager.get_terms() == ["VocaLinux", "Supabase"]
assert manager.build_initial_prompt() == "VocaLinux Supabase"
assert manager.get_terms() == ["Voculinux", "Supabase"]
assert manager.build_initial_prompt() == "Voculinux Supabase"
terms_file.write_text("PyGObject\n", encoding="utf-8")
assert manager.build_initial_prompt() == "PyGObject"
@@ -105,8 +105,8 @@ def test_terms_save_is_atomic_shape_and_transient_override_is_not_written(
) -> None:
"""UI saves standard terms safely while CLI terms remain session-only."""
manager = manager_at(tmp_path, monkeypatch)
assert manager.save_terms(["VocaLinux", "vocalinux", "# ignored", " PyGObject "])
assert (tmp_path / TERMS_FILENAME).read_text(encoding="utf-8") == "VocaLinux\nPyGObject\n"
assert manager.save_terms(["Voculinux", "voculinux", "# ignored", " PyGObject "])
assert (tmp_path / TERMS_FILENAME).read_text(encoding="utf-8") == "Voculinux\nPyGObject\n"
override = tmp_path / "override.txt"
override.write_text("Override\n", encoding="utf-8")
@@ -122,15 +122,15 @@ def test_incremental_term_edits_preserve_comments_and_blank_lines(
"""GTK add/remove operations retain unrelated scanner-managed source lines."""
manager = manager_at(tmp_path, monkeypatch)
terms_file = tmp_path / TERMS_FILENAME
terms_file.write_text("# scanner note\n\nVocaLinux\n# retain me\nPyGObject\n", encoding="utf-8")
terms_file.write_text("# scanner note\n\nVoculinux\n# retain me\nPyGObject\n", encoding="utf-8")
assert manager.add_term("Supabase")
assert terms_file.read_text(encoding="utf-8") == (
"# scanner note\n\nVocaLinux\n# retain me\nPyGObject\nSupabase\n"
"# scanner note\n\nVoculinux\n# retain me\nPyGObject\nSupabase\n"
)
assert manager.remove_term("PyGObject")
assert terms_file.read_text(encoding="utf-8") == (
"# scanner note\n\nVocaLinux\n# retain me\nSupabase\n"
"# scanner note\n\nVoculinux\n# retain me\nSupabase\n"
)
@@ -278,7 +278,7 @@ def test_pr_767_dictionary_configuration_keys_and_contract_are_preserved(
) -> None:
"""Existing enabled, file_path, and max_words settings remain effective."""
configured_path = tmp_path / TERMS_FILENAME
configured_path.write_text("VocaLinux\nPyGObject\n", encoding="utf-8")
configured_path.write_text("Voculinux\nPyGObject\n", encoding="utf-8")
config = FakeConfig(
{
"dictionary": {
@@ -294,7 +294,7 @@ def test_pr_767_dictionary_configuration_keys_and_contract_are_preserved(
assert Path(DEFAULT_TERMS_PATH).name == TERMS_FILENAME
assert manager.terms_enabled()
assert manager.terms_path() == configured_path
assert manager.build_initial_prompt() == "VocaLinux"
assert manager.build_initial_prompt() == "Voculinux"
def test_default_terms_path_follows_xdg_config_home(tmp_path: Path, monkeypatch) -> None:
@@ -306,10 +306,10 @@ def test_default_terms_path_follows_xdg_config_home(tmp_path: Path, monkeypatch)
assert manager.corrections_path() == tmp_path / CORRECTIONS_FILENAME
def test_legacy_vocalinux_terms_root_keeps_corrections_at_config_dir(
def test_legacy_voculinux_terms_root_keeps_corrections_at_config_dir(
tmp_path: Path, monkeypatch
) -> None:
"""Persisted ~/.config/vocalinux terms do not move the corrections file."""
"""Legacy ~/.config/vocalinux terms do not move the corrections file."""
monkeypatch.setattr("vocalinux.custom_dictionary.config_dir", lambda: str(tmp_path))
legacy_corrections = Path.home() / ".config" / "vocalinux" / CORRECTIONS_FILENAME
expanded_legacy = str(Path.home() / ".config" / "vocalinux" / TERMS_FILENAME)
@@ -434,7 +434,7 @@ def test_corrections_fallback_survives_copy_failure(tmp_path: Path, monkeypatch)
def test_corrections_stay_at_config_dir_with_arbitrary_terms_parent(
tmp_path: Path, monkeypatch
) -> None:
"""Corrections ignore the terms file location outside a vocalinux directory."""
"""Corrections ignore the terms file location outside a voculinux directory."""
xdg_dir = tmp_path / "xdg-config"
xdg_dir.mkdir()
monkeypatch.setattr("vocalinux.custom_dictionary.config_dir", lambda: str(xdg_dir))
@@ -568,7 +568,7 @@ def test_invalid_or_unreadable_configured_terms_path_is_not_persisted(
config = FakeConfig({"dictionary": {"file_path": previous_path}})
manager = manager_at(tmp_path, monkeypatch, config)
assert not manager.set_terms_path("~vocalinux-user-does-not-exist/dictionary.txt")
assert not manager.set_terms_path("~voculinux-user-does-not-exist/dictionary.txt")
assert config.get("dictionary", "file_path") == previous_path
assert not manager.set_terms_path(str(tmp_path))
@@ -581,7 +581,7 @@ def test_invalid_saved_terms_path_is_safe_to_read_and_report(tmp_path: Path, mon
{
"dictionary": {
"enabled": True,
"file_path": "~vocalinux-user-does-not-exist/dictionary.txt",
"file_path": "~voculinux-user-does-not-exist/dictionary.txt",
}
}
)
@@ -609,7 +609,7 @@ def test_terms_path_save_failure_preserves_the_previous_configured_path(
def test_invalid_transient_terms_path_does_not_crash(tmp_path: Path, monkeypatch) -> None:
"""An unresolved CLI tilde path disables only its session's terms safely."""
manager_at(tmp_path, monkeypatch)
manager = CustomDictionaryManager(FakeConfig(), "~vocalinux-user-does-not-exist/dictionary.txt")
manager = CustomDictionaryManager(FakeConfig(), "~voculinux-user-does-not-exist/dictionary.txt")
assert manager.terms_enabled()
assert manager.terms_path() is None
@@ -638,7 +638,7 @@ def test_file_edits_preserve_existing_permissions(tmp_path: Path, monkeypatch) -
manager = manager_at(tmp_path, monkeypatch)
terms_file = tmp_path / TERMS_FILENAME
terms_file.write_text("VocaLinux\n", encoding="utf-8")
terms_file.write_text("Voculinux\n", encoding="utf-8")
terms_file.chmod(0o600)
assert manager.save_terms(["Supabase"])
assert stat.S_IMODE(terms_file.stat().st_mode) == 0o600
@@ -652,7 +652,7 @@ def test_file_edits_preserve_existing_permissions(tmp_path: Path, monkeypatch) -
def test_dictionary_file_argument_is_available_for_a_session_override(monkeypatch) -> None:
"""The retained CLI option accepts a terms file without changing config."""
monkeypatch.setattr("sys.argv", ["vocalinux", "--dictionary-file", "/tmp/terms.txt"])
monkeypatch.setattr("sys.argv", ["voculinux", "--dictionary-file", "/tmp/terms.txt"])
assert parse_arguments().dictionary_file == "/tmp/terms.txt"
@@ -961,7 +961,7 @@ def test_invalid_terms_file_is_ignored_and_explained(tmp_path: Path, monkeypatch
def test_whisper_receives_the_live_custom_terms_prompt(tmp_path: Path, monkeypatch) -> None:
"""Whisper gets the current file contents as initial_prompt at transcription time."""
dictionary = manager_at(tmp_path, monkeypatch, FakeConfig({"dictionary": {"enabled": True}}))
(tmp_path / TERMS_FILENAME).write_text("VocaLinux\n", encoding="utf-8")
(tmp_path / TERMS_FILENAME).write_text("Voculinux\n", encoding="utf-8")
manager = recognition_manager("whisper", dictionary)
manager.model = MagicMock()
manager.model.transcribe.return_value = {"text": "ok"}
@@ -970,13 +970,13 @@ def test_whisper_receives_the_live_custom_terms_prompt(tmp_path: Path, monkeypat
with patch.dict("sys.modules", {"numpy": mock_numpy(), "torch": MagicMock()}):
assert manager._transcribe_with_whisper([b"\x00\x00"]) == "ok"
assert manager.model.transcribe.call_args.kwargs["initial_prompt"] == "VocaLinux"
assert manager.model.transcribe.call_args.kwargs["initial_prompt"] == "Voculinux"
def test_faster_whisper_receives_the_live_custom_terms_prompt(tmp_path: Path, monkeypatch) -> None:
"""Faster Whisper receives the same live vocabulary bias as OpenAI Whisper."""
dictionary = manager_at(tmp_path, monkeypatch, FakeConfig({"dictionary": {"enabled": True}}))
(tmp_path / TERMS_FILENAME).write_text("VocaLinux\n", encoding="utf-8")
(tmp_path / TERMS_FILENAME).write_text("Voculinux\n", encoding="utf-8")
manager = object.__new__(SpeechRecognitionManager)
manager.dictionary_manager = dictionary
manager._faster_whisper_engine = MagicMock()
@@ -985,7 +985,7 @@ def test_faster_whisper_receives_the_live_custom_terms_prompt(tmp_path: Path, mo
assert manager._transcribe_with_faster_whisper([b"\x00\x00"]) == "ok"
manager._faster_whisper_engine.transcribe.assert_called_once_with(
[b"\x00\x00"], language="auto", initial_prompt="VocaLinux"
[b"\x00\x00"], language="auto", initial_prompt="Voculinux"
)
@@ -994,7 +994,7 @@ def test_whispercpp_composes_and_clears_live_terms_prompt(tmp_path: Path, monkey
config = FakeConfig({"dictionary": {"enabled": True}})
dictionary = manager_at(tmp_path, monkeypatch, config)
terms_file = tmp_path / TERMS_FILENAME
terms_file.write_text("VocaLinux\n", encoding="utf-8")
terms_file.write_text("Voculinux\n", encoding="utf-8")
manager = recognition_manager("whisper_cpp", dictionary, advanced_prompt="Explicit context")
manager._model_lock = threading.Lock()
manager.model = MagicMock()
@@ -1008,7 +1008,7 @@ def test_whispercpp_composes_and_clears_live_terms_prompt(tmp_path: Path, monkey
assert manager._transcribe_with_whispercpp([b"\x00\x00"]) == "ok"
prompts = [call.kwargs["initial_prompt"] for call in manager.model.transcribe.call_args_list]
assert prompts == ["Explicit context VocaLinux", "Explicit context", ""]
assert prompts == ["Explicit context Voculinux", "Explicit context", ""]
def test_vosk_warns_once_for_bias_while_corrections_remain_available(
@@ -1016,7 +1016,7 @@ def test_vosk_warns_once_for_bias_while_corrections_remain_available(
) -> None:
"""VOSK rejects vocabulary bias only; its transcript can still be corrected."""
dictionary = manager_at(tmp_path, monkeypatch, FakeConfig({"dictionary": {"enabled": True}}))
(tmp_path / TERMS_FILENAME).write_text("VocaLinux\n", encoding="utf-8")
(tmp_path / TERMS_FILENAME).write_text("Voculinux\n", encoding="utf-8")
manager = recognition_manager("vosk", dictionary)
manager._get_vosk_model_path = MagicMock(return_value="/models/vosk")
vosk = MagicMock()
@@ -1041,11 +1041,11 @@ def test_whispercpp_prompt_composes_advanced_and_terms() -> None:
recognition_manager = object.__new__(SpeechRecognitionManager)
recognition_manager.whispercpp_initial_prompt = "Write product names accurately."
recognition_manager.dictionary_manager = type(
"Dictionary", (), {"build_initial_prompt": lambda self: "VocaLinux"}
"Dictionary", (), {"build_initial_prompt": lambda self: "Voculinux"}
)()
assert (
recognition_manager._get_whispercpp_prompt() == "Write product names accurately. VocaLinux"
recognition_manager._get_whispercpp_prompt() == "Write product names accurately. Voculinux"
)
recognition_manager.dictionary_manager = type(
"Dictionary", (), {"build_initial_prompt": lambda self: None}
+8 -8
View File
@@ -16,7 +16,7 @@ import pytest
if TYPE_CHECKING:
from vocalinux.dbus_service import VocalinuxDBusService
# gi is mocked globally by conftest. All vocalinux imports are done lazily
# gi is mocked globally by conftest. All voculinux imports are done lazily
# inside the individual tests. Importing them at module level would bind their
# `gi.repository` references at collection time and clash with test_main.py,
# which replaces sys.modules["gi"] / sys.modules["gi.repository"] wholesale --
@@ -35,7 +35,7 @@ def test_cli_flag_sends_matching_command(flag, command):
from vocalinux.main import main
with patch("vocalinux.dbus_service.send_command", return_value=True) as mock_send:
with patch.object(sys, "argv", ["vocalinux", flag]):
with patch.object(sys, "argv", ["voculinux", flag]):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
@@ -47,7 +47,7 @@ def test_cli_flag_exits_nonzero_when_no_instance():
from vocalinux.main import main
with patch("vocalinux.dbus_service.send_command", return_value=False):
with patch.object(sys, "argv", ["vocalinux", "--toggle"]):
with patch.object(sys, "argv", ["voculinux", "--toggle"]):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
@@ -58,7 +58,7 @@ def test_no_trigger_flag_does_not_dispatch():
from vocalinux.main import main
# Mock single_instance via sys.modules (not patch("...acquire_lock")) so we
# don't import the real module and set it as a vocalinux package attribute,
# don't import the real module and set it as a voculinux package attribute,
# which would defeat patch.dict-based single-instance tests elsewhere.
mock_single_instance = MagicMock()
mock_single_instance.acquire_lock.return_value = True
@@ -66,7 +66,7 @@ def test_no_trigger_flag_does_not_dispatch():
# check_dependencies returns False so main() exits early after dispatch.
with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}):
with patch("vocalinux.main.check_dependencies", return_value=False):
with patch.object(sys, "argv", ["vocalinux"]):
with patch.object(sys, "argv", ["voculinux"]):
with pytest.raises(SystemExit):
main()
mock_send.assert_not_called()
@@ -269,7 +269,7 @@ def test_on_bus_acquired_handles_register_failure():
# Patch GLib.Error to a real class so both the raise and the `except` clause
# reference the same type and the GLib.Error branch is exercised.
with patch.object(dbus_service.GLib, "Error", RuntimeError):
service._on_bus_acquired(connection, "com.vocalinux.Vocalinux")
service._on_bus_acquired(connection, "com.voculinux.Voculinux")
assert service._registration_id == 0
@@ -278,8 +278,8 @@ def test_name_acquired_and_lost_are_safe_to_call():
"""The name-acquired/lost callbacks only log and never raise."""
service = _make_service()
connection = MagicMock()
service._on_name_acquired(connection, "com.vocalinux.Vocalinux")
service._on_name_lost(connection, "com.vocalinux.Vocalinux")
service._on_name_acquired(connection, "com.voculinux.Voculinux")
service._on_name_lost(connection, "com.voculinux.Voculinux")
def test_invoke_swallows_callback_errors():
+2 -2
View File
@@ -136,7 +136,7 @@ class TestOverlayConfig(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/vocalinux")
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/voculinux")
_ensure_test_config_dir(self.temp_config_dir)
self.temp_config_file = os.path.join(self.temp_config_dir, "config.json")
@@ -1199,7 +1199,7 @@ class TestOverlayConfigMissingUiSection(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/vocalinux")
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/voculinux")
_ensure_test_config_dir(self.temp_config_dir)
self.temp_config_file = os.path.join(self.temp_config_dir, "config.json")
self.config_dir_patcher = patch(
+1 -1
View File
@@ -190,7 +190,7 @@ class TestDictationPadConfig(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/vocalinux")
self.temp_config_dir = os.path.join(self.temp_dir.name, ".config/voculinux")
_ensure_test_config_dir(self.temp_config_dir)
self.temp_config_file = os.path.join(self.temp_config_dir, "config.json")
+7 -7
View File
@@ -790,7 +790,7 @@ class TestEvdevKeyboardBackendPermissionHint:
result = backend.get_permission_hint()
assert result is None
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
@patch.dict("os.environ", {"SNAP": "/snap/voculinux/x1"}, clear=False)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
def test_permission_hint_snap_no_devices(self, mock_find_devices):
@@ -802,7 +802,7 @@ class TestEvdevKeyboardBackendPermissionHint:
assert "raw-input" in result
assert "hardware-observe" in result
@patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False)
@patch.dict("os.environ", {"SNAP": "/snap/voculinux/x1"}, clear=False)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.EVDEV_AVAILABLE", True)
@patch("vocalinux.ui.keyboard_backends.evdev_backend.find_keyboard_devices")
@patch("vocalinux.ui.keyboard_backends.evdev_backend.InputDevice")
@@ -1804,7 +1804,7 @@ class TestEvdevGrabAndForwarding:
device = MagicMock()
device.fileno.return_value = 10
device.info.bustype = 0x06 # BUS_VIRTUAL
device.name = "AT Translated Set 2 keyboard (vocalinux)"
device.name = "AT Translated Set 2 keyboard (voculinux)"
mock_input_device.return_value = device
backend = EvdevKeyboardBackend()
@@ -3192,7 +3192,7 @@ class TestWriteOnlyUInput:
patch.object(eb._WriteOnlyUInput, "_find_device", return_value=device),
patch.object(eb._uinput, "write") as write,
):
uinput = eb._WriteOnlyUInput(events={1: [30]}, name="kbd (vocalinux)", bustype=0x06)
uinput = eb._WriteOnlyUInput(events={1: [30]}, name="kbd (voculinux)", bustype=0x06)
yield uinput, mock_open, set_phys, enable, setup, create, write, device
def test_opens_wronly_and_runs_setup(self) -> None:
@@ -3243,7 +3243,7 @@ class TestWriteOnlyUInput:
),
):
with pytest.raises(TypeError):
eb._WriteOnlyUInput(events=None, name="kbd (vocalinux)", bustype=0x06)
eb._WriteOnlyUInput(events=None, name="kbd (voculinux)", bustype=0x06)
mock_close.assert_called_once_with(42)
@@ -3254,7 +3254,7 @@ class TestWriteOnlyUInput:
name = _clone_device_name(device)
assert name.endswith(" (vocalinux)")
assert name.endswith(" (voculinux)")
assert len(name.encode("utf-8")) <= 79
def test_clone_device_name_keeps_short_names(self) -> None:
@@ -3262,7 +3262,7 @@ class TestWriteOnlyUInput:
device = MagicMock()
device.name = "AT Translated Set 2 keyboard"
assert _clone_device_name(device) == "AT Translated Set 2 keyboard (vocalinux)"
assert _clone_device_name(device) == "AT Translated Set 2 keyboard (voculinux)"
class TestSharedEvdevDeviceLayer:
+4 -4
View File
@@ -326,17 +326,17 @@ class TestFasterWhisperEngine:
def test_transcribe_passes_initial_prompt(self) -> None:
"""Test that vocabulary bias is forwarded to faster-whisper."""
whisper_mock = self._mock_whisper_model([MagicMock(text="VocaLinux")])
whisper_mock = self._mock_whisper_model([MagicMock(text="Voculinux")])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
audio = np.array([0, 1000, -1000, 0], dtype=np.int16)
engine.transcribe([audio.tobytes()], initial_prompt="VocaLinux")
engine.transcribe([audio.tobytes()], initial_prompt="Voculinux")
assert (
whisper_mock.WhisperModel.return_value.transcribe.call_args.kwargs["initial_prompt"]
== "VocaLinux"
== "Voculinux"
)
def test_transcribe_empty_audio(self):
@@ -429,7 +429,7 @@ class TestFasterWhisperEngine:
assert text == ""
def test_language_mapping(self):
"""Test that Vocalinux language codes are mapped correctly."""
"""Test that Voculinux language codes are mapped correctly."""
engine = FasterWhisperEngine(language="en-us")
assert engine._normalize_language() == "en"
+1 -1
View File
@@ -47,7 +47,7 @@ class _FakeGtkDialog:
pass
_ISOLATED_MODULE_NAME = "vocalinux_test_first_run_dialog_isolated"
_ISOLATED_MODULE_NAME = "voculinux_test_first_run_dialog_isolated"
def _load_first_run_dialog_with_fake_gtk():
+1 -1
View File
@@ -47,7 +47,7 @@ PYPROJECT = REPO_ROOT / "pyproject.toml"
EXPORT = REPO_ROOT / "requirements" / "vad.txt"
FLATPAK_DIR = REPO_ROOT / "packaging" / "flatpak"
DEPS_YAML = FLATPAK_DIR / "python3-dependencies.yaml"
MANIFEST = FLATPAK_DIR / "com.vocalinux.Vocalinux.yml"
MANIFEST = FLATPAK_DIR / "com.voculinux.Voculinux.yml"
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "flatpak.yml"
#: Kept in step with scripts/sync_flatpak_deps.py, which resolves the same
+2 -2
View File
@@ -266,7 +266,7 @@ class TestPairingDecode(unittest.TestCase):
class TestSandbox(unittest.TestCase):
def test_flatpak_fails_closed(self):
state = detect_sandbox({"FLATPAK_ID": "com.vocahq.Vocalinux"})
state = detect_sandbox({"FLATPAK_ID": "com.vocahq.Voculinux"})
self.assertTrue(state.blocked)
self.assertEqual(state.kind, "flatpak")
self.assertIn("Flatpak", state.hint)
@@ -704,7 +704,7 @@ class TestOrphanComposeDetect(unittest.TestCase):
manager.stop_async()
fake_thread.assert_called_once()
kwargs = fake_thread.call_args.kwargs
self.assertEqual(kwargs.get("name"), "vocalinux-gateway-stop")
self.assertEqual(kwargs.get("name"), "voculinux-gateway-stop")
self.assertEqual(kwargs.get("target").__func__, manager._stop_worker.__func__)
started.start.assert_called_once()
+1 -1
View File
@@ -1,4 +1,4 @@
"""Guards for the environment Vocalinux hands to host binaries.
"""Guards for the environment Voculinux hands to host binaries.
An AppImage exports its own `LD_LIBRARY_PATH`, `GI_TYPELIB_PATH` and
`PYTHONHOME`, and every child process inherits them. A host `ibus` linked
+1 -1
View File
@@ -29,7 +29,7 @@ sys.modules["gi"] = _mock_gi
sys.modules["gi.repository"] = _mock_gi_repo
for _key in list(sys.modules.keys()):
if "vocalinux" in _key and "ibus_engine" in _key:
if "voculinux" in _key and "ibus_engine" in _key:
del sys.modules[_key]
import vocalinux.text_injection.ibus_engine as ibus_engine # noqa: E402
+20 -20
View File
@@ -94,7 +94,7 @@ class TestIBusTextInjectorSetupFailures(unittest.TestCase):
with self.assertRaises(IBusSetupError) as context:
IBusTextInjector(auto_activate=True)
self.assertIn("Failed to activate Vocalinux IBus engine", str(context.exception))
self.assertIn("Failed to activate Voculinux IBus engine", str(context.exception))
@patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True)
@patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir")
@@ -137,7 +137,7 @@ class TestIBusEngineHelpers(unittest.TestCase):
def test_ensure_ibus_dir_creates_directory(self):
"""Test that ensure_ibus_dir creates the directory."""
with patch("vocalinux.text_injection.ibus_engine.VOCALINUX_IBUS_DIR") as mock_dir:
with patch("vocalinux.text_injection.ibus_engine.VOCULINUX_IBUS_DIR") as mock_dir:
mock_dir.mkdir = MagicMock()
from vocalinux.text_injection.ibus_engine import ensure_ibus_dir
@@ -157,7 +157,7 @@ class TestIsEngineActive(unittest.TestCase):
@patch("subprocess.run")
def test_engine_active(self, mock_run):
"""Test detection when engine is active."""
mock_run.return_value = MagicMock(stdout="vocalinux", returncode=0)
mock_run.return_value = MagicMock(stdout="voculinux", returncode=0)
from vocalinux.text_injection.ibus_engine import is_engine_active
@@ -589,12 +589,12 @@ class TestSwitchEngine(unittest.TestCase):
# First call: ibus engine <name> (switch)
# Second call: ibus engine (get current) - for verification
switch_result = MagicMock(returncode=0)
verify_result = MagicMock(returncode=0, stdout="vocalinux\n")
verify_result = MagicMock(returncode=0, stdout="voculinux\n")
mock_run.side_effect = [switch_result, verify_result]
from vocalinux.text_injection.ibus_engine import switch_engine
result = switch_engine("vocalinux")
result = switch_engine("voculinux")
self.assertTrue(result)
self.assertEqual(mock_run.call_count, 2)
@@ -621,7 +621,7 @@ class TestSwitchEngine(unittest.TestCase):
from vocalinux.text_injection.ibus_engine import switch_engine
result = switch_engine("vocalinux")
result = switch_engine("voculinux")
self.assertFalse(result)
@@ -674,8 +674,8 @@ class TestIBusTextInjector(unittest.TestCase):
self.assertGreaterEqual(mock_ensure_dir.call_count, 1)
# Engine should be started
mock_start_engine.assert_called_once()
# Should switch to vocalinux engine
mock_switch.assert_called_once_with("vocalinux")
# Should switch to voculinux engine
mock_switch.assert_called_once_with("voculinux")
mock_wait_ready.assert_called_once_with()
self.assertEqual(injector._previous_engine, "xkb:us::eng")
@@ -1003,20 +1003,20 @@ class TestIBusTextInjector(unittest.TestCase):
@patch(
"vocalinux.text_injection.ibus_engine._query_active_engine",
return_value="vocalinux",
return_value="voculinux",
)
@patch("vocalinux.text_injection.ibus_engine.stop_engine_process")
@patch(
"vocalinux.text_injection.ibus_engine.get_current_engine",
return_value="vocalinux",
return_value="voculinux",
)
@patch("vocalinux.text_injection.ibus_engine.switch_engine", return_value=True)
@patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True)
@patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir")
def test_stop_uses_cached_engine_when_stuck_on_vocalinux(
def test_stop_uses_cached_engine_when_stuck_on_voculinux(
self, mock_ensure_dir, mock_switch, mock_get_current, mock_stop_proc, mock_live
):
"""If quit finds vocalinux selected, fall back to the warmup-captured engine (#558)."""
"""If quit finds voculinux selected, fall back to the warmup-captured engine (#558)."""
from vocalinux.text_injection.ibus_engine import IBusTextInjector
injector = IBusTextInjector(auto_activate=False)
@@ -1031,15 +1031,15 @@ class TestIBusTextInjector(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.stop_engine_process")
@patch(
"vocalinux.text_injection.ibus_engine.get_current_engine",
return_value="vocalinux",
return_value="voculinux",
)
@patch("vocalinux.text_injection.ibus_engine.switch_engine")
@patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True)
@patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir")
def test_stop_skips_restore_when_only_vocalinux_active(
def test_stop_skips_restore_when_only_voculinux_active(
self, mock_ensure_dir, mock_switch, mock_get_current, mock_stop_proc
):
"""Do not try to reselect vocalinux after its process has been torn down."""
"""Do not try to reselect voculinux after its process has been torn down."""
from vocalinux.text_injection.ibus_engine import IBusTextInjector
injector = IBusTextInjector(auto_activate=False)
@@ -1091,7 +1091,7 @@ class TestIBusTextInjector(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.start_engine_process", return_value=True)
@patch("vocalinux.text_injection.ibus_engine.IBusTextInjector._wait_for_engine_ready")
@patch("vocalinux.text_injection.ibus_engine.switch_engine")
def test_prepare_engine_does_not_activate_vocalinux_engine(
def test_prepare_engine_does_not_activate_voculinux_engine(
self,
mock_switch,
mock_wait_ready,
@@ -1124,7 +1124,7 @@ class TestIBusTextInjector(unittest.TestCase):
mock_is_active,
mock_get_current,
):
"""Warmup caches the live engine so quit can restore after a stuck vocalinux (#558)."""
"""Warmup caches the live engine so quit can restore after a stuck voculinux (#558)."""
from vocalinux.text_injection.ibus_engine import IBusTextInjector
injector = IBusTextInjector(auto_activate=False)
@@ -1194,7 +1194,7 @@ class TestIBusTextInjector(unittest.TestCase):
mock_get_xkb,
mock_restore_xkb,
):
"""Scoped injection must restore XKB after the Vocalinux engine switch (#664)."""
"""Scoped injection must restore XKB after the Voculinux engine switch (#664)."""
from vocalinux.text_injection.ibus_engine import ENGINE_NAME, IBusTextInjector
mock_socket_path.exists.return_value = True
@@ -1615,14 +1615,14 @@ class TestIBusTextInjector(unittest.TestCase):
mock_is_active,
mock_get_current,
):
"""Test inject_text returns False when switch to Vocalinux engine fails."""
"""Test inject_text returns False when switch to Voculinux engine fails."""
from vocalinux.text_injection.ibus_engine import IBusTextInjector
injector = IBusTextInjector(auto_activate=False)
result = injector.inject_text("Hello")
self.assertFalse(result)
mock_switch.assert_called_once_with("vocalinux")
mock_switch.assert_called_once_with("voculinux")
@patch("vocalinux.text_injection.ibus_engine.get_current_engine", return_value=None)
@patch("vocalinux.text_injection.ibus_engine.is_engine_active", return_value=False)
+32 -32
View File
@@ -194,7 +194,7 @@ class TestIBusEngineModuleFunctions(unittest.TestCase):
@patch("subprocess.run")
def test_is_engine_active_success(self, mock_run):
"""Test when engine is active."""
mock_run.return_value = MagicMock(returncode=0, stdout="vocalinux")
mock_run.return_value = MagicMock(returncode=0, stdout="voculinux")
from vocalinux.text_injection.ibus_engine import is_engine_active
result = is_engine_active()
@@ -231,10 +231,10 @@ class TestIBusEngineModuleFunctions(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.get_current_engine")
def test_switch_engine_success(self, mock_get_current, mock_run):
"""Test switching engine successfully."""
mock_get_current.return_value = "vocalinux"
mock_get_current.return_value = "voculinux"
from vocalinux.text_injection.ibus_engine import switch_engine
result = switch_engine("vocalinux")
result = switch_engine("voculinux")
self.assertTrue(result)
@patch("subprocess.run")
@@ -244,7 +244,7 @@ class TestIBusEngineModuleFunctions(unittest.TestCase):
mock_get_current.return_value = "other-engine"
from vocalinux.text_injection.ibus_engine import switch_engine
result = switch_engine("vocalinux")
result = switch_engine("voculinux")
self.assertFalse(result)
@patch("vocalinux.text_injection.ibus_engine.PID_FILE")
@@ -380,7 +380,7 @@ class TestVocalinuxEngine(unittest.TestCase):
VocalinuxEngine._server_socket = None
VocalinuxEngine._server_running = False
def test_vocalinux_engine_init(self):
def test_voculinux_engine_init(self):
"""Test VocalinuxEngine initialization."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -394,7 +394,7 @@ class TestVocalinuxEngine(unittest.TestCase):
self.assertIsNone(VocalinuxEngine._server_socket)
self.assertFalse(VocalinuxEngine._server_running)
def test_vocalinux_engine_do_enable(self):
def test_voculinux_engine_do_enable(self):
"""Test engine enable signal handler."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -404,7 +404,7 @@ class TestVocalinuxEngine(unittest.TestCase):
# Verify active instance is set
self.assertEqual(VocalinuxEngine._active_instance, engine)
def test_vocalinux_engine_do_disable(self):
def test_voculinux_engine_do_disable(self):
"""Test engine disable signal handler."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -414,7 +414,7 @@ class TestVocalinuxEngine(unittest.TestCase):
# Instance should still be active (not cleared on disable)
self.assertEqual(VocalinuxEngine._active_instance, engine)
def test_vocalinux_engine_do_focus_in(self):
def test_voculinux_engine_do_focus_in(self):
"""Test engine focus in signal handler."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -423,7 +423,7 @@ class TestVocalinuxEngine(unittest.TestCase):
self.assertEqual(VocalinuxEngine._active_instance, engine)
self.assertTrue(VocalinuxEngine._focus_event.is_set())
def test_vocalinux_engine_do_focus_out(self):
def test_voculinux_engine_do_focus_out(self):
"""Test engine focus out signal handler."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -508,7 +508,7 @@ class TestVocalinuxEngine(unittest.TestCase):
self.assertIsNone(VocalinuxEngine._active_instance)
self.assertFalse(VocalinuxEngine._focus_event.is_set())
def test_vocalinux_engine_do_process_key_event(self):
def test_voculinux_engine_do_process_key_event(self):
"""Test engine key event processing."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -517,7 +517,7 @@ class TestVocalinuxEngine(unittest.TestCase):
# Should return False to pass through all keys
self.assertFalse(result)
def test_vocalinux_engine_inject_text_empty(self):
def test_voculinux_engine_inject_text_empty(self):
"""Test text injection with empty text."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -526,7 +526,7 @@ class TestVocalinuxEngine(unittest.TestCase):
# Empty text should return True (no-op)
self.assertTrue(result)
def test_vocalinux_engine_inject_text_success(self):
def test_voculinux_engine_inject_text_success(self):
"""Test successful text injection."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -540,7 +540,7 @@ class TestVocalinuxEngine(unittest.TestCase):
self.assertTrue(result)
engine.commit_text.assert_called_once()
def test_vocalinux_engine_inject_text_failure(self):
def test_voculinux_engine_inject_text_failure(self):
"""Test text injection error handling."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngine
@@ -733,7 +733,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
pass
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
def test_vocalinux_engine_application_init(self, mock_start_server):
def test_voculinux_engine_application_init(self, mock_start_server):
"""Test VocalinuxEngineApplication initialization."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngineApplication
@@ -748,7 +748,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
self.assertEqual(type(app.mainloop).__name__, "MagicMock")
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
def test_vocalinux_engine_application_on_disconnected(self, mock_start_server):
def test_voculinux_engine_application_on_disconnected(self, mock_start_server):
"""Test IBus disconnection handler."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngineApplication
@@ -760,7 +760,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
app.mainloop.quit.assert_called_once()
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
def test_vocalinux_engine_application_run(self, mock_start_server):
def test_voculinux_engine_application_run(self, mock_start_server):
"""Test engine application main loop."""
from vocalinux.text_injection.ibus_engine import VocalinuxEngineApplication
@@ -774,7 +774,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.IBus.Component")
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_standalone_registers_component(
def test_voculinux_engine_application_standalone_registers_component(
self,
mock_bus_cls,
mock_factory_new,
@@ -810,7 +810,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_ibus_exec_requests_name(
def test_voculinux_engine_application_ibus_exec_requests_name(
self,
mock_bus_cls,
mock_factory_new,
@@ -835,7 +835,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_request_name_failure_raises(
def test_voculinux_engine_application_request_name_failure_raises(
self,
mock_bus_cls,
mock_factory_new,
@@ -858,7 +858,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_register_component_failure_raises(
def test_voculinux_engine_application_register_component_failure_raises(
self,
mock_bus_cls,
mock_factory_new,
@@ -880,7 +880,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_disconnected_bus_raises(
def test_voculinux_engine_application_disconnected_bus_raises(
self,
mock_bus_cls,
mock_start_server,
@@ -898,7 +898,7 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
@patch("vocalinux.text_injection.ibus_engine.IBus.Bus")
def test_vocalinux_engine_application_none_connection_raises(
def test_voculinux_engine_application_none_connection_raises(
self,
mock_bus_cls,
mock_factory_new,
@@ -962,7 +962,7 @@ class TestIBusEngineMainEntrypoint(unittest.TestCase):
msg=f"stdout={result.stdout!r} stderr={result.stderr!r}",
)
self.assertIn("<engines>", result.stdout)
self.assertIn("vocalinux", result.stdout)
self.assertIn("voculinux", result.stdout)
self.assertNotIn("attempted relative import", result.stderr)
def test_main_passes_ibus_flag_to_application(self):
@@ -1008,7 +1008,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", side_effect=running_states),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc) as mock_popen,
patch.object(ibus_engine.time, "sleep"),
patch.object(ibus_engine.sys, "prefix", "/venv"),
@@ -1040,7 +1040,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", side_effect=iter([False, True])),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE"),
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc) as mock_popen,
patch.object(ibus_engine.time, "sleep"),
):
@@ -1077,7 +1077,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", side_effect=popen_side_effect),
patch.object(ibus_engine.time, "sleep"),
patch.object(ibus_engine.logger, "error") as mock_error,
@@ -1110,7 +1110,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc),
patch.object(ibus_engine.time, "sleep"),
patch.object(ibus_engine.logger, "error") as mock_error,
@@ -1137,7 +1137,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc),
patch.object(ibus_engine.time, "sleep"),
patch.object(ibus_engine.logger, "error") as mock_error,
@@ -1173,7 +1173,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc),
patch.object(ibus_engine.time, "sleep"),
):
@@ -1198,7 +1198,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc),
patch.object(ibus_engine.time, "sleep"),
):
@@ -1230,7 +1230,7 @@ class TestStartEngineProcess(unittest.TestCase):
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "PID_FILE") as mock_pid,
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", tmp),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", tmp),
patch.object(ibus_engine.subprocess, "Popen", return_value=mock_proc),
patch.object(ibus_engine.time, "sleep"),
patch.object(ibus_engine.logger, "error") as mock_error,
@@ -1249,7 +1249,7 @@ class TestStartEngineProcess(unittest.TestCase):
with (
patch.object(ibus_engine, "is_engine_process_running", return_value=False),
patch.object(ibus_engine, "ensure_ibus_dir"),
patch.object(ibus_engine, "VOCALINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine, "VOCULINUX_IBUS_DIR", Path(tempfile.mkdtemp())),
patch.object(ibus_engine.subprocess, "Popen", side_effect=OSError("cannot exec")),
patch.object(ibus_engine.logger, "error") as mock_error,
):
+7 -7
View File
@@ -10,7 +10,7 @@ import sys
import unittest
from unittest.mock import MagicMock, patch
# Mock GI imports before importing any vocalinux modules that use gi.
# Mock GI imports before importing any voculinux modules that use gi.
# On CI, real gi/IBus may be installed; importing ibus_engine without mocks
# would set IBUS_AVAILABLE=True and VocalinuxEngine would inherit from real
# IBus.Engine, causing hangs when super().__init__() tries to connect to IBus.
@@ -31,7 +31,7 @@ sys.modules["gi.repository"] = _mock_gi_repo
# Force reload ibus_engine so it picks up mocks (in case it was already imported)
for _key in list(sys.modules.keys()):
if "vocalinux" in _key and "ibus_engine" in _key:
if "voculinux" in _key and "ibus_engine" in _key:
del sys.modules[_key]
@@ -323,8 +323,8 @@ class TestSwitchEngine(unittest.TestCase):
"""Test successfully switching engines."""
from vocalinux.text_injection.ibus_engine import switch_engine
mock_get_engine.return_value = "vocalinux"
result = switch_engine("vocalinux")
mock_get_engine.return_value = "voculinux"
result = switch_engine("voculinux")
self.assertTrue(result)
@patch("vocalinux.text_injection.ibus_engine.get_current_engine")
@@ -335,7 +335,7 @@ class TestSwitchEngine(unittest.TestCase):
from vocalinux.text_injection.ibus_engine import switch_engine
mock_get_engine.return_value = "other"
result = switch_engine("vocalinux")
result = switch_engine("voculinux")
self.assertFalse(result)
@@ -381,7 +381,7 @@ class TestStopEngineProcess(unittest.TestCase):
cmdline = MagicMock()
cmdline.exists.return_value = True
cmdline.read_text.return_value = "python /x/vocalinux/text_injection/ibus_engine.py --ibus"
cmdline.read_text.return_value = "python /x/voculinux/text_injection/ibus_engine.py --ibus"
mock_path_cls.return_value = cmdline
# SIGTERM succeeds; first liveness check still alive; second gone.
@@ -409,7 +409,7 @@ class TestStopEngineProcess(unittest.TestCase):
cmdline = MagicMock()
cmdline.exists.return_value = True
cmdline.read_text.return_value = "python /x/vocalinux/text_injection/ibus_engine.py --ibus"
cmdline.read_text.return_value = "python /x/voculinux/text_injection/ibus_engine.py --ibus"
mock_path_cls.return_value = cmdline
# SIGTERM, then keep reporting alive until wait timeout, then SIGKILL succeeds.
+1 -1
View File
@@ -30,7 +30,7 @@ sys.modules["gi"] = _mock_gi
sys.modules["gi.repository"] = _mock_gi_repo
for _key in list(sys.modules.keys()):
if "vocalinux" in _key and "ibus_engine" in _key:
if "voculinux" in _key and "ibus_engine" in _key:
del sys.modules[_key]
from vocalinux.text_injection.ibus_engine import restore_xkb_layout # noqa: E402
+7 -7
View File
@@ -84,14 +84,14 @@ class TestWaitForModifiersReleased:
def test_disabled_via_env(self, monkeypatch):
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", "0")
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", "0")
with patch.object(inj, "_held_modifier_keycodes") as held:
inj._wait_for_modifiers_released()
held.assert_not_called()
def test_gives_up_after_timeout(self, monkeypatch):
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", "0.05")
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", "0.05")
# Always held -> must still return (best-effort) rather than hang.
with patch.object(inj, "_held_modifier_keycodes", return_value={56}), patch("time.sleep"):
inj._wait_for_modifiers_released() # should return within the timeout
@@ -227,30 +227,30 @@ class TestInjectionModifierWaitSeconds:
@pytest.mark.parametrize("raw,expected", [("2.5", 2.5), ("0", 0.0), ("0.5", 0.5)])
def test_valid_values(self, raw, expected, monkeypatch):
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", raw)
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", raw)
assert inj._injection_modifier_wait_seconds() == expected
@pytest.mark.parametrize("raw", ["inf", "-inf", "nan", "Infinity", "NaN"])
def test_non_finite_falls_back_to_default(self, raw, monkeypatch):
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", raw)
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", raw)
assert inj._injection_modifier_wait_seconds() == 1.0
def test_non_numeric_falls_back_to_default(self, monkeypatch):
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", "abc")
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", "abc")
assert inj._injection_modifier_wait_seconds() == 1.0
def test_unset_uses_default(self, monkeypatch):
inj = _bare_injector()
monkeypatch.delenv("VOCALINUX_INJECT_MODIFIER_WAIT", raising=False)
monkeypatch.delenv("VOCULINUX_INJECT_MODIFIER_WAIT", raising=False)
assert inj._injection_modifier_wait_seconds() == 1.0
def test_inf_does_not_block_forever(self, monkeypatch):
# Regression: with 'inf', the wait must still terminate (bounded by the
# finite default) instead of looping forever while a modifier is held.
inj = _bare_injector()
monkeypatch.setenv("VOCALINUX_INJECT_MODIFIER_WAIT", "inf")
monkeypatch.setenv("VOCULINUX_INJECT_MODIFIER_WAIT", "inf")
# Simulate the clock crossing the finite default deadline in a few steps.
clock = iter([0.0, 0.5, 1.5])
monkeypatch.setattr("time.monotonic", lambda: next(clock, 2.0))
+1 -1
View File
@@ -1,7 +1,7 @@
"""Behavioral tests for the installer's speech-engine sanity check.
config.json survives reinstalls, so it can name an engine the current venv
cannot import — vosk is an optional extra since #705. Vocalinux then dies at
cannot import — vosk is an optional extra since #705. Voculinux then dies at
startup with "No module named 'vosk'" instead of the installer saying so.
"""
+10 -10
View File
@@ -174,7 +174,7 @@ def test_the_gate_takes_this_commit_from_git_not_the_working_directory():
), "the gate no longer extracts the tree from git"
installer = INSTALLER.read_text(encoding="utf-8").splitlines()
assert any(
"grep" in line and "pyproject.toml" in line and "vocalinux" in line for line in installer
"grep" in line and "pyproject.toml" in line and "voculinux" in line for line in installer
), (
"local mode is no longer detected from pyproject.toml in the working"
" directory; the gate may now be testing a downloaded tag instead of"
@@ -206,9 +206,9 @@ def test_the_smoke_checks_what_the_installer_claims_to_have_created():
gate = _gate_text()
for construct in (
r'\[ -x "\$VENV/bin/python" \]',
r'\[ -x "\$INSTALL_HOME/\.local/bin/vocalinux" \]',
r'\[ -f "\$INSTALL_HOME/\.local/share/applications/vocalinux\.desktop" \]',
r"'\$INSTALL_HOME/\.local/bin/vocalinux' --version",
r'\[ -x "\$INSTALL_HOME/\.local/bin/voculinux" \]',
r'\[ -f "\$INSTALL_HOME/\.local/share/applications/voculinux\.desktop" \]',
r"'\$INSTALL_HOME/\.local/bin/voculinux' --version",
):
assert re.search(construct, gate), f"the smoke no longer asserts {construct}"
@@ -306,13 +306,13 @@ def test_the_remote_gate_bootstraps_outside_a_checkout():
checkout exists. Overriding the clone source is what keeps the run on this
commit instead of the public repo's last release."""
gate = _without_comments(_remote_gate_text())
assert "VOCALINUX_REPO_URL" in gate, (
assert "VOCULINUX_REPO_URL" in gate, (
"the remote gate no longer redirects the clone source; it would"
" install the public repo's tag rather than this commit"
)
installer = _without_comments(INSTALLER.read_text(encoding="utf-8"))
assert re.search(r"VOCALINUX_REPO_URL:-https://github\.com/VocaHQ/vocalinux\.git", installer), (
"install.sh no longer honors VOCALINUX_REPO_URL; the gate's mirror"
assert re.search(r"VOCULINUX_REPO_URL:-https://github\.com/VocaHQ/vocalinux\.git", installer), (
"install.sh no longer honors VOCULINUX_REPO_URL; the gate's mirror"
" cannot reach it and the clone falls back to the public repo"
)
@@ -346,7 +346,7 @@ def test_the_remote_gate_checks_handoff_artifacts():
gate = _without_comments(_remote_gate_text())
for construct in (
r'\[ -x "\$REMOTE_VENV/bin/python" \]',
r'\[ -f "\$INSTALL_HOME/\.local/bin/activate-vocalinux\.sh" \]',
r'\[ -f "\$INSTALL_HOME/\.local/bin/activate-voculinux\.sh" \]',
):
assert re.search(construct, gate), f"the remote gate no longer asserts {construct}"
@@ -355,9 +355,9 @@ def test_the_remote_gate_smoke_tests_the_installed_app():
"""Same standard as the local gate: the app binary answers --version
through the wrapper the user's PATH carries."""
gate = _without_comments(_remote_gate_text())
assert re.search(r"'\$INSTALL_HOME/\.local/bin/vocalinux' --version", gate)
assert re.search(r"'\$INSTALL_HOME/\.local/bin/voculinux' --version", gate)
assert re.search(
r'\[ -x "\$INSTALL_HOME/\.local/bin/vocalinux" \]', gate
r'\[ -x "\$INSTALL_HOME/\.local/bin/voculinux" \]', gate
), "the remote gate no longer asserts the user-facing wrapper exists"
+10 -10
View File
@@ -67,7 +67,7 @@ print_info() {{ :; }}
require_distro_gi() {{ :; }}
VENV_DIR={shlex.quote(str(venv))}
INSTALL_DIR={shlex.quote(str(ROOT))}
VOCALINUX_TMP_DIR={shlex.quote(str(tmp_path))}
VOCULINUX_TMP_DIR={shlex.quote(str(tmp_path))}
{source}
{command}
"""
@@ -188,15 +188,15 @@ def test_remote_handoff_preserves_tagged_behavior(tmp_path: Path, remote_aware:
"""A main bootstrap must preserve remote behavior for every tag."""
clone = tmp_path / "old tagged tree"
clone.mkdir()
marker = 'CLEANUP_ON_EXIT="${VOCALINUX_REMOTE_INSTALL:-no}"\n' if remote_aware else ""
marker = 'CLEANUP_ON_EXIT="${VOCULINUX_REMOTE_INSTALL:-no}"\n' if remote_aware else ""
(clone / "install.sh").write_text(
"#!/bin/bash\n"
+ marker
+ 'printf "%s\\n" "${VOCALINUX_REMOTE_INSTALL:-no}" "$PWD" "$@"\n'
+ "helper=activate-vocalinux.sh\n"
+ 'if [ "${VOCALINUX_REMOTE_INSTALL:-no}" = yes ]; then\n'
+ 'printf "%s\\n" "${VOCULINUX_REMOTE_INSTALL:-no}" "$PWD" "$@"\n'
+ "helper=activate-voculinux.sh\n"
+ 'if [ "${VOCULINUX_REMOTE_INSTALL:-no}" = yes ]; then\n'
+ ' mkdir -p "$HOME/.local/bin"\n'
+ ' helper="$HOME/.local/bin/activate-vocalinux.sh"\n'
+ ' helper="$HOME/.local/bin/activate-voculinux.sh"\n'
+ "fi\n"
+ 'printf helper > "$helper"\n'
)
@@ -214,7 +214,7 @@ def test_remote_handoff_preserves_tagged_behavior(tmp_path: Path, remote_aware:
"-c",
f"set -eu\n{handoff}\ncd {shlex.quote(str(clone))}\n"
f"INSTALL_DIR={shlex.quote(str(clone))}\n"
f"VOCALINUX_TMP_DIR={shlex.quote(str(scratch))}\n"
f"VOCULINUX_TMP_DIR={shlex.quote(str(scratch))}\n"
"INSTALLER_ARGS=(--auto --tag=v0.17.0 --engine=remote_api)\n"
'print_error() { echo "$*" >&2; }\n'
"handoff_to_tagged_installer",
@@ -231,12 +231,12 @@ def test_remote_handoff_preserves_tagged_behavior(tmp_path: Path, remote_aware:
"--auto",
"--tag=v0.17.0",
"--engine=remote_api",
f"--venv-dir={home}/.local/share/vocalinux/venv",
f"--venv-dir={home}/.local/share/voculinux/venv",
]
assert scratch.exists() != remote_aware
helper = home / ".local/bin/activate-vocalinux.sh"
helper = home / ".local/bin/activate-voculinux.sh"
assert helper.read_text() == "helper"
assert not (clone / "activate-vocalinux.sh").exists()
assert not (clone / "activate-voculinux.sh").exists()
def test_every_pip_install_has_an_explicit_resolution_boundary() -> None:
+8 -8
View File
@@ -43,16 +43,16 @@ def test_help_does_not_create_install_log_or_scratch_dir(tmp_path, monkeypatch)
)
assert result.returncode == 0
assert not list((tmp_path / "state").rglob("install-*.log"))
assert not list((tmp_path / "tmp").glob("vocalinux-install.*"))
assert not list((tmp_path / "tmp").glob("voculinux-install.*"))
def test_scratch_dir_owns_pip_logs_and_model_temps() -> None:
source = _installer_source()
assert 'PIP_LOG_DIR="$VOCALINUX_TMP_DIR/pip"' in source
assert 'export TMPDIR="$VOCALINUX_TMP_DIR"' in source
assert 'TEMP_FILE="$VOCALINUX_TMP_DIR/tiny.pt"' in source
assert 'TEMP_FILE="$VOCALINUX_TMP_DIR/ggml-tiny.bin"' in source
assert 'TEMP_ZIP="$VOCALINUX_TMP_DIR/$(basename $SMALL_MODEL_URL)"' in source
assert 'PIP_LOG_DIR="$VOCULINUX_TMP_DIR/pip"' in source
assert 'export TMPDIR="$VOCULINUX_TMP_DIR"' in source
assert 'TEMP_FILE="$VOCULINUX_TMP_DIR/tiny.pt"' in source
assert 'TEMP_FILE="$VOCULINUX_TMP_DIR/ggml-tiny.bin"' in source
assert 'TEMP_ZIP="$VOCULINUX_TMP_DIR/$(basename $SMALL_MODEL_URL)"' in source
def test_system_dep_and_venv_failures_use_structured_exit_codes() -> None:
@@ -67,10 +67,10 @@ def test_system_dep_and_venv_failures_use_structured_exit_codes() -> None:
bootstrap_failure = source.split("install_pinned_build_tools || {", 1)[1].split("}", 1)[0]
assert 'exit "$EXIT_NETWORK"' in bootstrap_failure
assert (
'print_error "Failed to install Vocalinux package. Installation cannot continue."' in source
'print_error "Failed to install Voculinux package. Installation cannot continue."' in source
)
assert 'exit "$EXIT_NETWORK"' in source
assert "Could not terminate all Vocalinux processes" in source
assert "Could not terminate all Voculinux processes" in source
assert 'exit "$EXIT_USER_ABORT"' in source
+1 -1
View File
@@ -108,7 +108,7 @@ def test_piped_installer_reaches_past_the_stdin_probe(tmp_path: Path) -> None:
check=False,
)
output = transcript.read_text(encoding="utf-8", errors="replace")
assert "Vocalinux Installer" in output
assert "Voculinux Installer" in output
assert "Usage:" in output
+3 -3
View File
@@ -359,7 +359,7 @@ run_interactive_install
echo "U=$UPGRADE_MODE N=$NON_INTERACTIVE I=$INTERACTIVE_MODE"
""")
assert "existing Vocalinux installation was detected" in result.stdout
assert "existing Voculinux installation was detected" in result.stdout
assert "REACHED_GUIDED_SETUP" not in result.stdout
assert "U=yes N=yes I=no" in result.stdout
@@ -407,7 +407,7 @@ def test_piped_installer_defaults_to_upgrade_on_existing_install(tmp_path: Path)
venv = tmp_path / "venv"
(venv / "bin").mkdir(parents=True)
(venv / "bin" / "activate").touch()
config_dir = tmp_path / ".config" / "vocalinux"
config_dir = tmp_path / ".config" / "voculinux"
config_dir.mkdir(parents=True)
(config_dir / "config.json").write_text(json.dumps({"speech_recognition": {"engine": "vosk"}}))
@@ -432,7 +432,7 @@ def test_piped_installer_defaults_to_upgrade_on_existing_install(tmp_path: Path)
env = os.environ.copy()
env["HOME"] = str(tmp_path)
# conftest points XDG_CONFIG_HOME at an isolation dir; the installer reads
# config.json from $XDG_CONFIG_HOME/vocalinux, not straight from $HOME.
# config.json from $XDG_CONFIG_HOME/voculinux, not straight from $HOME.
env["XDG_CONFIG_HOME"] = str(tmp_path / ".config")
env["XDG_STATE_HOME"] = str(tmp_path / ".local" / "state")
env["TMPDIR"] = str(tmp_path)
+15 -15
View File
@@ -1,5 +1,5 @@
"""
Branch coverage tests for Vocalinux.
Branch coverage tests for Voculinux.
This module tests uncovered branches in:
- recognition_manager.py
@@ -533,7 +533,7 @@ class TestRecognitionManagerModelReady:
class TestParseArguments:
"""Test parse_arguments function."""
@patch("sys.argv", ["vocalinux", "--debug"])
@patch("sys.argv", ["voculinux", "--debug"])
def test_parse_arguments_debug(self):
"""Test parse_arguments with --debug flag."""
from vocalinux.main import parse_arguments
@@ -541,7 +541,7 @@ class TestParseArguments:
args = parse_arguments()
assert args.debug is True
@patch("sys.argv", ["vocalinux", "--engine", "whisper"])
@patch("sys.argv", ["voculinux", "--engine", "whisper"])
def test_parse_arguments_engine(self):
"""Test parse_arguments with --engine flag."""
from vocalinux.main import parse_arguments
@@ -549,7 +549,7 @@ class TestParseArguments:
args = parse_arguments()
assert args.engine == "whisper"
@patch("sys.argv", ["vocalinux", "--model", "large"])
@patch("sys.argv", ["voculinux", "--model", "large"])
def test_parse_arguments_model(self):
"""Test parse_arguments with --model flag."""
from vocalinux.main import parse_arguments
@@ -557,7 +557,7 @@ class TestParseArguments:
args = parse_arguments()
assert args.model == "large"
@patch("sys.argv", ["vocalinux", "--language", "en-us"])
@patch("sys.argv", ["voculinux", "--language", "en-us"])
def test_parse_arguments_language(self):
"""Test parse_arguments with --language flag."""
from vocalinux.main import parse_arguments
@@ -565,7 +565,7 @@ class TestParseArguments:
args = parse_arguments()
assert args.language == "en-us"
@patch("sys.argv", ["vocalinux", "--wayland"])
@patch("sys.argv", ["voculinux", "--wayland"])
def test_parse_arguments_wayland(self):
"""Test parse_arguments with --wayland flag."""
from vocalinux.main import parse_arguments
@@ -573,7 +573,7 @@ class TestParseArguments:
args = parse_arguments()
assert args.wayland is True
@patch("sys.argv", ["vocalinux", "--start-minimized"])
@patch("sys.argv", ["voculinux", "--start-minimized"])
def test_parse_arguments_start_minimized(self):
"""Test parse_arguments with --start-minimized flag."""
from vocalinux.main import parse_arguments
@@ -898,7 +898,7 @@ class TestRecognitionManagerUpdateState:
class TestParseArgumentsCombinations:
"""Test parse_arguments with various flag combinations."""
@patch("sys.argv", ["vocalinux", "--debug", "--engine", "vosk", "--wayland"])
@patch("sys.argv", ["voculinux", "--debug", "--engine", "vosk", "--wayland"])
def test_parse_arguments_multiple_flags(self):
"""Test parse_arguments with multiple flags."""
from vocalinux.main import parse_arguments
@@ -908,7 +908,7 @@ class TestParseArgumentsCombinations:
assert args.engine == "vosk"
assert args.wayland is True
@patch("sys.argv", ["vocalinux"])
@patch("sys.argv", ["voculinux"])
def test_parse_arguments_no_flags(self):
"""Test parse_arguments with no flags."""
from vocalinux.main import parse_arguments
@@ -1306,7 +1306,7 @@ class TestCallbackManagement:
class TestParseArgumentsAllCombos:
"""Test all argument combinations."""
@patch("sys.argv", ["vocalinux", "--model", "small"])
@patch("sys.argv", ["voculinux", "--model", "small"])
def test_parse_arguments_model_small(self):
"""Test parse_arguments with small model."""
from vocalinux.main import parse_arguments
@@ -1314,7 +1314,7 @@ class TestParseArgumentsAllCombos:
args = parse_arguments()
assert args.model == "small"
@patch("sys.argv", ["vocalinux", "--model", "medium"])
@patch("sys.argv", ["voculinux", "--model", "medium"])
def test_parse_arguments_model_medium(self):
"""Test parse_arguments with medium model."""
from vocalinux.main import parse_arguments
@@ -1322,7 +1322,7 @@ class TestParseArgumentsAllCombos:
args = parse_arguments()
assert args.model == "medium"
@patch("sys.argv", ["vocalinux", "--language", "hi"])
@patch("sys.argv", ["voculinux", "--language", "hi"])
def test_parse_arguments_language_hi(self):
"""Test parse_arguments with Hindi language."""
from vocalinux.main import parse_arguments
@@ -1330,7 +1330,7 @@ class TestParseArgumentsAllCombos:
args = parse_arguments()
assert args.language == "hi"
@patch("sys.argv", ["vocalinux", "--language", "es"])
@patch("sys.argv", ["voculinux", "--language", "es"])
def test_parse_arguments_language_es(self):
"""Test parse_arguments with Spanish language."""
from vocalinux.main import parse_arguments
@@ -1338,7 +1338,7 @@ class TestParseArgumentsAllCombos:
args = parse_arguments()
assert args.language == "es"
@patch("sys.argv", ["vocalinux", "--engine", "vosk"])
@patch("sys.argv", ["voculinux", "--engine", "vosk"])
def test_parse_arguments_engine_vosk(self):
"""Test parse_arguments with vosk engine."""
from vocalinux.main import parse_arguments
@@ -1346,7 +1346,7 @@ class TestParseArgumentsAllCombos:
args = parse_arguments()
assert args.engine == "vosk"
@patch("sys.argv", ["vocalinux", "--engine", "whisper_cpp"])
@patch("sys.argv", ["voculinux", "--engine", "whisper_cpp"])
def test_parse_arguments_engine_whisper_cpp(self):
"""Test parse_arguments with whisper_cpp engine."""
from vocalinux.main import parse_arguments
+9 -9
View File
@@ -56,7 +56,7 @@ class TestDesktopEnvironmentDetect:
is set — pynput over XWayland is not truly global.
"""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"DISPLAY": ":0",
}
@@ -66,7 +66,7 @@ class TestDesktopEnvironmentDetect:
def test_detect_flatpak_wayland_with_socket_stays_wayland(self):
"""With a Wayland socket exposed, detection still reports Wayland."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"WAYLAND_DISPLAY": "wayland-0",
}
@@ -81,7 +81,7 @@ class TestDesktopEnvironmentDetect:
fake.is_available.return_value = True
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"DISPLAY": ":0",
}
@@ -119,7 +119,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_native_x11_prefers_pynput(self) -> None:
"""Flatpak on native X11 uses pynput outside GNOME."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "x11",
"XDG_CURRENT_DESKTOP": "X-Cinnamon",
"DISPLAY": ":0",
@@ -140,7 +140,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_gnome_native_x11_prefers_evdev(self) -> None:
"""Flatpak on GNOME X11 prefers evdev."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "x11",
"XDG_CURRENT_DESKTOP": "GNOME",
"DISPLAY": ":0",
@@ -162,7 +162,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_gnome_native_x11_falls_back_to_pynput(self) -> None:
"""Flatpak on GNOME X11 falls back when evdev cannot read /dev/input."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "x11",
"XDG_CURRENT_DESKTOP": "GNOME",
"DISPLAY": ":0",
@@ -187,7 +187,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_x11_with_wayland_display_prefers_evdev(self) -> None:
"""A Wayland socket keeps Flatpak on the evdev path."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "x11",
"WAYLAND_DISPLAY": "wayland-0",
"DISPLAY": ":0",
@@ -209,7 +209,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_xwayland_without_evdev_returns_none(self) -> None:
"""A Flatpak on a Wayland session without input access needs evdev."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"DISPLAY": ":0",
}
@@ -231,7 +231,7 @@ class TestDesktopEnvironmentDetect:
def test_create_backend_flatpak_unknown_session_prefers_evdev(self) -> None:
"""Unknown Flatpak sessions conservatively prefer evdev."""
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"DISPLAY": ":0",
}
fake = MagicMock()
+1 -1
View File
@@ -375,7 +375,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
def test_unavailable_hints_snap_lists_both_connects(self):
"""Snap log banner should list both raw-input and hardware-observe."""
self.mock_create_backend.return_value = None
with patch.dict("os.environ", {"SNAP": "/snap/vocalinux/x1"}, clear=False):
with patch.dict("os.environ", {"SNAP": "/snap/voculinux/x1"}, clear=False):
with patch("vocalinux.ui.keyboard_shortcuts.logger") as mock_logger:
KeyboardShortcutManager()
+3 -3
View File
@@ -106,7 +106,7 @@ def test_normalize_language_shortcuts_dedupes_by_shortcut() -> None:
def test_language_shortcuts_roundtrip_through_config(tmp_path: Any) -> None:
config_dir = tmp_path / "vocalinux"
config_dir = tmp_path / "voculinux"
config_dir.mkdir()
config_file = config_dir / "config.json"
with (
@@ -137,7 +137,7 @@ def test_language_shortcuts_roundtrip_through_config(tmp_path: Any) -> None:
def test_get_language_shortcuts_survives_a_broken_shortcuts_section() -> None:
"""A hand-edited config replacing the section with a scalar reads as empty."""
with tempfile.TemporaryDirectory() as temp_dir:
config_dir = os.path.join(temp_dir, "vocalinux")
config_dir = os.path.join(temp_dir, "voculinux")
config_file = os.path.join(config_dir, "config.json")
os.makedirs(config_dir, exist_ok=True)
with open(config_file, "w") as handle:
@@ -1062,7 +1062,7 @@ def test_slovenian_shortcut_row_persists_through_config(
dialog_class: type[Any], tmp_path: Any
) -> None:
"""A row picked as Slovenian is saved and read back, not dropped (#970)."""
config_dir = tmp_path / "vocalinux"
config_dir = tmp_path / "voculinux"
config_dir.mkdir()
with (
patch("vocalinux.ui.config_manager.CONFIG_DIR", str(config_dir)),
+147
View File
@@ -0,0 +1,147 @@
"""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"
+5 -5
View File
@@ -129,7 +129,7 @@ class TestLoggingManager:
manager = lm.LoggingManager(max_records=10)
try:
assert manager.logs_dir == tmp_path / "vocalinux" / "logs"
assert manager.logs_dir == tmp_path / "voculinux" / "logs"
assert manager.logs_dir.is_dir()
finally:
root_logger.removeHandler(manager.handler)
@@ -296,7 +296,7 @@ class TestLoggingManager:
record2 = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="vocalinux.speech.test",
logger_name="voculinux.speech.test",
message="Speech message",
)
logging_manager.add_log_record(record1)
@@ -353,7 +353,7 @@ class TestLoggingManager:
with open(filepath, "r") as f:
content = f.read()
assert "VocaLinux Log Export" in content
assert "Voculinux Log Export" in content
assert "Export test message" in content
finally:
if os.path.exists(filepath):
@@ -451,7 +451,7 @@ class TestLoggingManager:
record2 = LogRecord(
timestamp=timestamp2,
level="ERROR",
logger_name="vocalinux.speech.test",
logger_name="voculinux.speech.test",
message="Message 2",
)
logging_manager.add_log_record(record1)
@@ -462,7 +462,7 @@ class TestLoggingManager:
assert stats["total"] == 2
assert stats["by_level"]["INFO"] == 1
assert stats["by_level"]["ERROR"] == 1
assert "vocalinux" in stats["by_module"]
assert "voculinux" in stats["by_module"]
assert stats["oldest"] == timestamp1
assert stats["newest"] == timestamp2
+18 -18
View File
@@ -29,7 +29,7 @@ class TestMainModule(unittest.TestCase):
def test_parse_arguments_defaults(self):
"""Test argument parsing with defaults."""
# Test with no arguments (model/engine/language will be None without defaults)
with patch("sys.argv", ["vocalinux"]):
with patch("sys.argv", ["voculinux"]):
args = parse_arguments()
self.assertFalse(args.debug)
self.assertIsNone(args.model) # No default set, loaded from config instead
@@ -45,7 +45,7 @@ class TestMainModule(unittest.TestCase):
with patch(
"sys.argv",
[
"vocalinux",
"voculinux",
"--debug",
"--model",
"large",
@@ -67,33 +67,33 @@ class TestMainModule(unittest.TestCase):
def test_parse_arguments_model_values(self):
"""Test model parsing for base and exact whisper.cpp model IDs."""
with patch("sys.argv", ["vocalinux", "--model", "small"]):
with patch("sys.argv", ["voculinux", "--model", "small"]):
args = parse_arguments()
self.assertEqual(args.model, "small")
with patch("sys.argv", ["vocalinux", "--model", "medium"]):
with patch("sys.argv", ["voculinux", "--model", "medium"]):
args = parse_arguments()
self.assertEqual(args.model, "medium")
with patch("sys.argv", ["vocalinux", "--model", "large"]):
with patch("sys.argv", ["voculinux", "--model", "large"]):
args = parse_arguments()
self.assertEqual(args.model, "large")
with patch("sys.argv", ["vocalinux", "--model", "medium.en-q5_0"]):
with patch("sys.argv", ["voculinux", "--model", "medium.en-q5_0"]):
args = parse_arguments()
self.assertEqual(args.model, "medium.en-q5_0")
with patch("sys.argv", ["vocalinux", "--model", "large-v3-turbo"]):
with patch("sys.argv", ["voculinux", "--model", "large-v3-turbo"]):
args = parse_arguments()
self.assertEqual(args.model, "large-v3-turbo")
def test_parse_arguments_engine_choices(self):
"""Test that engine only accepts valid choices."""
with patch("sys.argv", ["vocalinux", "--engine", "vosk"]):
with patch("sys.argv", ["voculinux", "--engine", "vosk"]):
args = parse_arguments()
self.assertEqual(args.engine, "vosk")
with patch("sys.argv", ["vocalinux", "--engine", "whisper"]):
with patch("sys.argv", ["voculinux", "--engine", "whisper"]):
args = parse_arguments()
self.assertEqual(args.engine, "whisper")
@@ -106,7 +106,7 @@ class TestMainModule(unittest.TestCase):
# Spot-check newly exposed languages (issue #565 and catalog expansion).
for lang in ["auto", "en-us", "hu", "ja", "ko", "ar", "nl", "pl", "sl", "uk"]:
self.assertIn(lang, LANGUAGE_CHOICES)
with patch("sys.argv", ["vocalinux", "--language", lang]):
with patch("sys.argv", ["voculinux", "--language", lang]):
args = parse_arguments()
self.assertEqual(args.language, lang)
@@ -399,7 +399,7 @@ class TestMainModule(unittest.TestCase):
mock_args.start_minimized = False
mock_parse.return_value = mock_args
with patch("sys.argv", ["vocalinux", "--engine", engine]):
with patch("sys.argv", ["voculinux", "--engine", engine]):
main()
text_callback = mock_speech_instance.register_text_callback.call_args.args[0]
@@ -923,7 +923,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
with patch(
"sys.argv",
[
"vocalinux",
"voculinux",
"--engine",
"whisper",
"--model",
@@ -989,7 +989,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
mock_action_handler.return_value = mock_action_instance
# No CLI args for engine/model/language
with patch("sys.argv", ["vocalinux"]):
with patch("sys.argv", ["voculinux"]):
with patch("vocalinux.main.logger"):
main()
@@ -1040,7 +1040,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
with patch(
"sys.argv",
[
"vocalinux",
"voculinux",
"--engine",
"parakeet",
"--model",
@@ -1094,7 +1094,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
mock_tray.return_value = MagicMock()
mock_action_handler.return_value = MagicMock()
with patch("sys.argv", ["vocalinux"]):
with patch("sys.argv", ["voculinux"]):
with patch("vocalinux.main.logger"):
main()
@@ -1151,7 +1151,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
mock_tray.return_value = MagicMock()
mock_action_handler.return_value = MagicMock()
with patch("sys.argv", ["vocalinux"]):
with patch("sys.argv", ["voculinux"]):
with patch("vocalinux.main.logger"):
main()
@@ -1368,7 +1368,7 @@ def _boot_main_callbacks(
)
)
mock_parse = stack.enter_context(patch("vocalinux.main.parse_arguments"))
stack.enter_context(patch("sys.argv", ["vocalinux"]))
stack.enter_context(patch("sys.argv", ["voculinux"]))
mock_args = MagicMock()
mock_args.debug = False
@@ -2144,7 +2144,7 @@ class TestSessionHistoryRecording(unittest.TestCase):
stack.enter_context(patch("vocalinux.ui.logging_manager.initialize_logging"))
mock_parse = stack.enter_context(patch("vocalinux.main.parse_arguments"))
stack.enter_context(patch("sys.argv", ["vocalinux"]))
stack.enter_context(patch("sys.argv", ["voculinux"]))
mock_args = MagicMock()
mock_args.debug = False
+14 -14
View File
@@ -1,7 +1,7 @@
"""
Additional coverage tests for main.py module.
Tests for parse_args(), main(), and VocalinuxApp class (if present).
Tests for parse_args(), main(), and VoculinuxApp class (if present).
Covers normal startup, debug mode, version flag, keyboard interrupt, and exception handling.
"""
@@ -41,7 +41,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_defaults(self):
"""Test parsing arguments with defaults."""
with patch.object(sys, "argv", ["vocalinux"]):
with patch.object(sys, "argv", ["voculinux"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -55,7 +55,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_debug_flag(self):
"""Test parsing with debug flag."""
with patch.object(sys, "argv", ["vocalinux", "--debug"]):
with patch.object(sys, "argv", ["voculinux", "--debug"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -66,18 +66,18 @@ class TestParseArguments(unittest.TestCase):
from vocalinux.version import __version__
output = StringIO()
with patch.object(sys, "argv", ["vocalinux", "--version"]):
with patch.object(sys, "argv", ["voculinux", "--version"]):
from vocalinux.main import parse_arguments
with redirect_stdout(output), self.assertRaises(SystemExit) as exit_context:
parse_arguments()
assert exit_context.exception.code == 0
assert output.getvalue() == f"vocalinux {__version__}\n"
assert output.getvalue() == f"voculinux {__version__}\n"
def test_parse_args_model_argument(self):
"""Test parsing with model argument."""
with patch.object(sys, "argv", ["vocalinux", "--model", "small"]):
with patch.object(sys, "argv", ["voculinux", "--model", "small"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -85,7 +85,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_language_argument(self):
"""Test parsing with language argument."""
with patch.object(sys, "argv", ["vocalinux", "--language", "en-us"]):
with patch.object(sys, "argv", ["voculinux", "--language", "en-us"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -93,7 +93,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_engine_argument(self):
"""Test parsing with engine argument."""
with patch.object(sys, "argv", ["vocalinux", "--engine", "whisper_cpp"]):
with patch.object(sys, "argv", ["voculinux", "--engine", "whisper_cpp"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -101,7 +101,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_wayland_flag(self):
"""Test parsing with wayland flag."""
with patch.object(sys, "argv", ["vocalinux", "--wayland"]):
with patch.object(sys, "argv", ["voculinux", "--wayland"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -109,7 +109,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_start_minimized_flag(self):
"""Test parsing with start-minimized flag."""
with patch.object(sys, "argv", ["vocalinux", "--start-minimized"]):
with patch.object(sys, "argv", ["voculinux", "--start-minimized"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -117,7 +117,7 @@ class TestParseArguments(unittest.TestCase):
def test_parse_args_transcribe_file(self):
"""Test parsing with the headless --transcribe-file flag."""
with patch.object(sys, "argv", ["vocalinux", "--transcribe-file", "/tmp/a.wav"]):
with patch.object(sys, "argv", ["voculinux", "--transcribe-file", "/tmp/a.wav"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
@@ -157,7 +157,7 @@ class TestParseArguments(unittest.TestCase):
sys,
"argv",
[
"vocalinux",
"voculinux",
"--debug",
"--model",
"large",
@@ -353,13 +353,13 @@ class TestMainFunction(unittest.TestCase):
output = StringIO()
with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}):
with patch.object(sys, "argv", ["vocalinux", "--version"]):
with patch.object(sys, "argv", ["voculinux", "--version"]):
with redirect_stdout(output):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
assert output.getvalue() == f"vocalinux {__version__}\n"
assert output.getvalue() == f"voculinux {__version__}\n"
mock_single_instance.acquire_lock.assert_not_called()
@patch("vocalinux.main.check_display_available")
+1 -1
View File
@@ -341,7 +341,7 @@ class TestMainFunctionExtra:
with patch.object(
sys,
"argv",
["vocalinux", "--engine", "vosk", "--model", "medium", "--language", "es"],
["voculinux", "--engine", "vosk", "--model", "medium", "--language", "es"],
):
with pytest.raises(SystemExit) as exc_info:
main()
+6 -6
View File
@@ -140,7 +140,7 @@ class TestManifestCoverage(unittest.TestCase):
class TestVerifyFile(unittest.TestCase):
def setUp(self):
self.tmp = Path(self.enterContext(__import__("tempfile").TemporaryDirectory()))
self.payload = b"vocalinux model payload"
self.payload = b"voculinux model payload"
self.path = self.tmp / "model.bin"
self.path.write_bytes(self.payload)
self.digest = hashlib.sha256(self.payload).hexdigest()
@@ -416,7 +416,7 @@ class TestExistingModelsAreVerified(unittest.TestCase):
for name, checker in (
("install_whisper_model", "verify_openai_model_checksum"),
("install_whispercpp_model", "verify_model_checksum"),
("install_vosk_models", ".vocalinux_verified"),
("install_vosk_models", ".voculinux_verified"),
):
guard = self._exists_guard(name)
self.assertLess(
@@ -484,9 +484,9 @@ print_error() { echo "ERROR: $*"; }
print_success() { echo "SUCCESS: $*"; }
command_exists() { command -v "$1" >/dev/null 2>&1; }
DATA_DIR="$ROOT/data"
VOCALINUX_TMP_DIR="$ROOT/tmp"
VOCULINUX_TMP_DIR="$ROOT/tmp"
MODEL_CHECKSUMS_FILE="$ROOT/model_checksums.txt"
mkdir -p "$VOCALINUX_TMP_DIR"
mkdir -p "$VOCULINUX_TMP_DIR"
# The installer only probes for these; the download itself is stubbed out.
check_connectivity() { return 0; }
@@ -663,8 +663,8 @@ download_model_file() { echo "DOWNLOAD ATTEMPTED"; return 1; }
"""The 40MB fetch must not happen when nothing can verify it."""
with TemporaryDirectory() as tmp:
result = self._run(
f'DATA_DIR="{tmp}/data"; VOCALINUX_TMP_DIR="{tmp}/tmp"; '
f'mkdir -p "$DATA_DIR" "$VOCALINUX_TMP_DIR"; '
f'DATA_DIR="{tmp}/data"; VOCULINUX_TMP_DIR="{tmp}/tmp"; '
f'mkdir -p "$DATA_DIR" "$VOCULINUX_TMP_DIR"; '
f'MODEL_CHECKSUMS_FILE="{tmp}/nope.txt"; '
"install_vosk_models; echo RC=$?",
functions=("pinned_digest_for", "install_vosk_models"),
+6 -6
View File
@@ -26,7 +26,7 @@ RELEASE_YML = WORKFLOWS / "release.yml"
PIPELINE_YML = WORKFLOWS / "unified-pipeline.yml"
#: Runtime dependencies no distro packages; build.sh vendors their wheels
#: under /usr/lib/vocalinux/vendor instead of declaring distro deps.
#: under /usr/lib/voculinux/vendor instead of declaring distro deps.
VENDORED = ("pywhispercpp", "pynput")
#: pip name -> the Debian/Ubuntu package that Provides it. pywhispercpp's own
@@ -267,9 +267,9 @@ def test_nfpm_contents_and_launcher_env_agree() -> None:
text = BUILD_SH.read_text(encoding="utf-8")
smoke = SMOKE_SH.read_text(encoding="utf-8")
for where in (text, smoke):
assert "/usr/lib/vocalinux/app" in where
assert "/usr/lib/vocalinux/vendor" in where
assert "/usr/bin/vocalinux" in text and "/usr/bin/vocalinux-gui" in text
assert "/usr/lib/voculinux/app" in where
assert "/usr/lib/voculinux/vendor" in where
assert "/usr/bin/voculinux" in text and "/usr/bin/voculinux-gui" in text
assert "pywhispercpp.libs" in text and "pywhispercpp.libs" in smoke
# Arguments keep their boundaries: $* into an unquoted exec re-splits a
# path like "Meeting notes.wav" before argparse sees it (PR #913).
@@ -286,8 +286,8 @@ def test_smoke_gate_covers_both_formats() -> None:
smoke = SMOKE_SH.read_text(encoding="utf-8")
assert "debian|ubuntu)" in smoke, "smoke must install the .deb on Debian-family images"
assert "fedora)" in smoke, "smoke must install the .rpm on Fedora"
assert "vocalinux --version" in smoke
assert "vocalinux-gui --version" in smoke
assert "voculinux --version" in smoke
assert "voculinux-gui --version" in smoke
assert "pywhispercpp.model" in smoke, "the smoke must prove the vendored extension loads"
assert (
'find_spec("pynput")' in smoke
+1 -1
View File
@@ -139,7 +139,7 @@ class TestYdotoolTerminalPasteCommand(unittest.TestCase):
def test_flatpak_uses_shift_keycodes(self, mock_which, _mock_map):
mock_which.return_value = "/app/bin/ydotool"
injector = _make_injector()
with patch.dict("os.environ", {"FLATPAK_ID": "com.vocalinux.Vocalinux"}):
with patch.dict("os.environ", {"FLATPAK_ID": "com.voculinux.Voculinux"}):
self.assertEqual(
injector._ydotool_ctrl_v_command(terminal=True),
["ydotool", "key", "29:1", "42:1", "47:1", "47:0", "42:0", "29:0"],
+8 -8
View File
@@ -11,9 +11,9 @@ class TestXdgHelpers:
with patch.dict(os.environ, {}, clear=True):
assert paths.xdg_config_home() == os.path.expanduser("~/.config")
assert paths.xdg_data_home() == os.path.expanduser("~/.local/share")
assert paths.config_dir() == os.path.expanduser("~/.config/vocalinux")
assert paths.data_dir() == os.path.expanduser("~/.local/share/vocalinux")
assert paths.models_dir() == os.path.expanduser("~/.local/share/vocalinux/models")
assert paths.config_dir() == os.path.expanduser("~/.config/voculinux")
assert paths.data_dir() == os.path.expanduser("~/.local/share/voculinux")
assert paths.models_dir() == os.path.expanduser("~/.local/share/voculinux/models")
def test_honours_xdg_environment(self):
env = {
@@ -21,9 +21,9 @@ class TestXdgHelpers:
"XDG_DATA_HOME": "/run/user/1000/data",
}
with patch.dict(os.environ, env, clear=True):
assert paths.config_dir() == "/run/user/1000/cfg/vocalinux"
assert paths.data_dir() == "/run/user/1000/data/vocalinux"
assert paths.models_dir() == "/run/user/1000/data/vocalinux/models"
assert paths.config_dir() == "/run/user/1000/cfg/voculinux"
assert paths.data_dir() == "/run/user/1000/data/voculinux"
assert paths.models_dir() == "/run/user/1000/data/voculinux/models"
def test_empty_env_falls_back_to_default(self):
# The XDG spec says empty values must be treated as unset.
@@ -33,9 +33,9 @@ class TestXdgHelpers:
def test_flatpak_style_data_home(self):
# Flatpak points XDG_DATA_HOME at ~/.var/app/<app-id>/data.
flatpak_data = "/home/user/.var/app/com.vocalinux.Vocalinux/data"
flatpak_data = "/home/user/.var/app/com.voculinux.Voculinux/data"
with patch.dict(os.environ, {"XDG_DATA_HOME": flatpak_data}, clear=True):
assert paths.models_dir() == os.path.join(flatpak_data, "vocalinux", "models")
assert paths.models_dir() == os.path.join(flatpak_data, "voculinux", "models")
def test_is_within_directory(self, tmp_path):
nested = tmp_path / "models" / "ggml-tiny.bin"
+2 -2
View File
@@ -21,9 +21,9 @@ SPEC.loader.exec_module(PROTOCOL)
def test_parse_vmrss_kb_reads_kilobytes() -> None:
"""VmRSS lines parse to kilobytes; anything else yields None."""
status = "Name:\tvocalinux\nVmRSS:\t 412336 kB\nVmSwap:\t0 kB\n"
status = "Name:\tvoculinux\nVmRSS:\t 412336 kB\nVmSwap:\t0 kB\n"
assert PROTOCOL.parse_vmrss_kb(status) == 412336
assert PROTOCOL.parse_vmrss_kb("Name:\tvocalinux\n") is None
assert PROTOCOL.parse_vmrss_kb("Name:\tvoculinux\n") is None
def test_describe_reports_count_mean_min_max() -> None:
+1 -1
View File
@@ -635,7 +635,7 @@ class TestManagerPipeWireIntegration(unittest.TestCase):
if isinstance(sys.modules.get("numpy"), MagicMock):
# Restore the stashed real module — re-importing it would
# leave sys.modules["numpy.*"] bound to an orphan.
real_numpy = getattr(sys, "_vocalinux_real_numpy", None)
real_numpy = getattr(sys, "_voculinux_real_numpy", None)
if real_numpy is not None:
sys.modules["numpy"] = real_numpy
else:
+2 -2
View File
@@ -36,7 +36,7 @@ def _numpy_is_real_while_comparing() -> Iterator[None]:
``isinstance(value, numpy.bool_)`` and raises TypeError.
"""
current = sys.modules.get("numpy")
real = getattr(sys, "_vocalinux_real_numpy", None)
real = getattr(sys, "_voculinux_real_numpy", None)
if isinstance(current, MagicMock) and real is not None:
sys.modules["numpy"] = real
try:
@@ -620,7 +620,7 @@ def test_pactl_restores_each_channel(tmp_path: Path) -> None:
def test_playback_duck_percent_clamps_and_round_trips(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
config_dir = tmp_path / "vocalinux"
config_dir = tmp_path / "voculinux"
config_dir.mkdir()
config_file = config_dir / "config.json"
monkeypatch.setattr("vocalinux.ui.config_manager.CONFIG_DIR", str(config_dir))
+7 -7
View File
@@ -35,10 +35,10 @@ def sdist(
extra: tuple[str, ...] = (),
) -> Path:
"""Write a small real sdist with independently controllable metadata/members."""
path = directory / f"vocalinux-{version}.tar.gz"
info = metadata if metadata is not None else f"Name: vocalinux\nVersion: {version}\n"
path = directory / f"voculinux-{version}.tar.gz"
info = metadata if metadata is not None else f"Name: voculinux\nVersion: {version}\n"
with tarfile.open(path, "w:gz") as archive:
for name, text in [(f"vocalinux-{version}/PKG-INFO", info)] + [
for name, text in [(f"voculinux-{version}/PKG-INFO", info)] + [
(name, "payload") for name in extra
]:
member = tarfile.TarInfo(name)
@@ -208,11 +208,11 @@ def test_release_verification_cannot_be_silently_skipped(
"metadata,extra",
[
("Name: other\nVersion: 0.18.4\n", ()),
("Name: vocalinux\nVersion: 0.18.3\n", ()),
("Name: vocalinux\nVersion: 0.18.4\nVersion: 0.18.3\n", ()),
("Name: voculinux\nVersion: 0.18.3\n", ()),
("Name: voculinux\nVersion: 0.18.4\nVersion: 0.18.3\n", ()),
(None, ("other-root/file",)),
(None, ("vocalinux-0.18.4/../unsafe",)),
(None, ("vocalinux-0.18.4/PKG-INFO",)),
(None, ("voculinux-0.18.4/../unsafe",)),
(None, ("voculinux-0.18.4/PKG-INFO",)),
],
)
def test_wrong_or_ambiguous_archive_metadata_is_rejected(
+3 -3
View File
@@ -38,7 +38,7 @@ from vocalinux.audio.pulse_sources import (
from vocalinux.utils import whispercpp_model_info
from vocalinux.utils.snap import SNAP_GPU_NOTE, running_in_snap
SNAP_ENV = {"SNAP": "/snap/vocalinux/42", "SNAP_NAME": "vocalinux"}
SNAP_ENV = {"SNAP": "/snap/voculinux/42", "SNAP_NAME": "voculinux"}
PACTL_JSON = [
{
@@ -108,7 +108,7 @@ def _runner(
def _have_pactl(name: str) -> Optional[str]:
return "/snap/vocalinux/42/usr/bin/pactl" if name == "pactl" else None
return "/snap/voculinux/42/usr/bin/pactl" if name == "pactl" else None
class TestPactlParsing:
@@ -503,7 +503,7 @@ class TestSnapGpuReporting:
def test_running_in_snap(self) -> None:
assert running_in_snap(SNAP_ENV)
assert running_in_snap({"SNAP": "/snap/vocalinux/42"})
assert running_in_snap({"SNAP": "/snap/voculinux/42"})
assert not running_in_snap({})
def test_snap_reports_cpu_without_probing_host_gpu_tools(
+1 -1
View File
@@ -14,7 +14,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
INSTALL_SH = REPO_ROOT / "install.sh"
PIPELINE = REPO_ROOT / ".github" / "workflows" / "unified-pipeline.yml"
PKGBUILD = REPO_ROOT / "packaging" / "aur" / "vocalinux" / "PKGBUILD"
PKGBUILD = REPO_ROOT / "packaging" / "aur" / "voculinux" / "PKGBUILD"
# Everything user-facing that could state the floor. Scanned as trees rather than
# a fixed list: docs and the website drifted to "3.9+" for a full release cycle
+1 -1
View File
@@ -1022,7 +1022,7 @@ class TestInitializationEdgeCases(unittest.TestCase):
with patch("os.path.exists") as mock_exists:
def exists_check(path):
if ".vocalinux_preinstalled" in path:
if ".voculinux_preinstalled" in path:
return True
return True
@@ -817,7 +817,7 @@ class TestDownmixToMono(unittest.TestCase):
def setUpClass(cls):
# Other test modules may have left sys.modules["numpy"] as MagicMock.
if isinstance(sys.modules.get("numpy"), MagicMock):
real = getattr(sys, "_vocalinux_real_numpy", None)
real = getattr(sys, "_voculinux_real_numpy", None)
if real is not None:
sys.modules["numpy"] = real
else:
@@ -1092,10 +1092,10 @@ class TestVoskInitialization(unittest.TestCase):
):
with patch(
"vocalinux.speech_recognition.recognition_manager.SYSTEM_MODELS_DIRS",
["/usr/share/vocalinux"],
["/usr/share/voculinux"],
):
with patch.object(
manager, "_get_vosk_model_path", return_value="/usr/share/vocalinux/model"
manager, "_get_vosk_model_path", return_value="/usr/share/voculinux/model"
):
with patch("os.path.exists", return_value=True):
with patch.dict("sys.modules", {"vosk": mock_vosk}):
+3 -3
View File
@@ -18,7 +18,7 @@ from unittest.mock import MagicMock, patch
import pytest
# Mock GI imports before importing any vocalinux modules that use gi.
# Mock GI imports before importing any voculinux modules that use gi.
# On CI, real gi/IBus packages are installed; without mocks, importing
# ibus_engine would connect to a real IBus daemon and hang.
if "gi" not in sys.modules:
@@ -463,7 +463,7 @@ class TestDownloadWhispercppModel:
class TestDownloadWhisperModel:
"""OpenAI Whisper downloads run through the shared streaming helper.
Vocalinux fetches the checkpoint only to give the UI progress and a working
Voculinux fetches the checkpoint only to give the UI progress and a working
cancel; whisper.load_model() is what verifies it. Both were briefly lost when
the downloader was removed altogether, so they are pinned here.
"""
@@ -534,7 +534,7 @@ class TestDownloadVoskModel:
# Extraction has to really happen here — the stamp lands in the tree it
# creates — and other modules leave a MagicMock in sys.modules["zipfile"].
real_zipfile = getattr(sys, "_vocalinux_real_zipfile", None) or __import__("zipfile")
real_zipfile = getattr(sys, "_voculinux_real_zipfile", None) or __import__("zipfile")
with patch.dict("sys.modules", {"requests": mock_requests, "zipfile": real_zipfile}):
with patch(
+2 -2
View File
@@ -167,8 +167,8 @@ class TestGetSystemModelPaths(unittest.TestCase):
mock_open.return_value.read.return_value = mock_os_release
paths = _get_system_model_paths()
self.assertIsInstance(paths, list)
self.assertNotIn("/usr/local/share/vocalinux/models", paths)
self.assertIn("/usr/share/vocalinux/models", paths)
self.assertNotIn("/usr/local/share/voculinux/models", paths)
self.assertIn("/usr/share/voculinux/models", paths)
def test_arch_paths_custom_xdg_dirs(self):
# Regression test: custom XDG_DATA_DIRS + Arch os-release used to
+1 -1
View File
@@ -22,7 +22,7 @@ from unittest.mock import MagicMock, patch # noqa: E402
# load and don't restore it. Reuse the real module cached by conftest instead
# of unloading/re-importing NumPy's compiled extensions.
if isinstance(sys.modules.get("numpy"), MagicMock):
_real_numpy = getattr(sys, "_vocalinux_real_numpy", None)
_real_numpy = getattr(sys, "_voculinux_real_numpy", None)
if _real_numpy is not None:
sys.modules["numpy"] = _real_numpy
+2 -2
View File
@@ -249,7 +249,7 @@ def test_flatpak_release_jobs_reuse_ci_builder_pins():
"action's restore-keys prefix matches after it appends -${arch}"
)
assert f"runs-on: {runner}" in block
assert f"Vocalinux-${{{{ steps.get_version.outputs.VERSION }}}}-{arch}.flatpak" in block
assert f"Voculinux-${{{{ steps.get_version.outputs.VERSION }}}}-{arch}.flatpak" in block
assert "actions/upload-artifact" in block, f"{name} does not upload a workflow artifact"
assert "gh release upload" not in block, f"{name} cannot run gh in the builder image"
assert "python -m build" not in _without_comments(block)
@@ -273,7 +273,7 @@ def test_the_release_notes_document_flatpak_bundles():
assert "org.gnome.Platform//50" in body
assert "no auto-update" in body
assert "not on Flathub" in body
assert "Vocalinux-__VERSION__-x86_64.flatpak" in body
assert "Voculinux-__VERSION__-x86_64.flatpak" in body
def test_the_remote_install_guidance_only_runs_when_the_remote_publishes():
+9 -9
View File
@@ -657,7 +657,7 @@ def _wayland_injector(
}
env.update(extra_env or {})
if forced:
env["VOCALINUX_FORCE_BACKEND"] = forced
env["VOCULINUX_FORCE_BACKEND"] = forced
for key, value in env.items():
if value is None:
monkeypatch.delenv(key, raising=False)
@@ -767,7 +767,7 @@ class TestPortalBackendSelection:
portal=cls,
extra_env={
"WAYLAND_DISPLAY": None,
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"DISPLAY": ":99",
},
)
@@ -872,7 +872,7 @@ class TestPortalInjectionPaths:
with (
patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
),
patch.object(injector, "_wait_for_modifiers_released"),
@@ -899,7 +899,7 @@ class TestPortalInjectionPaths:
with patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
):
assert injector._demote_portal_backend() is False
@@ -921,7 +921,7 @@ class TestPortalInjectionPaths:
with patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
):
assert injector._demote_portal_backend() is True
@@ -947,7 +947,7 @@ class TestPortalInjectionPaths:
with (
patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
),
patch.object(injector, "_wait_for_modifiers_released"),
@@ -970,7 +970,7 @@ class TestPortalInjectionPaths:
with patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
):
assert injector.inject_text("hello world") is False
@@ -989,7 +989,7 @@ class TestPortalInjectionPaths:
with patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
):
assert injector.inject_text("hello world") is False
@@ -1008,7 +1008,7 @@ class TestPortalInjectionPaths:
with patch.dict(
os.environ,
{"FLATPAK_ID": "com.vocalinux.Vocalinux"},
{"FLATPAK_ID": "com.voculinux.Voculinux"},
clear=True,
):
assert injector.inject_text("hello world") is True
+158
View File
@@ -0,0 +1,158 @@
"""Ticket #18: offline naming-sweep — no old-name strings on user-visible surfaces.
The fork takes its own name everywhere a user can see it. Anything still
spelling the old name must be on the allowlist below:
- upstream provenance (``VocaHQ/vocalinux`` URLs),
- the retained internal Python module name (``src/vocalinux`` paths,
``vocalinux.*`` imports/attributes, ``-m vocalinux.main``),
- retained internal class names (``VocalinuxEngine*``, ``VocalinuxDBusService``),
- one-time-legacy import sources (old config/dictionary/bus literals),
- generated or historical files (``uv.lock``, ``requirements/*.txt``,
``CHANGELOG.md``),
- the rest of ``web/`` (owned by ticket #24; only ``page.tsx`` and
``package.json`` belong to this ticket).
"""
import os
import re
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OLD_NAME = re.compile(r"vocalinux", re.IGNORECASE)
SKIP_DIRS = {".git", "__pycache__", ".venv", "venv", "node_modules", ".next", "build", "dist"}
SKIP_SUFFIXES = (".egg-info",)
SKIP_FILES = {"uv.lock", "CHANGELOG.md"}
SKIP_PREFIXES = ("requirements/",)
BINARY_EXTS = (
".wav",
".mp3",
".ogg",
".png",
".svg",
".ico",
".icns",
".bin",
".onnx",
".pt",
".zip",
".tar",
".gz",
".whl",
".AppImage",
".zsync",
".pyc",
)
#: web/ belongs to ticket #24 except the two rename-inventory files.
WEB_OWNED = {"web/src/app/page.tsx", "web/package.json"}
def _allowed(path: str, line: str) -> bool:
"""Return True when an old-name match is an allowlisted surface."""
low = line.lower()
if "vocahq/" in low or "jatinkrmalik/vocalinux" in low:
return True
if "vocalinux" in low and "voculinux" in low:
# Migration-compat shims that name both installs at once.
return True
if "/{name}/vocalinux" in line:
# Upstream repo-name template (retained provenance).
return True
if path == "snap/local/check_runtime.py" and '"vocalinux"' in line:
# Smoke imports resolve the retained import package.
return True
if re.search(r'vocalinux\.["a-z_]', line):
return True
if line.strip().rstrip(",").lstrip("/ ") == '"vocalinux"':
# Retained module-dir path segment element in tests and tooling
# (single-line and os.path.join element forms).
return True
if 'site / "vocalinux"' in line or '"platform/vocalinux"' in line:
# Fixture module trees named after the retained import package.
return True
if '"vocalinux/' in line:
# Fixture module file paths under the retained package name.
return True
if "/vocalinux/" in line or "src/vocalinux" in line:
return True
if "-m vocalinux" in line or "import vocalinux" in line:
return True
if "VocalinuxEngine" in line or "VocalinuxDBusService" in line:
return True
if "cmdline" in line:
return True
if " - vocalinux" in line:
# Log logger names keep the retained module path.
return True
if path in (
"src/vocalinux/legacy_import.py",
"tests/test_legacy_import.py",
):
return True
if "LEGACY" in line or "legacy" in line:
return True
if "~/.config/vocalinux" in line or '".config" / "vocalinux"' in line:
# Documented one-time-import source (ticket #18).
return True
if "from vocalinux" in line:
return True
if '"src"' in line and '"vocalinux"' in line:
# Retained module-dir path segments in tests and tooling.
return True
if path == "tests/test_rename_sweep.py":
return True
if path == "pyproject.toml" and re.search(r'"vocalinux[."\'\]]', line):
return True
if path == "pyproject.toml" and "vocalinux.version" in line:
return True
if 'PKG_NAME="vocalinux"' in line:
# Installer probes the retained import package, not the distribution.
return True
if '== "vocalinux"' in line or '.startswith("vocalinux' in line:
# Retained module identity comparisons in test isolation helpers.
return True
return False
def _iter_files():
for root, dirs, files in os.walk(REPO_ROOT):
dirs[:] = [d for d in sorted(dirs) if d not in SKIP_DIRS]
for name in sorted(files):
if name in SKIP_FILES or name.endswith(BINARY_EXTS):
continue
full = os.path.join(root, name)
rel = os.path.relpath(full, REPO_ROOT)
if rel.startswith(SKIP_PREFIXES):
continue
if rel.startswith("web/") and rel not in WEB_OWNED:
continue
yield rel, full
def test_naming_sweep_no_old_name_outside_allowlist() -> None:
"""Every old-name hit must be an allowlisted retained surface."""
offenders: list[str] = []
for rel, full in _iter_files():
if any(part.endswith(SKIP_SUFFIXES) for part in rel.split("/")):
continue
try:
with open(full, encoding="utf-8") as handle:
text = handle.read()
except (UnicodeDecodeError, ValueError):
continue
for number, line in enumerate(text.splitlines(), 1):
if OLD_NAME.search(line) and not _allowed(rel, line):
offenders.append(f"{rel}:{number}:{line.strip()[:160]}")
assert (
offenders == []
), f"{len(offenders)} old-name string(s) outside the allowlist:\n" + "\n".join(offenders[:60])
def test_owned_web_files_carry_new_name() -> None:
"""The two web rename surfaces name the fork (URLs aside)."""
for rel in sorted(WEB_OWNED):
with open(os.path.join(REPO_ROOT, rel), encoding="utf-8") as handle:
text = handle.read()
assert "Voculinux" in text or "voculinux" in text, rel
+4 -4
View File
@@ -11,13 +11,13 @@ from types import ModuleType
import pytest
# Other legacy tests replace zipfile in sys.modules during collection.
ZIPFILE = getattr(sys, "_vocalinux_real_zipfile", __import__("zipfile"))
ZIPFILE = getattr(sys, "_voculinux_real_zipfile", __import__("zipfile"))
SCRIPT = Path(__file__).resolve().parents[1] / "scripts/build_reproducible_wheel.py"
@pytest.fixture
def gate(monkeypatch: pytest.MonkeyPatch) -> ModuleType:
"""Load the standalone build tool without importing Vocalinux/GTK."""
"""Load the standalone build tool without importing Voculinux/GTK."""
spec = importlib.util.spec_from_file_location("reproducible_wheel", SCRIPT)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
@@ -29,7 +29,7 @@ def gate(monkeypatch: pytest.MonkeyPatch) -> ModuleType:
def wheel(path: Path, content: str = "payload", year: int = 2020) -> Path:
"""Make a tiny real archive, including independently controllable ZIP metadata."""
path.parent.mkdir(parents=True, exist_ok=True)
info = ZIPFILE.ZipInfo("vocalinux/__init__.py", date_time=(year, 1, 1, 0, 0, 0))
info = ZIPFILE.ZipInfo("voculinux/__init__.py", date_time=(year, 1, 1, 0, 0, 0))
with ZIPFILE.ZipFile(path, "w") as archive:
archive.writestr(info, content)
return path
@@ -59,7 +59,7 @@ def test_mismatches_fail_without_normalizing_archives(
gate.compare_wheels(first, second, report)
assert "FAIL" in report.read_text()
if difference == "payload":
assert "member content differs: vocalinux/__init__.py" in report.read_text()
assert "member content differs: voculinux/__init__.py" in report.read_text()
def test_snapshot_uses_working_source_without_ignored_build_state(
+5 -5
View File
@@ -162,10 +162,10 @@ class TestResourceManagerCandidateSelection:
(full_candidate / "sounds").mkdir(parents=True, exist_ok=True)
for icon in [
"vocalinux",
"vocalinux-microphone",
"vocalinux-microphone-off",
"vocalinux-microphone-process",
"voculinux",
"voculinux-microphone",
"voculinux-microphone-off",
"voculinux-microphone-process",
]:
(full_candidate / "icons" / "scalable" / f"{icon}.svg").write_text("x")
@@ -183,7 +183,7 @@ class TestResourceManagerCandidateSelection:
ResourceManager._resources_dir = None
manager = ResourceManager()
missing = Path("/tmp/this-path-should-not-exist-vocalinux-resource-manager")
missing = Path("/tmp/this-path-should-not-exist-voculinux-resource-manager")
candidate_score = getattr(manager, "_candidate_score")
score = candidate_score(missing)
assert score == -1
+16 -16
View File
@@ -1265,7 +1265,7 @@ class TestSettingsNavigation(unittest.TestCase):
"""Finalization keeps the Cancel button usable while recognition stops."""
finalize_body = self.source_code.split("def _finalize_test")[1].split("\n def ")[0]
self.assertIn("def stop_test", finalize_body)
self.assertIn('name="vocalinux-test-dictation-stop"', finalize_body)
self.assertIn('name="voculinux-test-dictation-stop"', finalize_body)
self.assertIn(
"GLib.timeout_add(100, self._wait_for_idle_then_restore_callbacks)",
finalize_body,
@@ -1342,7 +1342,7 @@ class TestAboutPage(unittest.TestCase):
cls.about = cls.about_source.split("def _build_about_section")[1].split("\n def ")[0]
def test_three_groups_keep_updater(self) -> None:
self.assertIn('title="Vocalinux"', self.about)
self.assertIn('title="Voculinux"', self.about)
self.assertIn('title="Part of VocaHQ"', self.about)
self.assertIn('title="Talk to us"', self.about)
self.assertIn('title="Updates"', self.about)
@@ -1369,7 +1369,7 @@ class TestAboutPage(unittest.TestCase):
def test_family_and_talk_urls(self):
for url in (
"https://vocalinux.com",
"https://voculinux.com",
"https://vocahq.com",
"https://vocamac.com",
"https://vocawin.com",
@@ -1395,29 +1395,29 @@ class TestAboutPage(unittest.TestCase):
self.assertIn("_set_accessible_name(button, tooltip)", helper)
self.assertIn('if label == "Open"', helper)
for name in (
"Open vocalinux.com website",
"Open voculinux.com website",
"Open vocahq.com",
"Open vocalinux.com",
"Open voculinux.com",
"Open vocamac.com",
"Open vocawin.com",
"Open vocaphone.vocahq.com",
"Open vocagateway.vocahq.com",
"Open the Vocalinux GitHub repository",
"Open the Voculinux GitHub repository",
"Open the latest release page",
):
self.assertIn(name, self.source)
self.assertIn("_set_accessible_name", self.about)
def test_website_is_product_site_not_only_github(self):
self.assertIn("VOCALINUX_SITE_URL", self.about)
self.assertIn("Product site at vocalinux.com", self.about)
self.assertNotIn("Open the Vocalinux GitHub page", self.about)
self.assertIn("VOCULINUX_SITE_URL", self.about)
self.assertIn("Product site at voculinux.com", self.about)
self.assertNotIn("Open the Voculinux GitHub page", self.about)
def test_source_code_row_sits_beside_website(self):
self.assertIn('label="Website"', self.about)
self.assertIn('label="Source code"', self.about)
self.assertIn("GITHUB_REPO_URL", self.about)
self.assertIn("Open the Vocalinux GitHub repository", self.about)
self.assertIn("Open the Voculinux GitHub repository", self.about)
self.assertLess(self.about.find('label="Website"'), self.about.find('label="Source code"'))
def test_talk_to_us_sits_above_updates(self):
@@ -1457,15 +1457,15 @@ class TestAboutPage(unittest.TestCase):
VOCAHQ_MAILTO_URL,
VOCAHQ_SITE_URL,
VOCAHQ_X_URL,
VOCALINUX_SITE_URL,
VOCAMAC_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAWIN_SITE_URL,
VOCULINUX_SITE_URL,
_can_open_url,
)
for url in (
VOCALINUX_SITE_URL,
VOCULINUX_SITE_URL,
VOCAHQ_SITE_URL,
VOCAMAC_SITE_URL,
VOCAWIN_SITE_URL,
@@ -1542,22 +1542,22 @@ class TestAboutPage(unittest.TestCase):
/ "resources"
/ "icons"
/ "scalable"
/ "vocalinux.svg"
/ "voculinux.svg"
)
shipped = (
Path(__file__).resolve().parents[1]
/ "resources"
/ "icons"
/ "scalable"
/ "vocalinux.svg"
/ "voculinux.svg"
)
packaged_svg = packaged.read_text(encoding="utf-8")
shipped_svg = shipped.read_text(encoding="utf-8")
self.assertEqual(packaged_svg, shipped_svg)
self.assertIn('<circle cx="229" cy="231.5" r="221.5" fill="#0F6B57"/>', packaged_svg)
self.assertIn('aria-label="Vocalinux"', packaged_svg)
self.assertIn('aria-label="Voculinux"', packaged_svg)
self.assertNotIn("<rect", packaged_svg)
self.assertIn('get_icon_path("vocalinux")', self.about)
self.assertIn('get_icon_path("voculinux")', self.about)
def test_platform_marks_are_currentcolor_svgs(self):
from pathlib import Path
+3 -3
View File
@@ -215,7 +215,7 @@ def test_test_dictation_cancel_runs_off_thread_and_restores_callbacks() -> None:
dialog.test_button.set_label.assert_called_with("Cancelling…")
thread_cls.assert_called_once()
assert thread_cls.call_args.kwargs["daemon"] is True
assert thread_cls.call_args.kwargs["name"] == "vocalinux-test-dictation-cancel"
assert thread_cls.call_args.kwargs["name"] == "voculinux-test-dictation-cancel"
with patch.object(settings_dialog, "GLib") as glib:
thread_cls.call_args.kwargs["target"]()
@@ -443,7 +443,7 @@ def test_finalize_test_buffered_reload_waits_for_idle_before_restore() -> None:
dialog.speech_engine.stop_recognition.assert_not_called()
thread_cls.assert_called_once()
assert thread_cls.call_args.kwargs["daemon"] is True
assert thread_cls.call_args.kwargs["name"] == "vocalinux-test-dictation-stop"
assert thread_cls.call_args.kwargs["name"] == "voculinux-test-dictation-stop"
thread_cls.call_args.kwargs["target"]()
dialog.speech_engine.stop_recognition.assert_called_once()
# Must not restore live injector yet — worker still holds the utterance.
@@ -518,7 +518,7 @@ def test_cancel_during_processing_restores_once_without_result_check() -> None:
):
SettingsDialog._finalize_test(dialog)
assert thread_cls.call_args.kwargs["name"] == "vocalinux-test-dictation-stop"
assert thread_cls.call_args.kwargs["name"] == "voculinux-test-dictation-stop"
dialog._test_cancelled = True
assert SettingsDialog._finish_test_after_user_cancel(dialog) is False
assert dialog.test_buffer._text == "(Test cancelled)"
+1 -1
View File
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch # noqa: E402
# load and don't restore it. Reuse the real module cached by conftest instead
# of unloading/re-importing NumPy's compiled extensions.
if isinstance(sys.modules.get("numpy"), MagicMock):
_real_numpy = getattr(sys, "_vocalinux_real_numpy", None)
_real_numpy = getattr(sys, "_voculinux_real_numpy", None)
if _real_numpy is not None:
sys.modules["numpy"] = _real_numpy
+1 -1
View File
@@ -183,7 +183,7 @@ class TestLockFileConstants(unittest.TestCase):
"""Tests for lock file constants."""
def test_lock_file_path_is_in_data_home(self):
"""Test that lock file lives under $XDG_DATA_HOME/vocalinux/."""
"""Test that lock file lives under $XDG_DATA_HOME/voculinux/."""
expected_dir = Path(data_dir())
self.assertEqual(single_instance_module.LOCK_FILE_DIR, expected_dir)
self.assertEqual(single_instance_module.LOCK_FILE_PATH, expected_dir / "instance.lock")
+19 -19
View File
@@ -116,10 +116,10 @@ def test_record_audits_payload_and_preserves_export_digests(tmp_path: Path) -> N
(source / "src/vocalinux/version.py").write_text('__version__ = "0.18.5"\n')
root = tmp_path / "install"
distribution(root, "demo", "1.0")
distribution(root, "vocalinux", "0.18.5")
distribution(root, "voculinux", "0.18.5")
CHECK.record(root, source)
manifest = json.loads((root / CHECK.MANIFEST).read_text())
assert manifest["packages"] == {"demo": "1.0", "vocalinux": "0.18.5"}
assert manifest["packages"] == {"demo": "1.0", "voculinux": "0.18.5"}
assert manifest["python"] == list(sys.version_info[:2])
for name, digest in manifest["exports"].items():
assert digest == hashlib.sha256((source / "requirements" / name).read_bytes()).hexdigest()
@@ -154,12 +154,12 @@ def test_launchers_are_relocatable_and_keep_locked_packages_first(
header = (
f"#!/bin/sh\n'''exec' {python} \"$0\" \"$@\"\n' '''\n" if long_shebang else f"#!{python}\n"
)
for name in ("vocalinux", "vocalinux-gui", "pwcpp"):
for name in ("voculinux", "voculinux-gui", "pwcpp"):
path = binary / name
path.write_text(header + "print('entry point')\n")
path.chmod(0o755)
(binary / "python3").symlink_to(sys.executable)
site = distribution(install, "vocalinux", "0.18.5")
site = distribution(install, "voculinux", "0.18.5")
package = site / "vocalinux"
package.mkdir()
(package / "__init__.py").write_text("")
@@ -169,13 +169,13 @@ def test_launchers_are_relocatable_and_keep_locked_packages_first(
platform.mkdir(parents=True)
(platform / "__init__.py").write_text("raise RuntimeError('wrong runtime')\n")
BUILD.relocate_scripts(install, tooling)
for name in ("vocalinux", "vocalinux-gui", "pwcpp"):
for name in ("voculinux", "voculinux-gui", "pwcpp"):
text = (binary / name).read_text()
assert text.startswith("#!/usr/bin/env python3\n")
assert str(tooling) not in text
assert os.access(binary / name, os.X_OK)
result = subprocess.run(
[str(binary / "vocalinux")],
[str(binary / "voculinux")],
env={
**os.environ,
"PATH": f"{binary}{os.pathsep}{os.environ['PATH']}",
@@ -211,7 +211,7 @@ def test_build_orchestration_uses_hashes_and_separates_tooling(
def run(argv: list[str], cwd: Path, env: dict[str, str]) -> None:
calls.append((argv, cwd, env.copy()))
if str(source) == argv[-1]:
for name in ("vocalinux", "vocalinux-gui"):
for name in ("voculinux", "voculinux-gui"):
(install / "bin" / name).write_text(f"#!{argv[0]}\n")
monkeypatch.setattr(BUILD.venv.EnvBuilder, "create", create)
@@ -309,7 +309,7 @@ def test_runtime_pip_flags_reject_corrupt_artifact_offline(tmp_path: Path) -> No
environment = tmp_path / "tooling"
venv.EnvBuilder(with_pip=True).create(environment)
wheel = tmp_path / "demo-1.0-py3-none-any.whl"
real_zipfile = getattr(sys, "_vocalinux_real_zipfile", zipfile)
real_zipfile = getattr(sys, "_voculinux_real_zipfile", zipfile)
with real_zipfile.ZipFile(wheel, "w") as archive:
archive.writestr("demo.py", "VALUE = 1\n")
archive.writestr(
@@ -369,7 +369,7 @@ def test_full_python_payload_build_is_offline_and_keeps_project_metadata(tmp_pat
(source / "src/vocalinux/version.py").write_text('__version__ = "0.18.5"\n')
wheelhouse = tmp_path / "wheels"
wheelhouse.mkdir()
real_zipfile = getattr(sys, "_vocalinux_real_zipfile", zipfile)
real_zipfile = getattr(sys, "_voculinux_real_zipfile", zipfile)
def wheel(name: str, version: str, files: dict[str, str]) -> Path:
path = wheelhouse / f"{name}-{version}-py3-none-any.whl"
@@ -399,13 +399,13 @@ def test_full_python_payload_build_is_offline_and_keeps_project_metadata(tmp_pat
)
runtime = wheel("demo", "1.0", {"demo.py": "VALUE = 1\n"})
app = wheel(
"vocalinux",
"voculinux",
"0.18.5",
{
"vocalinux/__init__.py": "",
"vocalinux/main.py": "def main():\n print('fixture app')\n",
"vocalinux-0.18.5.dist-info/entry_points.txt": "[console_scripts]\nvocalinux = vocalinux.main:main\n"
"[gui_scripts]\nvocalinux-gui = vocalinux.main:main\n",
"voculinux-0.18.5.dist-info/entry_points.txt": "[console_scripts]\nvoculinux = vocalinux.main:main\n"
"[gui_scripts]\nvoculinux-gui = vocalinux.main:main\n",
},
)
for filename, name, version, artifact in (
@@ -421,7 +421,7 @@ def test_full_python_payload_build_is_offline_and_keeps_project_metadata(tmp_pat
metadata = source / "pyproject.toml"
metadata.write_text(
'[build-system]\nrequires = []\nbuild-backend = "backend"\nbackend-path = ["."]\n'
'[project]\nname = "vocalinux"\nversion = "0.18.5"\ndependencies = ["PyGObject"]\n'
'[project]\nname = "voculinux"\nversion = "0.18.5"\ndependencies = ["PyGObject"]\n'
)
(source / "backend.py").write_text(
"import shutil\nfrom pathlib import Path\n"
@@ -434,11 +434,11 @@ def test_full_python_payload_build_is_offline_and_keeps_project_metadata(tmp_pat
install = tmp_path / "install"
BUILD.build(source, install, tmp_path / "work")
assert metadata.read_bytes() == before
CHECK.audit(install, {"demo": "1.0", "vocalinux": "0.18.5"})
CHECK.audit(install, {"demo": "1.0", "voculinux": "0.18.5"})
assert not list((tmp_path / "work").iterdir())
assert not (install / "bin/pip").exists()
result = subprocess.run(
[str(install / "bin/python3"), str(install / "bin/vocalinux")],
[str(install / "bin/python3"), str(install / "bin/voculinux")],
capture_output=True,
text=True,
timeout=5,
@@ -464,7 +464,7 @@ def test_artifact_smoke_refuses_user_install_and_checks_the_supplied_file(
"with open(os.environ['SNAP_TEST_LOG'], 'a') as stream:\n"
" stream.write(json.dumps([Path(sys.argv[0]).name, sys.argv[1:], "
"os.environ.get('PYNPUT_BACKEND')]) + '\\n')\n"
"if sys.argv[1:3] == ['list', 'vocalinux']:\n"
"if sys.argv[1:3] == ['list', 'voculinux']:\n"
" sys.exit(0 if os.environ['SNAP_TEST_EXISTING'] == '1' else 1)\n"
)
path.chmod(0o755)
@@ -490,8 +490,8 @@ def test_artifact_smoke_refuses_user_install_and_checks_the_supplied_file(
else:
assert result.returncode == 0, result.stderr
assert calls[1][:2] == ["sudo", ["snap", "install", "--dangerous", str(artifact)]]
assert calls[2][:2] == ["snap", ["run", "vocalinux", "--version"]]
assert calls[3][1][:4] == ["run", "--shell", "vocalinux", "-c"]
assert calls[2][:2] == ["snap", ["run", "voculinux", "--version"]]
assert calls[3][1][:4] == ["run", "--shell", "voculinux", "-c"]
assert '"$SNAP/snap/command-chain/desktop-launch"' in calls[3][1][4]
assert '"$SNAP/usr/lib/vocalinux/check_runtime.py"' in calls[3][1][4]
assert '"$SNAP/usr/lib/voculinux/check_runtime.py"' in calls[3][1][4]
assert calls[3][2] == "dummy"
+18 -18
View File
@@ -6,35 +6,35 @@ import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
SNAPCRAFT_YAML = REPO_ROOT / "snap" / "snapcraft.yaml"
DESKTOP_FILE = REPO_ROOT / "snap" / "gui" / "vocalinux.desktop"
SNAP_PNG = REPO_ROOT / "snap" / "gui" / "vocalinux.png"
DESKTOP_FILE = REPO_ROOT / "snap" / "gui" / "voculinux.desktop"
SNAP_PNG = REPO_ROOT / "snap" / "gui" / "voculinux.png"
def test_snapcraft_recipe_and_gui_assets() -> None:
assert SNAPCRAFT_YAML.is_file()
doc = yaml.safe_load(SNAPCRAFT_YAML.read_text(encoding="utf-8"))
assert doc["name"] == "vocalinux"
assert doc["name"] == "voculinux"
assert doc["base"] == "core24"
assert doc["summary"] == "Free offline voice dictation for Linux"
assert doc["license"] == "AGPL-3.0-only"
assert doc["icon"] == "snap/gui/vocalinux.png"
assert doc["website"] == "https://vocalinux.com"
assert doc["icon"] == "snap/gui/voculinux.png"
assert doc["website"] == "https://voculinux.com"
assert doc["confinement"] == "strict"
assert doc["grade"] == "stable"
plugs = set((doc.get("apps") or {}).get("vocalinux", {}).get("plugs") or [])
plugs = set((doc.get("apps") or {}).get("voculinux", {}).get("plugs") or [])
assert "raw-input" in plugs
assert "hardware-observe" in plugs
assert "audio-record" in plugs
# The Store denied the super-privileged uinput grant; the plug (and the
# uinput-only ydotool) must stay out or every revision is held in review.
assert "uinput" not in plugs
stage = doc["parts"]["vocalinux"].get("stage-packages") or []
stage = doc["parts"]["voculinux"].get("stage-packages") or []
assert "ydotool" not in stage
recipe_text = SNAPCRAFT_YAML.read_text(encoding="utf-8")
assert "sudo snap connect vocalinux:raw-input" in recipe_text
assert "sudo snap connect vocalinux:hardware-observe" in recipe_text
assert "sudo snap connect voculinux:raw-input" in recipe_text
assert "sudo snap connect voculinux:hardware-observe" in recipe_text
assert DESKTOP_FILE.is_file()
assert SNAP_PNG.is_file()
@@ -50,7 +50,7 @@ def test_snap_puts_gnome_platform_first_on_ld_library_path() -> None:
come first or About-page SVGs fail with "Image type svg is not supported".
"""
doc = yaml.safe_load(SNAPCRAFT_YAML.read_text(encoding="utf-8"))
env = (doc.get("apps") or {}).get("vocalinux", {}).get("environment") or {}
env = (doc.get("apps") or {}).get("voculinux", {}).get("environment") or {}
ld_path = env.get("LD_LIBRARY_PATH")
assert isinstance(ld_path, str)
assert ld_path.startswith("$SNAP/gnome-platform/usr/lib/$CRAFT_ARCH_TRIPLET:")
@@ -63,8 +63,8 @@ def test_snap_docs_tell_users_to_connect_hardware_observe() -> None:
install = (REPO_ROOT / "docs" / "INSTALL.md").read_text(encoding="utf-8")
snapcraft = SNAPCRAFT_YAML.read_text(encoding="utf-8")
for text in (readme, install, snapcraft):
assert "sudo snap connect vocalinux:raw-input" in text
assert "sudo snap connect vocalinux:hardware-observe" in text
assert "sudo snap connect voculinux:raw-input" in text
assert "sudo snap connect voculinux:hardware-observe" in text
def test_snap_docs_explain_typing_without_uinput_plug() -> None:
@@ -88,13 +88,13 @@ def test_snap_preserves_metadata_and_uses_gnome_gi() -> None:
"""GI comes from GNOME without rewriting project dependencies."""
text = SNAPCRAFT_YAML.read_text(encoding="utf-8")
doc = yaml.safe_load(text)
part = doc["parts"]["vocalinux"]
part = doc["parts"]["voculinux"]
assert part["plugin"] == "nil"
assert "python-packages" not in part
assert "sed -i" not in part["override-pull"]
assert "snap/local/build.py" in part["override-build"]
assert "craftctl default" not in part["override-build"]
stage = doc["parts"]["vocalinux"].get("stage-packages") or []
stage = doc["parts"]["voculinux"].get("stage-packages") or []
assert "python3-gi" not in stage
assert "python3-gi-cairo" not in stage
@@ -143,7 +143,7 @@ def test_release_notes_document_snap_sideload() -> None:
assert "snap install --dangerous" in body
assert "RemoteDesktop portal" in body
assert "no `uinput` plug" in body
assert "vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap" in body
assert "voculinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap" in body
def test_snap_backfill_workflow_attests_only_the_snap() -> None:
@@ -179,11 +179,11 @@ def test_snap_promote_dispatch_gates_stable_on_candidate() -> None:
"""stable is the gated option of #783: a manual dispatch releasing the candidate revision."""
text = (REPO_ROOT / ".github" / "workflows" / "snap-promote.yml").read_text(encoding="utf-8")
assert "workflow_dispatch:" in text
assert 'snapcraft release vocalinux "$REVISION" latest/stable' in text
assert 'snapcraft release voculinux "$REVISION" latest/stable' in text
assert "SNAPCRAFT_STORE_CREDENTIALS is unset; cannot promote the snap" in text
assert "environment:" in text
# The promote must prove the candidate revision carries the tag's version.
assert "snapcraft status vocalinux" in text
assert "snapcraft status voculinux" in text
assert 'if [ "$seen_version" != "$VERSION" ]' in text
@@ -222,7 +222,7 @@ def test_snap_builds_pywhispercpp_at_the_portable_baseline() -> None:
alone leaves them all on."""
doc = yaml.safe_load(SNAPCRAFT_YAML.read_text(encoding="utf-8"))
env: dict = {}
for entry in doc["parts"]["vocalinux"].get("build-environment") or []:
for entry in doc["parts"]["voculinux"].get("build-environment") or []:
env.update(entry)
build = (REPO_ROOT / "snap/local/build.py").read_text()
assert '"--no-binary=pywhispercpp"' in build
+1 -1
View File
@@ -32,7 +32,7 @@ def _numpy_is_real_while_capturing() -> Iterator[None]:
stalled read this file depends on.
"""
current = sys.modules.get("numpy")
real = getattr(sys, "_vocalinux_real_numpy", None)
real = getattr(sys, "_voculinux_real_numpy", None)
if isinstance(current, MagicMock) and real is not None:
sys.modules["numpy"] = real
try:
+1 -1
View File
@@ -31,7 +31,7 @@ def audio(monkeypatch: pytest.MonkeyPatch) -> Mock:
audio.open.return_value.get_read_available.return_value = 1024
audio.open.return_value.read.return_value = b"\x00\x01" * 1024
monkeypatch.setitem(sys.modules, "pyaudio", SimpleNamespace(PyAudio=lambda: audio, paInt16=8))
monkeypatch.setitem(sys.modules, "numpy", sys._vocalinux_real_numpy)
monkeypatch.setitem(sys.modules, "numpy", sys._voculinux_real_numpy)
return audio
+18 -18
View File
@@ -197,7 +197,7 @@ class TestTextInjector(unittest.TestCase):
def test_wayland_wtype_fail_xdotool_when_uinput_blocked(self) -> None:
"""ydotool in PATH without /dev/uinput still cannot rescue a wtype miss."""
with patch.dict(
"os.environ", {"XDG_SESSION_TYPE": "wayland", "SNAP": "/snap/vocalinux/x1"}
"os.environ", {"XDG_SESSION_TYPE": "wayland", "SNAP": "/snap/voculinux/x1"}
):
self.mock_which.side_effect = lambda cmd: {
"wtype": "/usr/bin/wtype",
@@ -298,7 +298,7 @@ class TestTextInjector(unittest.TestCase):
injector.inject_text("Hello world")
# Clipboard paste fails (no wl-copy/xclip/xsel in this mock); type fallback.
# Default key-delay is 2 (overridable via VOCALINUX_YDOTOOL_KEY_DELAY).
# Default key-delay is 2 (overridable via VOCULINUX_YDOTOOL_KEY_DELAY).
self.mock_subprocess.assert_any_call(
["ydotool", "type", "--key-delay", "2", "--", "Hello world"],
check=True,
@@ -891,7 +891,7 @@ class TestTextInjector(unittest.TestCase):
"""Flatpak pins ydotool 1.0.4; always use keycode:value form."""
mock_which.return_value = "/app/bin/ydotool"
injector = TextInjector.__new__(TextInjector)
with patch.dict("os.environ", {"FLATPAK_ID": "com.vocalinux.Vocalinux"}):
with patch.dict("os.environ", {"FLATPAK_ID": "com.voculinux.Voculinux"}):
cmd = injector._ydotool_ctrl_v_command()
self.assertEqual(cmd, ["ydotool", "key", "29:1", "47:1", "47:0", "29:0"])
@@ -3516,7 +3516,7 @@ class TestBackendPreference(unittest.TestCase):
def test_environment_overrides_config(self):
"""A one-off experiment must not require editing the user's config."""
with _fake_config({"text_injection": {"backend": "ibus"}}):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "wtype"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "wtype"}):
self.assertEqual(TextInjector._backend_preference(), "wtype")
def test_config_used_when_environment_unset(self):
@@ -3530,14 +3530,14 @@ class TestBackendPreference(unittest.TestCase):
self.assertEqual(TextInjector._backend_preference(), "auto")
def test_explicit_auto_overrides_a_saved_pin(self):
"""``VOCALINUX_FORCE_BACKEND=auto`` asks for autodetection *this run*.
"""``VOCULINUX_FORCE_BACKEND=auto`` asks for autodetection *this run*.
It must not be read as "nothing was set" and fall through to the saved
pin, or the variable cannot undo a pin for a single run -- which is the
one-off A/B test it exists for.
"""
with _fake_config({"text_injection": {"backend": "wtype"}}):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "auto"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "auto"}):
self.assertEqual(TextInjector._backend_preference(), "auto")
def test_unset_environment_uses_the_saved_pin(self):
@@ -3554,7 +3554,7 @@ class TestBackendPreference(unittest.TestCase):
``test_explicit_auto_overrides_a_saved_pin``, where they did ask.
"""
with _fake_config({"text_injection": {"backend": "wtype"}}):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "wtpye"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "wtpye"}):
self.assertEqual(TextInjector._backend_preference(), "wtype")
@@ -3562,7 +3562,7 @@ class TestSelectableBackends(unittest.TestCase):
"""xdotool is a pinnable value, and both readers accept the same set."""
def test_xdotool_is_accepted_from_the_environment(self):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "xdotool"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "xdotool"}):
self.assertEqual(TextInjector._forced_backend_setting(), "xdotool")
def test_xdotool_is_accepted_from_config(self):
@@ -3572,7 +3572,7 @@ class TestSelectableBackends(unittest.TestCase):
def test_both_readers_accept_the_same_values(self):
"""A value valid in one source must be valid in the other."""
for value in TextInjector._SELECTABLE_BACKENDS:
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": value}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": value}):
self.assertEqual(TextInjector._forced_backend_setting(), value)
with _fake_config({"text_injection": {"backend": value}}):
self.assertEqual(TextInjector._configured_backend(), value)
@@ -3586,7 +3586,7 @@ class TestSelectableBackends(unittest.TestCase):
class TestForcedBackendSetting(unittest.TestCase):
"""The raw three-state reading of VOCALINUX_FORCE_BACKEND.
"""The raw three-state reading of VOCULINUX_FORCE_BACKEND.
``_forced_backend()`` collapses unset and explicit ``auto`` together, which
is fine for its own callers but loses the distinction ``_backend_preference()``
@@ -3599,40 +3599,40 @@ class TestForcedBackendSetting(unittest.TestCase):
def test_explicit_auto_is_the_string_not_none(self):
for value in ("auto", " AUTO "):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": value}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": value}):
self.assertEqual(TextInjector._forced_backend_setting(), "auto")
def test_empty_value_counts_as_unset(self):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": " "}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": " "}):
self.assertIsNone(TextInjector._forced_backend_setting())
def test_recognised_backends_are_returned(self):
for value in ("ibus", "wtype", "ydotool", "xdotool"):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": value.upper()}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": value.upper()}):
self.assertEqual(TextInjector._forced_backend_setting(), value)
def test_unknown_value_is_unset_rather_than_auto(self):
"""See ``test_typo_in_environment_is_treated_as_unset_not_as_auto``."""
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "ibsu"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "ibsu"}):
self.assertIsNone(TextInjector._forced_backend_setting())
class TestForcedBackend(unittest.TestCase):
"""VOCALINUX_FORCE_BACKEND overrides backend autodetection."""
"""VOCULINUX_FORCE_BACKEND overrides backend autodetection."""
def test_unset_or_auto_means_auto(self):
for value in ("", "auto", " AUTO "):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": value}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": value}):
self.assertEqual(TextInjector._forced_backend(), "auto")
def test_recognised_backends(self):
for value in ("ibus", "wtype", "ydotool", "xdotool"):
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": value.upper()}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": value.upper()}):
self.assertEqual(TextInjector._forced_backend(), value)
def test_unknown_value_falls_back_to_auto(self):
"""A typo must not silently pin the wrong backend."""
with patch.dict("os.environ", {"VOCALINUX_FORCE_BACKEND": "ibsu"}):
with patch.dict("os.environ", {"VOCULINUX_FORCE_BACKEND": "ibsu"}):
self.assertEqual(TextInjector._forced_backend(), "auto")
def test_missing_variable_means_auto(self):
+22 -22
View File
@@ -111,7 +111,7 @@ class TestDetectEnvironment(unittest.TestCase):
obj = _make_injector(DesktopEnvironment.X11)
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"DISPLAY": ":0",
}
@@ -126,7 +126,7 @@ class TestDetectEnvironment(unittest.TestCase):
obj = _make_injector(DesktopEnvironment.X11)
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"DISPLAY": ":0",
}
@@ -140,7 +140,7 @@ class TestDetectEnvironment(unittest.TestCase):
obj = _make_injector(DesktopEnvironment.WAYLAND)
env = {
"FLATPAK_ID": "com.vocalinux.Vocalinux",
"FLATPAK_ID": "com.voculinux.Voculinux",
"XDG_SESSION_TYPE": "wayland",
"WAYLAND_DISPLAY": "wayland-0",
"DISPLAY": ":0",
@@ -346,7 +346,7 @@ class TestCheckDependencies(unittest.TestCase):
mock_ibus_class.assert_called_once()
def test_force_backend_wtype_skips_ibus(self):
"""VOCALINUX_FORCE_BACKEND=wtype pins wtype even where IBus would be chosen."""
"""VOCULINUX_FORCE_BACKEND=wtype pins wtype even where IBus would be chosen."""
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
@@ -354,7 +354,7 @@ class TestCheckDependencies(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "VOCALINUX_FORCE_BACKEND": "wtype"},
{"XDG_SESSION_TYPE": "wayland", "VOCULINUX_FORCE_BACKEND": "wtype"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -450,7 +450,7 @@ class TestCheckDependencies(unittest.TestCase):
run(pinned).assert_not_called()
def test_explicit_auto_environment_ignores_the_saved_config_pin(self):
"""VOCALINUX_FORCE_BACKEND=auto is how you get one run of plain autodetection.
"""VOCULINUX_FORCE_BACKEND=auto is how you get one run of plain autodetection.
The resolver already knows an explicit auto from an unset variable; this
drives the whole of _check_dependencies to show the saved pin really is
@@ -467,7 +467,7 @@ class TestCheckDependencies(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "VOCALINUX_FORCE_BACKEND": "auto"},
{"XDG_SESSION_TYPE": "wayland", "VOCULINUX_FORCE_BACKEND": "auto"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.config_dir", return_value="/fake/config"),
@@ -512,7 +512,7 @@ class TestCheckDependencies(unittest.TestCase):
"XDG_CURRENT_DESKTOP": desktop,
}
if not config_pin:
environ["VOCALINUX_FORCE_BACKEND"] = "ibus"
environ["VOCULINUX_FORCE_BACKEND"] = "ibus"
with (
patch.dict(
@@ -628,7 +628,7 @@ class TestCheckDependencies(unittest.TestCase):
"XDG_CURRENT_DESKTOP": "Hyprland",
}
if pin:
environ["VOCALINUX_FORCE_BACKEND"] = pin
environ["VOCULINUX_FORCE_BACKEND"] = pin
with (
patch.dict(os.environ, environ, clear=True),
patch(
@@ -716,7 +716,7 @@ class TestCheckDependencies(unittest.TestCase):
self.assertFalse(self._not_applied(output))
def test_pinned_log_line_names_the_config_source_not_the_variable(self):
"""A config-sourced pin must not be reported as VOCALINUX_FORCE_BACKEND."""
"""A config-sourced pin must not be reported as VOCULINUX_FORCE_BACKEND."""
from vocalinux.text_injection.text_injector import DesktopEnvironment, TextInjector
obj = _make_injector(DesktopEnvironment.WAYLAND)
@@ -736,7 +736,7 @@ class TestCheckDependencies(unittest.TestCase):
pinned_lines = [line for line in logs.output if "wtype" in line and "=" in line]
self.assertTrue(pinned_lines)
self.assertFalse(
[line for line in pinned_lines if "VOCALINUX_FORCE_BACKEND" in line],
[line for line in pinned_lines if "VOCULINUX_FORCE_BACKEND" in line],
f"config-sourced pin reported as the env var: {pinned_lines}",
)
@@ -764,7 +764,7 @@ class TestCheckDependencies(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "VOCALINUX_FORCE_BACKEND": "ibus"},
{"XDG_SESSION_TYPE": "wayland", "VOCULINUX_FORCE_BACKEND": "ibus"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -864,7 +864,7 @@ class TestCheckDependencies(unittest.TestCase):
)
def test_force_backend_ydotool_skips_ibus_and_wtype(self):
"""VOCALINUX_FORCE_BACKEND=ydotool pins ydotool even when wtype is available."""
"""VOCULINUX_FORCE_BACKEND=ydotool pins ydotool even when wtype is available."""
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
@@ -872,7 +872,7 @@ class TestCheckDependencies(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "VOCALINUX_FORCE_BACKEND": "ydotool"},
{"XDG_SESSION_TYPE": "wayland", "VOCULINUX_FORCE_BACKEND": "ydotool"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -890,7 +890,7 @@ class TestCheckDependencies(unittest.TestCase):
mock_ibus_class.assert_not_called()
def test_force_backend_ibus_bypasses_reachability_guards(self):
"""VOCALINUX_FORCE_BACKEND=ibus selects IBus even on an unbridged compositor."""
"""VOCULINUX_FORCE_BACKEND=ibus selects IBus even on an unbridged compositor."""
from vocalinux.text_injection.text_injector import DesktopEnvironment, TextInjector
obj = _make_injector(DesktopEnvironment.WAYLAND)
@@ -901,7 +901,7 @@ class TestCheckDependencies(unittest.TestCase):
{
"XDG_SESSION_TYPE": "wayland",
"XDG_CURRENT_DESKTOP": "Hyprland",
"VOCALINUX_FORCE_BACKEND": "ibus",
"VOCULINUX_FORCE_BACKEND": "ibus",
},
clear=True,
),
@@ -1596,7 +1596,7 @@ class TestPressBackspace(unittest.TestCase):
def test_ydotool_v1_uses_keycode_14(self):
obj = _make_backspace_injector("WAYLAND", tool="ydotool", legacy=False)
with patch.dict(os.environ, {"VOCALINUX_YDOTOOL_KEY_DELAY": "2"}):
with patch.dict(os.environ, {"VOCULINUX_YDOTOOL_KEY_DELAY": "2"}):
with patch("subprocess.run") as mock_run:
self.assertTrue(obj.press_backspace(2))
self.assertEqual(
@@ -1788,7 +1788,7 @@ class TestBackgroundIBusInitialization(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "x11", "VOCALINUX_FORCE_BACKEND": "ibus"},
{"XDG_SESSION_TYPE": "x11", "VOCULINUX_FORCE_BACKEND": "ibus"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -1832,7 +1832,7 @@ class TestBackgroundIBusInitialization(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "VOCALINUX_FORCE_BACKEND": "ibus"},
{"XDG_SESSION_TYPE": "wayland", "VOCULINUX_FORCE_BACKEND": "ibus"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -1872,7 +1872,7 @@ class TestBackgroundIBusInitialization(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "x11", "VOCALINUX_FORCE_BACKEND": "ibus"},
{"XDG_SESSION_TYPE": "x11", "VOCULINUX_FORCE_BACKEND": "ibus"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -1905,7 +1905,7 @@ class TestBackgroundIBusInitialization(unittest.TestCase):
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "x11", "VOCALINUX_FORCE_BACKEND": "ibus"},
{"XDG_SESSION_TYPE": "x11", "VOCULINUX_FORCE_BACKEND": "ibus"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
@@ -1939,7 +1939,7 @@ class TestBackgroundIBusInitialization(unittest.TestCase):
obj._ibus_injector = MagicMock()
obj.wayland_tool = "ydotool"
obj._ibus_injector.prepare_engine.side_effect = RuntimeError("late failure")
obj._backend_pin = ("ibus", "VOCALINUX_FORCE_BACKEND")
obj._backend_pin = ("ibus", "VOCULINUX_FORCE_BACKEND")
with self.assertLogs("vocalinux.text_injection.text_injector", level="DEBUG") as logs:
obj._warn_if_pin_not_honoured(*obj._backend_pin)
+17 -17
View File
@@ -120,10 +120,10 @@ class TestTrayIndicator(unittest.TestCase):
self.patcher_listdir = patch(
"os.listdir",
return_value=[
"vocalinux.svg",
"vocalinux-microphone-off.svg",
"vocalinux-microphone.svg",
"vocalinux-microphone-process.svg",
"voculinux.svg",
"voculinux-microphone-off.svg",
"voculinux-microphone.svg",
"voculinux-microphone-process.svg",
],
)
self.mock_listdir = self.patcher_listdir.start()
@@ -350,7 +350,7 @@ class TestTrayIndicator(unittest.TestCase):
self.tray_indicator._cancel_menu_item.set_sensitive.assert_called_with(False)
self.tray_indicator._cancel_menu_item.set_visible.assert_called_with(False)
self.tray_indicator.indicator.set_title.assert_called_with("Vocalinux")
self.tray_indicator.indicator.set_title.assert_called_with("Voculinux")
def test_cancel_tooltip_uses_fallback_without_shortcut_name(self) -> None:
self.tray_indicator.indicator = MagicMock()
@@ -728,7 +728,7 @@ class TestTrayIndicator(unittest.TestCase):
with patch.object(_resource_manager, "validate_resources") as mock_validate:
mock_validate.return_value = {
"resources_dir_exists": True,
"missing_icons": ["vocalinux.svg"],
"missing_icons": ["voculinux.svg"],
"missing_sounds": [],
}
self.tray_indicator._validate_resources()
@@ -1220,7 +1220,7 @@ class TestTrayIndicator(unittest.TestCase):
)
def test_overlay_close_request_quits_app(self):
"""A dock/taskbar close of the overlay window quits Vocalinux (#1028)."""
"""A dock/taskbar close of the overlay window quits Voculinux (#1028)."""
self.assertEqual(self.tray_indicator.overlay._on_close_request, self.tray_indicator._quit)
def test_quit_destroys_overlay(self):
@@ -1355,7 +1355,7 @@ class TestTrayIndicatorFlatpakIcons(unittest.TestCase):
Inside a Flatpak the StatusNotifier host runs outside the sandbox and resolves
icons from the exported ``<app-id>-*`` theme, so the indicator must reference
those names rather than the bundled ``vocalinux-*`` names.
those names rather than the bundled ``voculinux-*`` names.
"""
def setUp(self):
@@ -1369,18 +1369,18 @@ class TestTrayIndicatorFlatpakIcons(unittest.TestCase):
with patch.object(tray, "FLATPAK_ID", None):
names = tray._themed_icon_names()
self.assertEqual(names["default"], "vocalinux-microphone-off")
self.assertEqual(names["active"], "vocalinux-microphone")
self.assertEqual(names["processing"], "vocalinux-microphone-process")
self.assertEqual(names["default"], "voculinux-microphone-off")
self.assertEqual(names["active"], "voculinux-microphone")
self.assertEqual(names["processing"], "voculinux-microphone-process")
def test_themed_icon_names_inside_flatpak(self):
import vocalinux.ui.tray_indicator as tray
with patch.object(tray, "FLATPAK_ID", "com.vocalinux.Vocalinux"):
with patch.object(tray, "FLATPAK_ID", "com.voculinux.Voculinux"):
names = tray._themed_icon_names()
self.assertEqual(names["default"], "com.vocalinux.Vocalinux-microphone-off")
self.assertEqual(names["active"], "com.vocalinux.Vocalinux-microphone")
self.assertEqual(names["processing"], "com.vocalinux.Vocalinux-microphone-process")
self.assertEqual(names["default"], "com.voculinux.Voculinux-microphone-off")
self.assertEqual(names["active"], "com.voculinux.Voculinux-microphone")
self.assertEqual(names["processing"], "com.voculinux.Voculinux-microphone-process")
class TestAppIndicatorImportFallback(unittest.TestCase):
@@ -1447,7 +1447,7 @@ class TestAppIndicatorImportFallback(unittest.TestCase):
AyatanaAppIndicator3=sentinel,
)
with patch.dict(os.environ, {"APPDIR": "/tmp/.mount_vocalinux"}):
with patch.dict(os.environ, {"APPDIR": "/tmp/.mount_voculinux"}):
import vocalinux.ui.tray_indicator as tray
self.assertIs(tray.AppIndicator, sentinel)
@@ -1519,7 +1519,7 @@ class TestIndicatorBackendWiring(TestTrayIndicator):
patch(
"vocalinux.ui.tray_indicator.AppIndicator.Indicator.new", return_value=raw
) as new,
patch("vocalinux.ui.tray_indicator.FLATPAK_ID", "com.vocalinux.Vocalinux"),
patch("vocalinux.ui.tray_indicator.FLATPAK_ID", "com.voculinux.Voculinux"),
):
self.tray_indicator._init_indicator()
+1 -1
View File
@@ -669,7 +669,7 @@ class TestExternalTriggerHandlers(unittest.TestCase):
"""Tests for the D-Bus external Start/Stop handlers on TrayIndicator.
These use normal (non push-to-talk) start semantics and guard on the
current recognition state, so repeated `vocalinux --start`/`--stop` calls
current recognition state, so repeated `voculinux --start`/`--stop` calls
are idempotent.
"""
+4 -4
View File
@@ -218,11 +218,11 @@ def test_gmenu_indicator_adapts_gtk3_calls():
exported = raw.set_menu.call_args[0][0]
assert _labels(exported) == [["Start"]]
indicator.set_icon_full("vocalinux-microphone", "Microphone on")
raw.set_icon.assert_called_once_with("vocalinux-microphone", "Microphone on")
indicator.set_icon_full("voculinux-microphone", "Microphone on")
raw.set_icon.assert_called_once_with("voculinux-microphone", "Microphone on")
indicator.set_title("Vocalinux")
raw.set_title.assert_called_once_with("Vocalinux")
indicator.set_title("Voculinux")
raw.set_title.assert_called_once_with("Voculinux")
def test_refresh_before_set_menu_is_a_no_op():
+36 -36
View File
@@ -2,7 +2,7 @@
The website runs ``bash /tmp/vul.sh`` from whatever directory the terminal is
in. ``venv/``, ``build/``, ``dist/``, and Python bytecode are removed only when
the script itself lives in a Vocalinux source checkout, and only inside that
the script itself lives in a Voculinux source checkout, and only inside that
checkout.
"""
@@ -40,8 +40,8 @@ def _decoy_tree(root: Path) -> None:
(root / ".pytest_cache").mkdir()
(root / ".coverage").write_text("cov", encoding="utf-8")
(root / "src" / "pkg.egg-info").mkdir(parents=True)
(root / "activate-vocalinux.sh").write_text("echo hi\n", encoding="utf-8")
(root / "vocalinux-run.py").write_text("print(1)\n", encoding="utf-8")
(root / "activate-voculinux.sh").write_text("echo hi\n", encoding="utf-8")
(root / "voculinux-run.py").write_text("print(1)\n", encoding="utf-8")
def _assert_decoy_intact(root: Path) -> None:
@@ -56,15 +56,15 @@ def _assert_decoy_intact(root: Path) -> None:
assert (root / ".pytest_cache").is_dir()
assert (root / ".coverage").is_file()
assert (root / "src" / "pkg.egg-info").is_dir()
assert (root / "activate-vocalinux.sh").is_file()
assert (root / "vocalinux-run.py").is_file()
assert (root / "activate-voculinux.sh").is_file()
assert (root / "voculinux-run.py").is_file()
def _make_checkout(root: Path) -> Path:
checkout = root / "vocalinux"
checkout = root / "voculinux"
(checkout / "src" / "vocalinux").mkdir(parents=True)
(checkout / "pyproject.toml").write_text(
'[project]\nname = "vocalinux"\n',
'[project]\nname = "voculinux"\n',
encoding="utf-8",
)
(checkout / "src" / "vocalinux" / "__init__.py").write_text("", encoding="utf-8")
@@ -81,7 +81,7 @@ def _run(script: Path, cwd: Path, home: Path, *args: str) -> subprocess.Complete
"""Run uninstall.sh against a fake home, without the host's pgrep or kill.
The uninstaller looks up this uid's processes and can TERM then KILL a
running Vocalinux. A stub earlier on PATH records the call and matches
running Voculinux. A stub earlier on PATH records the call and matches
nothing, so the suite cannot stop the app a developer has open.
"""
bin_dir = home.parent / "uninstall-test-bin"
@@ -90,17 +90,17 @@ def _run(script: Path, cwd: Path, home: Path, *args: str) -> subprocess.Complete
kill_log = home.parent / "kill.log"
_write_stub(
bin_dir / "pgrep",
"#!/bin/sh\n" 'printf \'%s\\n\' "$*" >> "$VOCALINUX_TEST_PGREP_LOG"\n' "exit 1\n",
"#!/bin/sh\n" 'printf \'%s\\n\' "$*" >> "$VOCULINUX_TEST_PGREP_LOG"\n' "exit 1\n",
)
_write_stub(
bin_dir / "kill",
"#!/bin/sh\n" 'printf \'%s\\n\' "$*" >> "$VOCALINUX_TEST_KILL_LOG"\n' "exit 0\n",
"#!/bin/sh\n" 'printf \'%s\\n\' "$*" >> "$VOCULINUX_TEST_KILL_LOG"\n' "exit 0\n",
)
env = os.environ.copy()
env["HOME"] = str(home)
env["PATH"] = os.pathsep.join((str(bin_dir), env.get("PATH", "")))
env["VOCALINUX_TEST_PGREP_LOG"] = str(pgrep_log)
env["VOCALINUX_TEST_KILL_LOG"] = str(kill_log)
env["VOCULINUX_TEST_PGREP_LOG"] = str(pgrep_log)
env["VOCULINUX_TEST_KILL_LOG"] = str(kill_log)
env.pop("XDG_DATA_HOME", None)
env.pop("XDG_CONFIG_HOME", None)
env.pop("VIRTUAL_ENV", None)
@@ -114,9 +114,9 @@ def _run(script: Path, cwd: Path, home: Path, *args: str) -> subprocess.Complete
stdin=subprocess.DEVNULL,
check=False,
)
if "Checking for running Vocalinux processes" in result.stdout:
if "Checking for running Voculinux processes" in result.stdout:
logged = pgrep_log.read_text(encoding="utf-8") if pgrep_log.is_file() else ""
assert "vocalinux" in logged, "uninstall.sh did not use the test pgrep stub"
assert "voculinux" in logged, "uninstall.sh did not use the test pgrep stub"
if kill_log.is_file():
assert kill_log.read_text(encoding="utf-8").strip() == ""
return result
@@ -136,8 +136,8 @@ def test_curl_style_run_leaves_the_callers_directory_alone(tmp_path: Path) -> No
script = tmp_path / "vul.sh"
_write_script(script)
data = home / ".local" / "share" / "vocalinux"
config = home / ".config" / "vocalinux"
data = home / ".local" / "share" / "voculinux"
config = home / ".config" / "voculinux"
data.mkdir(parents=True)
(data / "marker").write_text("data", encoding="utf-8")
(data / "venv").mkdir()
@@ -146,12 +146,12 @@ def test_curl_style_run_leaves_the_callers_directory_alone(tmp_path: Path) -> No
(config / "config.json").write_text("{}\n", encoding="utf-8")
bindir = home / ".local" / "bin"
bindir.mkdir(parents=True)
(bindir / "vocalinux").write_text("#!/bin/sh\n", encoding="utf-8")
(bindir / "vocalinux-gui").symlink_to("vocalinux")
(bindir / "activate-vocalinux.sh").write_text("echo\n", encoding="utf-8")
(bindir / "voculinux").write_text("#!/bin/sh\n", encoding="utf-8")
(bindir / "voculinux-gui").symlink_to("voculinux")
(bindir / "activate-voculinux.sh").write_text("echo\n", encoding="utf-8")
desktop = home / ".local" / "share" / "applications"
desktop.mkdir(parents=True)
(desktop / "vocalinux.desktop").write_text("[Desktop Entry]\n", encoding="utf-8")
(desktop / "voculinux.desktop").write_text("[Desktop Entry]\n", encoding="utf-8")
result = _run(script, cwd, home)
@@ -161,10 +161,10 @@ def test_curl_style_run_leaves_the_callers_directory_alone(tmp_path: Path) -> No
_assert_decoy_intact(cwd)
assert not data.exists()
assert not config.exists()
assert not (bindir / "vocalinux").exists()
assert not (bindir / "vocalinux-gui").exists()
assert not (bindir / "activate-vocalinux.sh").exists()
assert not (desktop / "vocalinux.desktop").exists()
assert not (bindir / "voculinux").exists()
assert not (bindir / "voculinux-gui").exists()
assert not (bindir / "activate-voculinux.sh").exists()
assert not (desktop / "voculinux.desktop").exists()
def test_keep_flags_still_do_not_touch_the_working_directory(tmp_path: Path) -> None:
@@ -175,8 +175,8 @@ def test_keep_flags_still_do_not_touch_the_working_directory(tmp_path: Path) ->
_decoy_tree(cwd)
script = tmp_path / "vul.sh"
_write_script(script)
config = home / ".config" / "vocalinux"
data = home / ".local" / "share" / "vocalinux"
config = home / ".config" / "voculinux"
data = home / ".local" / "share" / "voculinux"
config.mkdir(parents=True)
data.mkdir(parents=True)
(config / "config.json").write_text("{}\n", encoding="utf-8")
@@ -217,8 +217,8 @@ def test_source_checkout_cleans_itself_and_leaves_the_caller_alone(tmp_path: Pat
assert not (checkout / ".pytest_cache").exists()
assert not (checkout / "proj" / "pkg" / "__pycache__").exists()
assert not (checkout / "src" / "pkg.egg-info").exists()
assert not (checkout / "activate-vocalinux.sh").exists()
assert not (checkout / "vocalinux-run.py").exists()
assert not (checkout / "activate-voculinux.sh").exists()
assert not (checkout / "voculinux-run.py").exists()
def test_dev_venv_records_survive_checkout_cleanup(tmp_path: Path) -> None:
@@ -234,8 +234,8 @@ def test_dev_venv_records_survive_checkout_cleanup(tmp_path: Path) -> None:
dev_cache.mkdir(parents=True)
(dev_cache / "mod.cpython-312.pyc").write_bytes(b"pyc")
(checkout / ".venv" / ".coverage").write_text("keep", encoding="utf-8")
(checkout / "vocalinux.egg-info").mkdir()
(checkout / "vocalinux.egg-info" / "PKG-INFO").write_text("name\n", encoding="utf-8")
(checkout / "voculinux.egg-info").mkdir()
(checkout / "voculinux.egg-info" / "PKG-INFO").write_text("name\n", encoding="utf-8")
result = _run(checkout / "uninstall.sh", tmp_path, home)
@@ -245,7 +245,7 @@ def test_dev_venv_records_survive_checkout_cleanup(tmp_path: Path) -> None:
assert (dev_cache / "mod.cpython-312.pyc").is_file()
assert (checkout / ".venv" / ".coverage").is_file()
assert not (checkout / "src" / "pkg.egg-info").exists()
assert not (checkout / "vocalinux.egg-info").exists()
assert not (checkout / "voculinux.egg-info").exists()
assert not (checkout / "proj" / "pkg" / "__pycache__").exists()
assert not (checkout / ".coverage").exists()
@@ -257,11 +257,11 @@ def test_curl_clone_uninstall_does_not_print_a_cleanup_error(tmp_path: Path) ->
home.mkdir()
cwd.mkdir()
(cwd / "notes.txt").write_text("keep", encoding="utf-8")
clone = home / ".local" / "share" / "vocalinux-install"
clone = home / ".local" / "share" / "voculinux-install"
(clone / "src" / "vocalinux").mkdir(parents=True)
(clone / "pyproject.toml").write_text('[project]\nname = "vocalinux"\n', encoding="utf-8")
(clone / "pyproject.toml").write_text('[project]\nname = "voculinux"\n', encoding="utf-8")
(clone / "src" / "vocalinux" / "__init__.py").write_text("", encoding="utf-8")
(clone / "src" / "vocalinux.egg-info").mkdir()
(clone / "src" / "voculinux.egg-info").mkdir()
(clone / "build").mkdir()
(clone / "build" / "out").write_text("build", encoding="utf-8")
_write_script(clone / "uninstall.sh")
@@ -298,7 +298,7 @@ def test_another_projects_tree_is_not_treated_as_a_checkout(tmp_path: Path) -> N
home.mkdir()
project = tmp_path / "other"
(project / "src" / "vocalinux").mkdir(parents=True)
(project / "pyproject.toml").write_text('name = "not-vocalinux"\n', encoding="utf-8")
(project / "pyproject.toml").write_text('name = "not-voculinux"\n', encoding="utf-8")
_write_script(project / "uninstall.sh")
_decoy_tree(project)
@@ -399,7 +399,7 @@ def test_uninstaller_is_valid_bash() -> None:
"pyproject",
[
"",
'name = "vocalinux"\n',
'name = "voculinux"\n',
],
)
def test_checkout_detection_needs_both_the_name_and_the_package_dir(
+5 -5
View File
@@ -114,7 +114,7 @@ class TestTrustedUrls:
def test_rejects_untrusted_urls(self):
assert not is_trusted_release_url("https://evil.example/payload")
assert not is_trusted_release_url("http://github.com/VocaHQ/vocalinux")
assert not is_trusted_release_url("https://github.com/evil/vocalinux")
assert not is_trusted_release_url("https://github.com/evil/voculinux")
assert not is_trusted_release_url("")
@@ -315,7 +315,7 @@ class TestFetchLatestRelease:
def test_evil_update_url_does_not_become_html_url(self, caplog):
tag_url = "https://github.com/VocaHQ/vocalinux/releases/tag/v0.17.0"
evil_paths = [
"/evil/vocalinux/releases/tag/v9",
"/evil/voculinux/releases/tag/v9",
"https://evil.example/x",
"/VocaHQ/vocalinux/releases/../evil",
]
@@ -343,7 +343,7 @@ class TestFetchLatestRelease:
}
untrusted_urls = (
"https://evil.example/VocaHQ/vocalinux/releases/tag/v0.17.0",
"https://github.com/evil/vocalinux/releases/tag/v0.17.0",
"https://github.com/evil/voculinux/releases/tag/v0.17.0",
"http://github.com/VocaHQ/vocalinux/releases/tag/v0.17.0",
)
caplog.set_level(logging.WARNING, logger="vocalinux.utils.update_checker")
@@ -392,11 +392,11 @@ class TestFetchLatestRelease:
first, second = mock_requests.get.call_args_list
assert first[0][0] == "https://api.github.com/repos/VocaHQ/vocalinux/releases/latest"
assert first.kwargs["headers"]["Accept"] == "application/vnd.github+json"
assert first.kwargs["headers"]["User-Agent"] == "Vocalinux-UpdateChecker"
assert first.kwargs["headers"]["User-Agent"] == "Voculinux-UpdateChecker"
assert "Authorization" not in first.kwargs["headers"]
assert second[0][0] == "https://github.com/VocaHQ/vocalinux/releases/latest"
assert second.kwargs["headers"]["Accept"] == "application/json"
assert second.kwargs["headers"]["User-Agent"] == "Vocalinux-UpdateChecker"
assert second.kwargs["headers"]["User-Agent"] == "Voculinux-UpdateChecker"
assert "Authorization" not in second.kwargs["headers"]
assert fallback.url == tag_url
assert _checker_warnings(caplog) == []
+1 -1
View File
@@ -52,7 +52,7 @@ def test_policy_states_qualification_rule() -> None:
def test_policy_names_retained_internal_module_name() -> None:
policy = _policy()
assert "vocalinux" in policy
assert "voculinux" in policy
assert "internal" in policy.lower()
+7 -7
View File
@@ -35,11 +35,11 @@ def test_the_dispatch_input_never_reaches_the_shell():
def test_the_manifest_parses_as_sha256sum_writes_it():
parsed = verify.parse_manifest(
"0ee0d8e6 Vocalinux-0.16.2-x86_64.AppImage\n22deb51a *vocalinux-0.16.2.whl\n\n"
"0ee0d8e6 Voculinux-0.16.2-x86_64.AppImage\n22deb51a *voculinux-0.16.2.whl\n\n"
)
assert parsed == {
"Vocalinux-0.16.2-x86_64.AppImage": "0ee0d8e6",
"vocalinux-0.16.2.whl": "22deb51a",
"Voculinux-0.16.2-x86_64.AppImage": "0ee0d8e6",
"voculinux-0.16.2.whl": "22deb51a",
}
@@ -108,7 +108,7 @@ def test_provenance_asks_only_for_slsa_build_provenance(monkeypatch):
def test_a_release_missing_a_distribution_names_it():
"""Comparing only what is left would pass while PyPI serves an unchecked file."""
assert verify.check_pypi("0.16.2", {"Vocalinux-0.16.2-x86_64.AppImage": "aa"}) == [
assert verify.check_pypi("0.16.2", {"Voculinux-0.16.2-x86_64.AppImage": "aa"}) == [
"the release carries no .whl to compare against PyPI",
"the release carries no .tar.gz to compare against PyPI",
]
@@ -130,9 +130,9 @@ def test_an_extra_pypi_distribution_fails(monkeypatch):
import urllib.request
wheel = "vocalinux-0.16.2-py3-none-any.whl"
sdist = "vocalinux-0.16.2.tar.gz"
extra = "vocalinux-0.16.2-extra.whl"
wheel = "voculinux-0.16.2-py3-none-any.whl"
sdist = "voculinux-0.16.2.tar.gz"
extra = "voculinux-0.16.2-extra.whl"
payload = {
"urls": [
{"filename": wheel, "digests": {"sha256": "aa"}},
+1 -1
View File
@@ -7,7 +7,7 @@ import sys
import types
from unittest.mock import MagicMock, patch # noqa: F401
# Mock GTK and other dependencies before importing vocalinux
# Mock GTK and other dependencies before importing voculinux
sys.modules["gi"] = MagicMock()
sys.modules["gi.repository"] = MagicMock()
sys.modules["vosk"] = MagicMock()
+1 -1
View File
@@ -210,7 +210,7 @@ class TestModelInfo(unittest.TestCase):
"large-v2",
"large-v2-q5_0",
"large-v2-q8_0",
"large", # Vocalinux alias for upstream large-v3
"large", # Voculinux alias for upstream large-v3
"large-v3-q5_0",
"large-v3-turbo",
"large-v3-turbo-q5_0",