fix(app): drop upstream references from user-facing source #40
@@ -6,6 +6,9 @@ optional power-user path. It is **not** on-device processing: microphone audio
|
||||
still leaves the desktop client and is transcribed by the gateway process on
|
||||
your machine (or LAN).
|
||||
|
||||
VocaGateway is a separate upstream project by VocaHQ, not part of Voculinux.
|
||||
Voculinux does not publish it; the Settings action clones the pinned upstream tag.
|
||||
|
||||
The default speech engine remains **whisper.cpp** on-device. Enabling a local
|
||||
gateway never changes that default by itself. Use **Use this Gateway** only when
|
||||
you want Voculinux to talk to the gateway over the existing Remote API settings
|
||||
|
||||
@@ -4,6 +4,9 @@ This is an opt-in path for power users who want to run
|
||||
https://github.com/VocaHQ/vocagateway on the same Linux box via podman
|
||||
(or docker). It is not on-device processing: audio still leaves the
|
||||
desktop client and goes to the local gateway container.
|
||||
|
||||
VocaGateway is a separate upstream project, not part of Voculinux. This fork
|
||||
does not publish it; the helpers clone its pinned upstream tag.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -13,6 +13,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Keep pin explicit: never float on latest.
|
||||
GATEWAY_RELEASE_TAG = "v0.1.0"
|
||||
# VocaGateway is a separate upstream project, not part of Voculinux, and this fork
|
||||
# does not publish it. The URL is upstream's on purpose: the Settings action clones
|
||||
# this pinned tag from it. Do not point it at a fork.
|
||||
GATEWAY_GIT_URL = "https://github.com/VocaHQ/vocagateway.git"
|
||||
COMPOSE_PROJECT = "vocagateway"
|
||||
DEFAULT_PORT = 8765
|
||||
|
||||
@@ -256,14 +256,12 @@ def check_dependencies():
|
||||
"typelib-1_0-Notify-0_7 libnotify4"
|
||||
)
|
||||
logger.error("")
|
||||
logger.error(" Void Linux (signed xbps package, pulls in GTK and PyGObject):")
|
||||
logger.error(" Register the repository once, as described in:")
|
||||
logger.error(
|
||||
"For pipx users: Install system packages BEFORE running 'pipx install voculinux'"
|
||||
)
|
||||
logger.error("")
|
||||
logger.error("For the best experience, use the recommended installer:")
|
||||
logger.error(
|
||||
" curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh | bash"
|
||||
" https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/INSTALL.md"
|
||||
)
|
||||
logger.error(" sudo xbps-install -Su voculinux")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -106,7 +106,7 @@ except (ImportError, ValueError) as e:
|
||||
|
||||
# File paths for communication ($XDG_DATA_HOME, defaults to ~/.local/share).
|
||||
# Identity strings come from vocalinux/app_identity.py (see the import above);
|
||||
# the homepage below is the retained upstream URL, never renamed.
|
||||
# the homepage below is this fork's repository, not upstream's.
|
||||
VOCULINUX_IBUS_DIR = Path(xdg_data_home()) / IBUS_DIR_NAME
|
||||
SOCKET_PATH = VOCULINUX_IBUS_DIR / "inject.sock"
|
||||
PID_FILE = VOCULINUX_IBUS_DIR / "engine.pid"
|
||||
@@ -125,7 +125,7 @@ _COMPONENT_META = {
|
||||
"version": "1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"author": APP_NAME,
|
||||
"homepage": "https://github.com/VocaHQ/vocalinux",
|
||||
"homepage": "https://git.bongbetic.com/xavierk/voculinux",
|
||||
"textdomain": IBUS_TEXTDOMAIN,
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,21 @@ def _kde_wayland_ibus_hint() -> str:
|
||||
)
|
||||
|
||||
|
||||
#: This fork's docs on Gitea. The upstream wiki describes a different project.
|
||||
_FORK_DOCS_URL = "https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs"
|
||||
|
||||
|
||||
def _ibus_daemon_missing_hint() -> str:
|
||||
"""Return where to look when the IBus daemon is not running.
|
||||
|
||||
niri excludes IBus (see ``TextInjector._wayland_compositor_bridges_ibus``), so
|
||||
there the hint names niri's notes rather than an IBus setup guide.
|
||||
"""
|
||||
if _is_niri_session():
|
||||
return f"IBus is excluded on niri; see {_FORK_DOCS_URL}/NIRI.md"
|
||||
return f"For IBus setup, see: {_FORK_DOCS_URL}/TROUBLESHOOTING.md"
|
||||
|
||||
|
||||
class DesktopEnvironment(Enum):
|
||||
"""Enum representing the desktop environment."""
|
||||
|
||||
@@ -977,8 +992,8 @@ class TextInjector:
|
||||
elif not force_ibus and not is_ibus_daemon_running():
|
||||
logger.info(
|
||||
"IBus daemon not running. This is normal on some desktop environments "
|
||||
"(e.g., KDE Plasma). Using alternative text injection method. "
|
||||
"For IBus setup, see: https://github.com/VocaHQ/vocalinux/wiki/IBus-Setup"
|
||||
"(e.g., KDE Plasma). Using alternative text injection method. %s",
|
||||
_ibus_daemon_missing_hint(),
|
||||
)
|
||||
# Some Wayland compositors (COSMIC, Sway, Hyprland, ...) do not deliver
|
||||
# IBus commits to native Wayland apps, so IBus would silently drop the
|
||||
|
||||
@@ -806,6 +806,10 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
|
||||
|
||||
component_kwargs = mock_component_cls.call_args.kwargs
|
||||
self.assertIn("--ibus", component_kwargs["command_line"])
|
||||
# The component's homepage is this fork's repository, not upstream's.
|
||||
self.assertEqual(
|
||||
component_kwargs["homepage"], "https://git.bongbetic.com/xavierk/voculinux"
|
||||
)
|
||||
|
||||
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
|
||||
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
|
||||
|
||||
@@ -224,6 +224,25 @@ class TestCheckDependencies(unittest.TestCase):
|
||||
)
|
||||
mock_logger.error.assert_any_call(" sudo apt install gnome-shell-extension-appindicator")
|
||||
|
||||
def test_check_dependencies_points_void_users_at_the_xbps_package(self):
|
||||
"""Voculinux ships only as a Void xbps package: the hint names it, no upstream script."""
|
||||
from vocalinux.main import check_dependencies
|
||||
|
||||
with patch("vocalinux.main.logger") as mock_logger:
|
||||
with patch("builtins.__import__", side_effect=ImportError("No module")):
|
||||
result = check_dependencies()
|
||||
|
||||
assert result is False
|
||||
logged = [call.args[0] for call in mock_logger.error.call_args_list if call.args]
|
||||
assert " sudo xbps-install -Su voculinux" in logged
|
||||
assert (
|
||||
" https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/INSTALL.md"
|
||||
in logged
|
||||
)
|
||||
assert not any(
|
||||
marker in line for line in logged for marker in ("curl", "| bash", "pipx", "VocaHQ")
|
||||
)
|
||||
|
||||
|
||||
class TestCheckDisplayAvailable(unittest.TestCase):
|
||||
"""Tests for check_display_available() function."""
|
||||
|
||||
@@ -26,12 +26,17 @@ install message. On those, upstream references are not allowlisted by pattern
|
||||
at all. An upstream URL, handle or host is a regression unless the line is
|
||||
exactly the one credit line recorded for that file.
|
||||
|
||||
Under ``src/`` the slug and label rules are not allowlisted either. Every
|
||||
upstream reference in source is a recorded line (``SRC_UPSTREAM_CREDITS``),
|
||||
matched exactly and once, so a new one fails the source scan.
|
||||
|
||||
``tests/test_rename_sweep.py`` also checks the allowlist itself, so a loosened
|
||||
rule fails here before it lets an old name onto a user-visible surface.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SRC_PACKAGE = os.path.join(REPO_ROOT, "src", "vocalinux")
|
||||
@@ -116,6 +121,34 @@ FORK_SURFACES = frozenset(
|
||||
"packaging/void/srcpkgs/voculinux/INSTALL.msg",
|
||||
}
|
||||
)
|
||||
#: Upstream references under ``src/``, by the release checklist's pattern. Each
|
||||
#: hit must be a recorded line in SRC_UPSTREAM_CREDITS, not a pattern match.
|
||||
SRC_UPSTREAM_REFERENCE = re.compile(
|
||||
r"github\.com/(?:VocaHQ|jatinkrmalik)|vocahq|raw\.githubusercontent", re.IGNORECASE
|
||||
)
|
||||
#: Every upstream reference under src/, as stripped lines per file. The scan
|
||||
#: counts occurrences, so a duplicated or stale entry fails too.
|
||||
SRC_UPSTREAM_CREDITS: dict[str, frozenset[str]] = {
|
||||
# About-page credit (also the fork-surface credit above).
|
||||
"src/vocalinux/ui/settings_about.py": FORK_SURFACE_CREDITS[
|
||||
"src/vocalinux/ui/settings_about.py"
|
||||
],
|
||||
# Provenance record in RETAINED_SURFACES.
|
||||
"src/vocalinux/app_identity.py": frozenset(
|
||||
{'"https://github.com/VocaHQ/vocalinux (upstream URLs and provenance)",'}
|
||||
),
|
||||
# Docstring: the upstream repository is never queried.
|
||||
"src/vocalinux/utils/update_checker.py": frozenset(
|
||||
{"upstream VocaHQ repository is never queried, and packaged installs never query"}
|
||||
),
|
||||
# VocaGateway: a separate upstream project, cloned at its pinned tag.
|
||||
"src/vocalinux/gateway_embed/paths_embed.py": frozenset(
|
||||
{'GATEWAY_GIT_URL = "https://github.com/VocaHQ/vocagateway.git"'}
|
||||
),
|
||||
"src/vocalinux/gateway_embed/__init__.py": frozenset(
|
||||
{"https://github.com/VocaHQ/vocagateway on the same Linux box via podman"}
|
||||
),
|
||||
}
|
||||
#: Upstream hosts, handles and URL forms. Plain words such as "Flathub" are not
|
||||
#: listed, so a sentence that says a channel is not published stays legal.
|
||||
UPSTREAM_REFERENCE = re.compile(
|
||||
@@ -229,7 +262,12 @@ def _allowed(path: str, line: str) -> bool:
|
||||
if path == WEB_REBRAND_TEST and line.strip() in WEB_REBRAND_SCANNER_LINES:
|
||||
# The web rebrand test names the old word only to scan for it.
|
||||
return True
|
||||
if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line):
|
||||
if line.strip() in SRC_UPSTREAM_CREDITS.get(path, frozenset()):
|
||||
return True
|
||||
if not path.startswith("src/") and (
|
||||
_UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line)
|
||||
):
|
||||
# Upstream provenance on tests, docs, web and packaging. Source is pinned line by line.
|
||||
return True
|
||||
if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line):
|
||||
return True
|
||||
@@ -305,6 +343,45 @@ def test_fork_surfaces_carry_their_credit_exactly_once() -> None:
|
||||
assert lines.count(credit) == 1, f"{rel}: credit line must appear exactly once"
|
||||
|
||||
|
||||
def test_src_upstream_references_are_exactly_the_recorded_credits() -> None:
|
||||
"""Under src/, an upstream reference is allowed only on its recorded line, once each."""
|
||||
found: Counter[tuple[str, str]] = Counter()
|
||||
for rel, full in _iter_files():
|
||||
if not rel.startswith("src/") or 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 line in text.splitlines():
|
||||
if SRC_UPSTREAM_REFERENCE.search(line):
|
||||
found[(rel, line.strip())] += 1
|
||||
expected: Counter[tuple[str, str]] = Counter(
|
||||
(rel, line) for rel, lines in SRC_UPSTREAM_CREDITS.items() for line in lines
|
||||
)
|
||||
unrecorded = sorted(f"{rel}: {line}" for rel, line in (found - expected).elements())
|
||||
missing = sorted(f"{rel}: {line}" for rel, line in (expected - found).elements())
|
||||
assert not unrecorded and not missing, (
|
||||
"src/ upstream references must match SRC_UPSTREAM_CREDITS exactly, once each.\n"
|
||||
f"unrecorded: {unrecorded}\nmissing: {missing}"
|
||||
)
|
||||
|
||||
|
||||
def test_gateway_docs_state_vocagateway_is_upstream() -> None:
|
||||
"""The gateway guide says VocaGateway is upstream's project, not Voculinux's."""
|
||||
path = os.path.join(REPO_ROOT, "docs", "GATEWAY_EMBED.md")
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
text = " ".join(handle.read().split())
|
||||
sentence = (
|
||||
"VocaGateway is a separate upstream project by VocaHQ, not part of Voculinux. "
|
||||
"Voculinux does not publish it; the Settings action clones the pinned upstream tag."
|
||||
)
|
||||
assert sentence in text
|
||||
|
||||
|
||||
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):
|
||||
@@ -337,6 +414,18 @@ def test_allowlist_denies_user_facing_old_names() -> None:
|
||||
("src/vocalinux/ui/tray_indicator.py", 'ICON = "vocalinux-microphone"'),
|
||||
("packaging/void/srcpkgs/voculinux/template", "pkgname=vocalinux"),
|
||||
("docs/INSTALL.md", "Open vocalinux.desktop from the menu."),
|
||||
(
|
||||
"src/vocalinux/main.py",
|
||||
'" curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh | bash"',
|
||||
),
|
||||
(
|
||||
"src/vocalinux/text_injection/text_injector.py",
|
||||
'"For IBus setup, see: https://github.com/VocaHQ/vocalinux/wiki/IBus-Setup"',
|
||||
),
|
||||
(
|
||||
"src/vocalinux/text_injection/ibus_engine.py",
|
||||
'"homepage": "https://github.com/VocaHQ/vocalinux",',
|
||||
),
|
||||
]
|
||||
for path, line in denied:
|
||||
assert not _allowed(path, line), f"{path}: {line!r} must not be allowlisted"
|
||||
@@ -358,6 +447,10 @@ def test_allowlist_keeps_the_retained_surfaces() -> None:
|
||||
("tests/test_app_identity.py", "from vocalinux import app_identity"),
|
||||
("packaging/void/container-release.sh", 'grep -E "usr/share/applications/vocalinux"'),
|
||||
("install.sh", 'local PKG_NAME="vocalinux"'),
|
||||
(
|
||||
"src/vocalinux/app_identity.py",
|
||||
'"https://github.com/VocaHQ/vocalinux (upstream URLs and provenance)",',
|
||||
),
|
||||
]
|
||||
for path, line in kept:
|
||||
assert _allowed(path, line), f"{path}: {line!r} is a retained surface"
|
||||
|
||||
@@ -19,6 +19,7 @@ from unittest.mock import MagicMock, mock_open, patch
|
||||
from vocalinux.text_injection.text_injector import (
|
||||
DesktopEnvironment,
|
||||
TextInjector,
|
||||
_ibus_daemon_missing_hint,
|
||||
_warn_if_ydotool_globally_enabled,
|
||||
_ydotool_install_guidance,
|
||||
)
|
||||
@@ -3410,6 +3411,34 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
|
||||
class TestIBusDaemonMissingHint(unittest.TestCase):
|
||||
"""The "IBus daemon not running" hint points at fork docs; niri gets its own notes."""
|
||||
|
||||
_SESSION_VARS = ("NIRI_SOCKET", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION")
|
||||
_FORK_DOCS = "https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs"
|
||||
|
||||
def _hint_for(self, **session: str) -> str:
|
||||
env = {name: "" for name in self._SESSION_VARS} | session
|
||||
with mock.patch.dict(os.environ, env):
|
||||
return _ibus_daemon_missing_hint()
|
||||
|
||||
def test_niri_session_names_niri_notes_not_ibus_setup(self):
|
||||
self.assertEqual(
|
||||
self._hint_for(XDG_CURRENT_DESKTOP="niri"),
|
||||
f"IBus is excluded on niri; see {self._FORK_DOCS}/NIRI.md",
|
||||
)
|
||||
|
||||
def test_niri_socket_alone_counts_as_niri(self):
|
||||
hint = self._hint_for(NIRI_SOCKET="/run/user/1000/niri.wayland-1.sock")
|
||||
self.assertEqual(hint, f"IBus is excluded on niri; see {self._FORK_DOCS}/NIRI.md")
|
||||
|
||||
def test_other_sessions_point_at_the_fork_ibus_docs(self):
|
||||
self.assertEqual(
|
||||
self._hint_for(XDG_CURRENT_DESKTOP="GNOME"),
|
||||
f"For IBus setup, see: {self._FORK_DOCS}/TROUBLESHOOTING.md",
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _fake_config(config: Any) -> Iterator[None]:
|
||||
"""Pretend config.json holds ``config``; ``None`` means no file at all.
|
||||
|
||||
Reference in New Issue
Block a user