This commit is contained in:
Javis committed 2026-10-04 15:31:44 +02:00
1 parent 0246cbb6ba
commit 0ca4b4bf7a
89 files changed
+7042 -2480

No files matched your search

+6
View File
@@ -79,6 +79,12 @@ def _probe(win) -> None:
print("\n[probe] OK — retheme registry is theme-reactive (dark->light->dark round-trip).")
# GUI-01 regression: voice-profile cards used undefined names.
card = win._make_voice_card(
{"name": "Alice", "clip_count": 3, "enrolled_at": "2026-01-01T00:00:00"})
assert card.layout() is not None and card.layout().count() == 2, "voice card layout broken"
print("[probe] OK — voice-profile card renders.")
def main() -> int:
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
+396
View File
@@ -0,0 +1,396 @@
"""Regression tests for the fixes listed in docs/AUDIT.md."""
from __future__ import annotations
import asyncio
import json
import tempfile
import unittest
import zipfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from javis.core.config import Config, PathsConfig, load_config
class SchemaBridgeTests(unittest.TestCase):
"""MCP-01 / MCP-03."""
def test_input_schema_read_from_snake_and_camel_case(self) -> None:
from javis.mcp_client.schema_bridge import mcp_tool_to_ollama
schema = {"type": "object", "properties": {"q": {"type": "string"}}}
v2 = SimpleNamespace(name="search", description="d", input_schema=schema)
v1 = SimpleNamespace(name="search", description="d", inputSchema=schema)
for tool in (v2, v1):
entry = mcp_tool_to_ollama("ddg", tool)
self.assertEqual(entry["function"]["parameters"], schema)
self.assertEqual(entry["function"]["name"], "ddg__search")
def test_real_mcp_tool_schema_is_forwarded(self) -> None:
import mcp.types as t
from javis.mcp_client.schema_bridge import mcp_tool_to_ollama
schema = {"type": "object", "properties": {"a": {"type": "string"}}}
fields = t.Tool.model_fields
key = "input_schema" if "input_schema" in fields else "inputSchema"
tool = t.Tool(name="x", **{key: schema})
self.assertEqual(mcp_tool_to_ollama("s", tool)["function"]["parameters"], schema)
def test_bare_server_name_is_not_mapped_to_a_tool(self) -> None:
from javis.mcp_client.schema_bridge import validate_tool_name
self.assertEqual(validate_tool_name("homey", {"homey": ["delete_all"]}), ("", "homey"))
self.assertEqual(validate_tool_name("homey__find_devices"), ("homey", "find_devices"))
class ResultToTextTests(unittest.TestCase):
def test_is_error_detected_for_both_sdk_generations(self) -> None:
from javis.mcp_client.host import _result_to_text
content = [SimpleNamespace(text="boom")]
self.assertEqual(_result_to_text(SimpleNamespace(content=content, is_error=True)), "ERROR: boom")
self.assertEqual(_result_to_text(SimpleNamespace(content=content, isError=True)), "ERROR: boom")
self.assertEqual(_result_to_text(SimpleNamespace(content=content, is_error=False)), "boom")
def _host_with_fake_server(approval: str = "ask"):
from javis.mcp_client.host import ConnectedServer, MCPHost, ServerSpec
calls: list[tuple[str, dict]] = []
class FakeSession:
async def call_tool(self, tool, arguments):
calls.append((tool, arguments))
return SimpleNamespace(content=[SimpleNamespace(text="ok")], is_error=False)
host = MCPHost(Config())
spec = ServerSpec(name="fs", command="x", approval=approval)
host.servers["fs"] = ConnectedServer(spec=spec, session=FakeSession(),
tools=[SimpleNamespace(name="write")])
return host, calls
class ApprovalTests(unittest.IsolatedAsyncioTestCase):
"""MCP-02: "Allow once" must not become "Always allow"."""
async def test_allow_once_lasts_only_for_the_turn(self) -> None:
host, calls = _host_with_fake_server()
asked: list[str] = []
async def approve(server, tool, args):
asked.append(tool)
return True
host.approval_cb = approve
self.assertEqual(await host.call("fs__write", {"p": 1}), "ok")
self.assertEqual(await host.call("fs__write", {"p": 2}), "ok")
self.assertEqual(len(asked), 1) # same turn: not asked again
host.clear_denials() # next turn
await host.call("fs__write", {"p": 3})
self.assertEqual(len(asked), 2)
self.assertEqual(len(calls), 3)
async def test_always_allow_persists(self) -> None:
host, _ = _host_with_fake_server()
asked: list[str] = []
async def approve(server, tool, args):
asked.append(tool)
return True
host.approval_cb = approve
host.allow_tool("fs", "write")
host.clear_denials()
await host.call("fs__write", {})
self.assertEqual(asked, [])
async def test_bare_server_name_does_not_execute_anything(self) -> None:
host, calls = _host_with_fake_server(approval="allow")
result = await host.call("fs", {"p": 1})
self.assertTrue(result.startswith("ERROR:"))
self.assertIn("fs__write", result)
self.assertEqual(calls, [])
async def test_protocol_error_is_not_retried(self) -> None:
from javis.mcp_client import host as host_mod
host, _ = _host_with_fake_server(approval="allow")
attempts = []
class FailingSession:
async def call_tool(self, tool, arguments):
attempts.append(tool)
raise host_mod._MCPError(-32602, "invalid params")
host.servers["fs"].session = FailingSession()
async def no_reconnect(*a, **kw): # pragma: no cover - must not run
raise AssertionError("protocol errors must not trigger a reconnect")
with patch.object(host_mod.MCPHost, "_reconnect", no_reconnect):
result = await host.call("fs__write", {})
self.assertTrue(result.startswith("ERROR:"))
self.assertEqual(attempts, ["write"])
async def test_destructive_tool_asks_even_on_allow_server(self) -> None:
host, calls = _host_with_fake_server(approval="allow")
host.servers["fs"].tools = [
SimpleNamespace(name="write", annotations=SimpleNamespace(destructive_hint=True)),
SimpleNamespace(name="read", annotations=SimpleNamespace(
read_only_hint=True, destructive_hint=True)),
]
asked: list[str] = []
async def deny(server, tool, args):
asked.append(tool)
return False
host.approval_cb = deny
self.assertTrue((await host.call("fs__write", {})).startswith("ERROR:"))
self.assertEqual(await host.call("fs__read", {}), "ok") # read-only wins
self.assertEqual(asked, ["write"])
self.assertEqual(calls, [("read", {})])
host.config.mcp.confirm_destructive = False
host.clear_denials()
self.assertEqual(await host.call("fs__write", {}), "ok")
self.assertEqual(asked, ["write"])
class ConversationTrimTests(unittest.TestCase):
"""LLM-02: the in-progress turn is never trimmed away."""
def test_current_turn_survives_oversized_tool_results(self) -> None:
from javis.llm.conversation import Conversation
conv = Conversation(system_prompt="s" * 100, max_turns=20, max_chars=500)
conv.add_user("old question")
conv.add_assistant("old answer")
conv.add_user("current question")
conv.add_assistant("", tool_calls=[{"function": {"name": "a__b", "arguments": {}}}])
conv.add_tool_result("a__b", "x" * 2000)
roles = [m["role"] for m in conv.history]
self.assertEqual(roles, ["user", "assistant", "tool"])
self.assertEqual(conv.history[0]["content"], "current question")
class QuickEngineValidationTests(unittest.TestCase):
def test_invalid_regex_rejected_and_never_breaks_matching(self) -> None:
from javis.agent.quick_engine import QuickAction, QuickCommand, QuickEngine
with tempfile.TemporaryDirectory() as d:
q = QuickEngine(Path(d) / "q.json")
with self.assertRaises(ValueError):
q.add("([unclosed", "regex", [{"tool": "a__b", "args": {}}])
with self.assertRaises(ValueError):
q.add("x", "fuzzy", [{"tool": "a__b", "args": {}}])
# A bad pattern already on disk must be skipped, not raise.
q._commands["bad"] = QuickCommand("bad", "([", "regex", [QuickAction("a__b")])
q.add("licht an", "contains", [{"tool": "homey__on", "args": {}}])
self.assertIsNotNone(q.match("licht an"))
class QuickPrivilegeTests(unittest.IsolatedAsyncioTestCase):
"""AGT-01: quick commands honour speaker privileges."""
async def test_denied_action_skips_quick_path(self) -> None:
from javis.agent.orchestrator import Orchestrator
from javis.agent.quick_engine import QuickEngine
from javis.stt.voice_profiler import SpeakerPrivileges
with tempfile.TemporaryDirectory() as d:
orch = object.__new__(Orchestrator)
orch.quick = QuickEngine(Path(d) / "q.json")
orch.quick.add("licht an", "contains", [{"tool": "homey__on", "args": {}}])
executed = []
class FakeMCP:
async def call(self, name, args):
executed.append(name)
return "ok"
orch.mcp = FakeMCP()
guest = SpeakerPrivileges(denied_tools=["homey__*"])
ran = await orch._try_quick_response("licht an", privileges=guest)
self.assertFalse(ran)
self.assertEqual(executed, [])
class StopIdempotentTests(unittest.IsolatedAsyncioTestCase):
"""AGT-03: concurrent stop() calls tear down exactly once."""
async def test_concurrent_stop_runs_teardown_once(self) -> None:
from javis.agent.orchestrator import Orchestrator
orch = object.__new__(Orchestrator)
orch._stop = asyncio.Event()
orch._stop_lock = asyncio.Lock()
orch._stopped = False
orch.mic = None
orch._barge_in_task = None
counts = {"mcp": 0, "llm": 0}
class Timers:
async def aclose(self):
await asyncio.sleep(0.01)
class MCP:
async def stop(self):
counts["mcp"] += 1
await asyncio.sleep(0.01)
class LLM:
async def aclose(self):
counts["llm"] += 1
orch.timer_service = Timers()
orch.mcp = MCP()
orch.llm = orch.memory_llm = LLM()
await asyncio.gather(orch.stop(), orch.stop(), orch.stop())
self.assertEqual(counts, {"mcp": 1, "llm": 1})
class PcmWriterTests(unittest.IsolatedAsyncioTestCase):
"""AGT-06: cancelling playback stops the writer thread before returning."""
async def test_cancel_waits_for_writer_thread(self) -> None:
import threading
import time
import numpy as np
from javis.audio_io.stream import write_pcm_blocks
writes = []
closed = threading.Event()
class FakeStream:
def write(self, block):
if closed.is_set():
raise AssertionError("write after close")
time.sleep(0.01)
writes.append(len(block))
task = asyncio.ensure_future(
write_pcm_blocks(FakeStream(), np.zeros((16000, 1), np.float32), 160))
await asyncio.sleep(0.05)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task
closed.set() # caller would close the stream now
n = len(writes)
await asyncio.sleep(0.05)
self.assertEqual(len(writes), n)
self.assertLess(n, 100)
class ConsoleLaunchTests(unittest.TestCase):
"""GUI-04: every argument is quoted for cmd.exe."""
def test_metacharacters_are_quoted(self) -> None:
from javis.gui.main_window import _launch_in_console
with patch("subprocess.Popen") as popen:
_launch_in_console(["C:\\Py thon\\python.exe", "--voice", "a&calc"], cwd=".")
line = popen.call_args.args[0]
self.assertEqual(line, 'cmd.exe /k ""C:\\Py thon\\python.exe" "--voice" "a&calc""')
with self.assertRaises(ValueError):
_launch_in_console(['x"y'], cwd=".")
class VoiceImportTests(unittest.TestCase):
"""VOI-01: pickle checkpoints are not imported by default."""
def test_checkpoints_skipped(self) -> None:
from javis.voices.io import import_profile
with tempfile.TemporaryDirectory() as d:
arc = Path(d) / "v.javisvoice"
with zipfile.ZipFile(arc, "w") as zf:
zf.writestr("MANIFEST.json", json.dumps({"kind": "javis.voice_profile", "name": "v"}))
zf.writestr("profile.json", "{}")
zf.writestr("refs/a.wav", b"RIFF")
zf.writestr("training/piper_lightning/last.ckpt", b"\x80\x04evil")
zf.writestr("training/base_checkpoint.txt", "C:/x.ckpt")
res = import_profile(arc, Path(d) / "voices")
self.assertTrue((res.path / "refs" / "a.wav").exists())
self.assertFalse((res.path / "training" / "piper_lightning" / "last.ckpt").exists())
self.assertFalse((res.path / "training" / "base_checkpoint.txt").exists())
class VoiceExportConfigTests(unittest.TestCase):
"""An exported model needs its .onnx.json even after piper_config.json was deleted."""
def test_voice_config_rebuilt_from_training_run(self) -> None:
try:
from piper.config import PiperConfig
except ImportError:
self.skipTest("piper-tts not installed")
from javis.voices.piper_train import _rebuild_voice_config
with tempfile.TemporaryDirectory() as d:
run = Path(d) / "lightning_logs" / "version_0"
run.mkdir(parents=True)
(run / "config.yaml").write_text(
"data:\n espeak_voice: de\n sample_rate: 22050\n num_symbols: 256\n"
" phoneme_type: null\n phonemes_path: null\n vowel_clusters: null\n",
encoding="utf-8")
dest = Path(d) / "piper_model.onnx.json"
self.assertTrue(_rebuild_voice_config(Path(d), dest))
cfg = PiperConfig.from_dict(json.loads(dest.read_text(encoding="utf-8")))
self.assertEqual((cfg.espeak_voice, cfg.sample_rate, cfg.num_symbols), ("de", 22050, 256))
self.assertIn("a", cfg.phoneme_id_map)
(run / "config.yaml").write_text(
"data:\n espeak_voice: de\n phonemes_path: custom.json\n", encoding="utf-8")
self.assertFalse(_rebuild_voice_config(Path(d), dest))
class ConfigValidationTests(unittest.TestCase):
def test_sample_rate_and_threshold_are_validated(self) -> None:
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "config.yaml"
p.write_text(
f"paths: {{base_dir: '{Path(d).as_posix()}'}}\n"
"audio: {sample_rate: 48000}\nspeaker_id: {threshold: 3}\n",
encoding="utf-8",
)
cfg = load_config(p)
self.assertEqual(cfg.audio.sample_rate, 16000)
self.assertEqual(cfg.speaker_id.threshold, 0.65)
def test_non_mapping_root_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "config.yaml"
p.write_text("- just\n- a list\n", encoding="utf-8")
with self.assertRaises(ValueError):
load_config(p)
class ApiBodyLimitTests(unittest.TestCase):
"""API-01: oversized / unsized bodies are rejected before parsing."""
def test_body_size_guard(self) -> None:
from fastapi.testclient import TestClient
from javis.api.devices import SlaveDeviceStore
from javis.api.pairing import PairingCodeManager
from javis.api.server import build_app
with tempfile.TemporaryDirectory() as d:
cfg = Config(paths=PathsConfig(base_dir=Path(d)))
cfg.api.max_upload_bytes = 1000
app = build_app(cfg, orchestrator=None, device_store=SlaveDeviceStore(Path(d) / "s.json"),
pairing_mgr=PairingCodeManager())
client = TestClient(app)
big = client.post("/api/v1/turn", content=b"x" * 200_000,
headers={"content-type": "application/octet-stream"})
self.assertEqual(big.status_code, 413)
unauth = client.post("/api/v1/turn", files={"audio": ("a.wav", b"RIFF", "audio/wav")})
self.assertEqual(unauth.status_code, 401)
self.assertEqual(client.get("/api/v1/health").status_code, 200)
if __name__ == "__main__":
unittest.main()
+61
View File
@@ -0,0 +1,61 @@
"""Tests for duckduckgo-mcp (run with duckduckgo-mcp/.venv: needs mcp 1.x FastMCP).
duckduckgo-mcp\\.venv\\Scripts\\python.exe -m pytest tests/test_duckduckgo_mcp.py -q
"""
from __future__ import annotations
import asyncio
import threading
import pytest
pytest.importorskip("mcp.server.fastmcp")
pytest.importorskip("duckduckgo_mcp")
from mcp.server.fastmcp.exceptions import ToolError # noqa: E402
from duckduckgo_mcp import server as srv # noqa: E402
class FakeDDGS:
def __init__(self, fail: Exception | None = None):
self.fail = fail
self.threads: list[str] = []
def text(self, query, **kwargs):
self.threads.append(threading.current_thread().name)
if self.fail:
raise self.fail
return [{"title": "T", "href": "https://example.org", "body": "B"}]
def news(self, query, **kwargs):
return self.text(query, **kwargs)
def test_search_runs_off_the_event_loop(monkeypatch):
fake = FakeDDGS()
monkeypatch.setattr(srv, "_get_ddgs", lambda: fake)
out = asyncio.run(srv.mcp.call_tool("ddg_search", {"query": "python"}))
assert "example.org" in str(out)
assert fake.threads and fake.threads[0] != threading.main_thread().name
def test_failures_are_tool_errors(monkeypatch):
monkeypatch.setattr(srv, "_get_ddgs", lambda: FakeDDGS(RuntimeError("Ratelimit")))
with pytest.raises(ToolError, match="Ratelimit"):
asyncio.run(srv.mcp.call_tool("ddg_search", {"query": "python"}))
with pytest.raises(ToolError, match="Ratelimit"):
asyncio.run(srv.mcp.call_tool("ddg_answers", {"query": "python"}))
def test_input_validation(monkeypatch):
monkeypatch.setattr(srv, "_get_ddgs", lambda: FakeDDGS())
with pytest.raises(ToolError, match="empty"):
asyncio.run(srv.mcp.call_tool("ddg_news", {"query": " "}))
with pytest.raises(ToolError, match="safesearch"):
asyncio.run(srv.mcp.call_tool("ddg_search", {"query": "x", "safesearch": "strict"}))
def test_tool_annotations():
for t in asyncio.run(srv.mcp.list_tools()):
assert t.annotations.readOnlyHint is True and t.annotations.openWorldHint is True, t.name
+126
View File
@@ -0,0 +1,126 @@
"""Tests for emby-mcp (run with emby-mcp/.venv: needs mcp 1.x FastMCP).
emby-mcp\\.venv\\Scripts\\python.exe -m pytest tests/test_emby_mcp.py -q
"""
from __future__ import annotations
import asyncio
import types
import pytest
pytest.importorskip("mcp.server.fastmcp")
pytest.importorskip("emby_mcp")
import httpx # noqa: E402
from emby_mcp import server as srv # noqa: E402
from emby_mcp.client import EmbyClient # noqa: E402
from emby_mcp.config import Settings # noqa: E402
SESSIONS = [
{"Id": "s-tv", "DeviceName": "Living Room TV", "Client": "Emby Theater", "SupportsRemoteControl": True},
{"Id": "s-srv", "DeviceName": "Tracearr Server", "Client": "Tracearr", "SupportsRemoteControl": False},
]
def _client(handler) -> tuple[EmbyClient, list[httpx.Request]]:
seen: list[httpx.Request] = []
def _record(request: httpx.Request) -> httpx.Response:
seen.append(request)
return handler(request)
client = EmbyClient(Settings(host="emby.local:8096", api_key="k"))
client._http = httpx.AsyncClient(base_url=client.settings.base_url,
transport=httpx.MockTransport(_record))
return client, seen
def _ctx(client: EmbyClient):
return types.SimpleNamespace(
request_context=types.SimpleNamespace(lifespan_context=srv.AppContext(emby=client)))
def test_base_url_defaults_to_http_and_accepts_ports_and_urls():
assert Settings(host="10.0.0.5:9096", api_key="k").base_url == "http://10.0.0.5:9096"
assert Settings(host="10.0.0.5", port=8096, api_key="k").base_url == "http://10.0.0.5:8096"
assert Settings(host="10.0.0.5:9096", port=443, api_key="k").base_url == "http://10.0.0.5:9096"
assert Settings(host="https://emby.example.org/", api_key="k").base_url == "https://emby.example.org"
assert Settings(host="emby.example.org", scheme="https", api_key="k").base_url == "https://emby.example.org"
def test_ids_cannot_reach_other_endpoints():
client, seen = _client(lambda r: httpx.Response(200, json={}))
async def run():
await client.get_user("../System/Restart")
await client.get_playback_info("123/Delete")
await client.play_on_session("s/../../System/Shutdown", ["1"])
await client.aclose()
asyncio.run(run())
paths = [r.url.raw_path.decode().split("?")[0] for r in seen]
assert paths == ["/Users/..%2FSystem%2FRestart", "/Items/123%2FDelete/PlaybackInfo",
"/Sessions/s%2F..%2F..%2FSystem%2FShutdown/Playing"]
def test_search_is_server_side():
client, seen = _client(lambda r: httpx.Response(200, json={"Items": [
{"Id": "1", "Name": "Stranger Things", "Type": "Series", "ProductionYear": 2016,
"RunTimeTicks": 30_000_000_000}]}))
hits = asyncio.run(srv.search_items(_ctx(client), "Stranger Things"))
params = seen[0].url.params
assert params["SearchTerm"] == "Stranger Things"
assert "Movie" in params["IncludeItemTypes"] and "Person" not in params["IncludeItemTypes"]
assert hits[0].name == "Stranger Things" and hits[0].year == 2016 and hits[0].runtime_minutes == 50
def test_get_item_uses_items_query():
client, seen = _client(lambda r: httpx.Response(200, json={"Items": [{"Id": "42", "Name": "X"}]}))
item = asyncio.run(srv.get_item(_ctx(client), "42"))
assert item["Name"] == "X"
assert seen[0].url.path == "/Items" and seen[0].url.params["Ids"] == "42"
empty, _ = _client(lambda r: httpx.Response(200, json={"Items": []}))
with pytest.raises(LookupError):
asyncio.run(srv.get_item(_ctx(empty), "nope"))
def test_play_item_targets_a_controllable_session():
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/Sessions":
return httpx.Response(200, json=SESSIONS)
return httpx.Response(204)
client, seen = _client(handler)
result = asyncio.run(srv.play_item(_ctx(client), "42"))
assert result["session_id"] == "s-tv"
play = seen[-1]
assert play.method == "POST" and play.url.path == "/Sessions/s-tv/Playing"
assert play.url.params["ItemIds"] == "42" and play.url.params["PlayCommand"] == "PlayNow"
with pytest.raises(ValueError, match="No matching"):
asyncio.run(srv.play_item(_ctx(client), "42", device="Kitchen"))
with pytest.raises(ValueError, match="No matching"):
asyncio.run(srv.play_item(_ctx(client), "42", session_id="s-srv"))
def test_list_devices_unwraps_items():
client, _ = _client(lambda r: httpx.Response(200, json={"Items": [{"Id": "d", "Name": "Phone"}]}))
assert asyncio.run(srv.list_devices(_ctx(client))) == [
{"id": "d", "name": "Phone", "app": None, "last_user": None, "last_active": None}]
def test_playback_url_has_no_credentials():
client, _ = _client(lambda r: httpx.Response(200, json={"Items": [
{"Id": "7", "Name": "Song", "MediaType": "Audio"}]}))
out = asyncio.run(srv.playback_url(_ctx(client), "7"))
assert out["url"] == "http://emby.local:8096/Audio/7/stream?static=true"
assert "api_key" not in out["url"] and "k" not in out["url"].split("/")[-1]
def test_tool_annotations():
tools = {t.name: t for t in asyncio.run(srv.mcp.list_tools())}
assert tools["play_item"].annotations.readOnlyHint is False
for name in ("search_items", "get_item", "list_sessions", "playback_url"):
assert tools[name].annotations.readOnlyHint is True, name
+158
View File
@@ -0,0 +1,158 @@
"""Tests for homey-mcp (run with homey-mcp/.venv: needs mcp 1.x FastMCP).
homey-mcp\\.venv\\Scripts\\python.exe -m pytest tests/test_homey_mcp.py -q
"""
from __future__ import annotations
import asyncio
import types
import pytest
pytest.importorskip("mcp.server.fastmcp")
pytest.importorskip("homey_mcp")
import httpx # noqa: E402
from starlette.testclient import TestClient # noqa: E402
from homey_mcp import server as srv # noqa: E402
from homey_mcp.config import Settings # noqa: E402
from homey_mcp.homey_client import HomeyClient, event_entry # noqa: E402
ZONES = {
"z1": {"id": "z1", "name": "Wohnzimmer Ecke"},
"z2": {"id": "z2", "name": "Wohnzimmer"},
"z3": {"id": "z3", "name": "Esszimmer"},
}
DEVICES = {
"d1": {"id": "d1", "name": "Deckenlicht", "zone": "z3", "class": "light",
"capabilities": ["onoff", "dim"]},
"d2": {"id": "d2", "name": "Licht Fenster", "zone": "z2", "class": "light",
"capabilities": ["onoff"]},
"d3": {"id": "d3", "name": "Heizung", "zone": "z3", "class": "thermostat",
"capabilities": ["target_temperature"]},
}
def _client(handler) -> tuple[HomeyClient, list[httpx.Request]]:
seen: list[httpx.Request] = []
def _record(request: httpx.Request) -> httpx.Response:
seen.append(request)
return handler(request)
client = HomeyClient(Settings(host="homey.local", token="t", realtime=False))
client._http = httpx.AsyncClient(base_url=client.settings.base_url,
transport=httpx.MockTransport(_record))
return client, seen
def _ctx(client: HomeyClient):
return types.SimpleNamespace(
request_context=types.SimpleNamespace(lifespan_context=srv.AppContext(homey=client)))
def _api(request: httpx.Request) -> httpx.Response:
if request.url.path == "/api/manager/zones/zone":
return httpx.Response(200, json=ZONES)
if request.url.path == "/api/manager/devices/device":
return httpx.Response(200, json=DEVICES)
return httpx.Response(200, json={})
def test_ids_cannot_escape_their_path_segment():
client, seen = _client(lambda r: httpx.Response(200, json={}))
async def run():
await client.trigger_flow("../../system/reboot#")
await client.set_capability("x/../../../system/reboot?", "onoff", True)
await client.aclose()
asyncio.run(run())
paths = [r.url.raw_path.decode() for r in seen]
assert paths[0] == "/api/manager/flow/flow/..%2F..%2Fsystem%2Freboot%23/trigger"
assert paths[1].startswith("/api/manager/devices/device/x%2F..%2F")
assert all("/system/reboot" not in p for p in paths)
def test_zone_resolution_prefers_exact_match():
assert srv._resolve_zone(ZONES, "wohnzimmer") == ("z2", "Wohnzimmer")
assert srv._resolve_zone(ZONES, "Wohnzimmer E") == ("z1", "Wohnzimmer Ecke")
assert srv._resolve_zone(ZONES, "ess") == ("z3", "Esszimmer")
assert srv._resolve_zone(ZONES, "zimmer") == ("z3", "Esszimmer") # shortest substring hit
assert srv._resolve_zone(ZONES, "Bad") is None
def test_find_devices_requires_all_words_first():
client, _ = _client(_api)
hits = asyncio.run(srv.find_devices(_ctx(client), "esszimmer licht"))
assert [d.id for d in hits] == ["d1"] # not every light, not every Esszimmer device
fallback = asyncio.run(srv.find_devices(_ctx(client), "licht garage"))
assert {d.id for d in fallback} == {"d1", "d2"}
def test_control_zone_uses_resolved_zone_only():
puts: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "PUT":
puts.append(request.url.path)
return httpx.Response(200, json={})
return _api(request)
client, _ = _client(handler)
result = asyncio.run(srv.control_zone(_ctx(client), "Wohnzimmer", "onoff", "off"))
assert result["zone"] == "Wohnzimmer" and result["devices"] == ["Licht Fenster"]
assert puts == ["/api/manager/devices/device/d2/capability/onoff"]
def test_control_all_refuses_unscoped_capability():
client, _ = _client(_api)
with pytest.raises(ValueError, match="Refusing"):
asyncio.run(srv.control_all(_ctx(client), "target_temperature", 30))
def test_event_entry_is_compact():
e = event_entry("homey:device:abc", "capability", {"capabilityId": "onoff", "value": True})
assert e["kind"] == "capability" and e["device_id"] == "abc"
big = {"id": "d1", "name": "Lamp", "zone": "z1", "available": True, "capabilitiesObj": {"x": 1}}
assert event_entry("homey:manager:devices", "device.update", big)["data"] == {
"id": "d1", "name": "Lamp", "zone": "z1", "available": True}
def test_bearer_mode_protects_every_mcp_route():
app = srv.build_lan_app("sse", token="s3cret", auth_mode="bearer", allowed_hosts=None)
c = TestClient(app)
assert c.get("/").status_code == 200 # discovery stays public
assert c.get("/sse").status_code == 401 # was unauthenticated before
assert c.post("/messages/?session_id=x", json={}).status_code == 401
assert c.get("/api/zones").status_code == 401
ok = c.post("/messages/?session_id=x", json={}, headers={"Authorization": "Bearer s3cret"})
assert ok.status_code != 401
wrong = c.post("/messages/?session_id=x", json={},
headers={"Authorization": "Bearer nöpe".encode()})
assert wrong.status_code == 401 # non-ASCII token no longer crashes compare_digest
http_app = srv.build_lan_app("streamable-http", token="s3cret", auth_mode="bearer",
allowed_hosts=None)
assert TestClient(http_app).post("/", json={}).status_code == 401
def test_host_guard_blocks_dns_rebinding():
hosts = srv._allowed_hosts("10.0.0.5", "")
app = srv.build_lan_app("streamable-http", token="", auth_mode="none", allowed_hosts=hosts)
c = TestClient(app)
assert c.get("/", headers={"Host": "evil.example:8080"}).status_code == 421
assert c.get("/", headers={"Host": "10.0.0.5:8080"}).status_code == 200
assert c.get("/", headers={"Host": "localhost:8080"}).status_code == 200
assert srv._allowed_hosts("0.0.0.0", "") is None
assert srv._allowed_hosts("0.0.0.0", "homey.lan") >= {"homey.lan", "localhost"}
assert srv._allowed_hosts("10.0.0.5", "*") is None
def test_tool_annotations():
tools = {t.name: t for t in asyncio.run(srv.mcp.list_tools())}
for name in ("control_all", "control_zone", "trigger_flow"):
assert tools[name].annotations.destructiveHint is True, name
for name in ("list_devices", "find_devices", "get_zone_status", "list_recent_events"):
assert tools[name].annotations.readOnlyHint is True, name
+280
View File
@@ -0,0 +1,280 @@
"""Regression tests for the javis-slave audit fixes (docs/AUDIT_SLAVE.md)."""
from __future__ import annotations
import asyncio
import json
import sys
import time
from pathlib import Path
import httpx
import numpy as np
import pytest
SLAVE_SRC = Path(__file__).resolve().parents[1] / "javis-slave" / "src"
if str(SLAVE_SRC) not in sys.path:
sys.path.insert(0, str(SLAVE_SRC))
from javis_slave import config as slave_config # noqa: E402
from javis_slave.api_client import MasterClient, MasterError # noqa: E402
from javis_slave.config import SlaveConfig, ensure_control_token # noqa: E402
TOKEN = "t" * 32
@pytest.fixture
def cfg_dir(tmp_path, monkeypatch):
monkeypatch.setenv("JAVIS_SLAVE_CONFIG_DIR", str(tmp_path))
return tmp_path
# ── SLV-07: config loading ───────────────────────────────────────────────────
def test_corrupt_config_is_kept_aside(cfg_dir):
(cfg_dir / "slave_config.json").write_text("{not json", encoding="utf-8")
cfg = slave_config.load_config()
assert not cfg.paired
assert (cfg_dir / "slave_config.json.corrupt").read_text(encoding="utf-8") == "{not json"
assert not (cfg_dir / "slave_config.json").exists()
def test_config_type_mismatch_keeps_default(cfg_dir):
(cfg_dir / "slave_config.json").write_text(
json.dumps({"master_port": "abc", "input_gain": 2, "api_key": "k", "master_host": "h"}),
encoding="utf-8")
cfg = slave_config.load_config()
assert cfg.master_port == 8765
assert cfg.input_gain == 2.0 and isinstance(cfg.input_gain, float)
assert cfg.paired
def test_control_token_roundtrip(cfg_dir):
cfg = SlaveConfig()
assert ensure_control_token(cfg) is True
assert len(cfg.control_token) >= 24
assert ensure_control_token(cfg) is False
slave_config.save_config(cfg)
assert slave_config.load_config().control_token == cfg.control_token
# ── SLV-01: control API authentication ───────────────────────────────────────
def _app(token: str = TOKEN):
from javis_slave.http_server import build_http_app
cfg = SlaveConfig(control_token=token)
return build_http_app(cfg, lambda: None, lambda: "idle", lambda: None, lambda: (0.0, 0.0))
def test_control_api_requires_token_from_lan():
from fastapi.testclient import TestClient
remote = TestClient(_app(), client=("192.168.1.50", 50000))
assert remote.get("/mcp/v1/health").status_code == 200
assert remote.get("/mcp/v1/status").status_code == 401
# Rejected by the middleware before the handler runs: nothing is executed.
assert remote.post("/mcp/v1/power/cancel").status_code == 401
assert remote.get("/mcp/v1/status", headers={"Authorization": "Bearer wrong"}).status_code == 401
# The loopback header is not a credential for remote callers.
assert remote.get("/mcp/v1/status", headers={"X-Javis-Control": "1"}).status_code == 401
ok = remote.get("/mcp/v1/status", headers={"Authorization": f"Bearer {TOKEN}"})
assert ok.status_code == 200 and ok.json()["audio_state"] == "idle"
assert remote.get("/docs").status_code in (401, 404)
def test_control_api_loopback_needs_custom_header():
from fastapi.testclient import TestClient
for host in ("127.0.0.1", "::1", "::ffff:127.0.0.1"):
local = TestClient(_app(), client=(host, 50000))
# Plain loopback request (e.g. a browser form post / CSRF) is refused.
assert local.get("/mcp/v1/status").status_code == 401, host
assert local.get("/mcp/v1/status", headers={"X-Javis-Control": "1"}).status_code == 200, host
def test_empty_token_never_authorizes():
from fastapi.testclient import TestClient
remote = TestClient(_app(token=""), client=("10.0.0.2", 50000))
assert remote.get("/mcp/v1/status", headers={"Authorization": "Bearer "}).status_code == 401
# ── SLV-03: no blind retry of a non-idempotent turn after a read timeout ────
def _client_with(handler) -> MasterClient:
cfg = SlaveConfig(master_host="master.invalid", api_key="k")
client = MasterClient(cfg)
client._backoff_base = 0.0
client._client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
return client
def test_send_turn_does_not_retry_after_read_timeout():
calls = []
def handler(request):
calls.append(request.url.path)
raise httpx.ReadTimeout("slow", request=request)
async def run():
client = _client_with(handler)
try:
with pytest.raises(MasterError, match="Zeitüberschreitung"):
await client.send_turn(b"RIFF")
finally:
await client.aclose()
asyncio.run(run())
assert calls == ["/api/v1/turn"]
def test_send_turn_retries_connect_error_once():
calls = []
def handler(request):
calls.append(1)
raise httpx.ConnectError("refused", request=request)
async def run():
client = _client_with(handler)
try:
with pytest.raises(MasterError):
await client.send_turn(b"RIFF")
finally:
await client.aclose()
asyncio.run(run())
assert len(calls) == 2
# ── SLV-04 / SLV-06: audio engine ────────────────────────────────────────────
def test_engine_start_failure_releases_mic(monkeypatch):
from javis_slave import audio_engine
class FakeMic:
instances: list = []
def __init__(self, **kw):
self.stopped = False
FakeMic.instances.append(self)
async def start(self):
pass
async def stop(self):
self.stopped = True
def broken_vad(**kw):
raise RuntimeError("vad model missing")
monkeypatch.setattr(audio_engine, "MicStream", FakeMic)
monkeypatch.setattr(audio_engine, "make_vad", broken_vad)
async def run():
eng = audio_engine.AudioEngine(SlaveConfig(), client=None)
with pytest.raises(RuntimeError, match="vad model missing"):
await eng.start()
return eng
eng = asyncio.run(run())
assert FakeMic.instances and FakeMic.instances[0].stopped
assert eng.mic is None
@pytest.mark.parametrize("name", ["", ".", "..", "../evil", "sub/model", "..\\evil", "C:\\x\\model"])
def test_wake_model_name_cannot_address_a_path(name):
from javis_slave.audio_engine import AudioEngine
class NoDownload:
async def download_wake_model(self, dest): # pragma: no cover - must not be reached
raise AssertionError(f"download attempted for {dest}")
async def run():
eng = AudioEngine(SlaveConfig(), client=NoDownload())
with pytest.raises(RuntimeError, match="invalid wake word model name"):
await eng._load_wake(name, 0.5)
asyncio.run(run())
# ── SLV-05: playback writer stops before cancellation completes ─────────────
def test_write_pcm_blocks_cancel_stops_writer_thread():
from javis_slave.audio_io.stream import write_pcm_blocks
class SlowStream:
writes = 0
def write(self, block):
time.sleep(0.01)
SlowStream.writes += 1
async def run():
task = asyncio.ensure_future(
write_pcm_blocks(SlowStream(), np.zeros(16000 * 5, dtype=np.float32), 160))
await asyncio.sleep(0.05)
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
done = SlowStream.writes
await asyncio.sleep(0.05)
return done
done = asyncio.run(run())
assert 0 < done < 500
assert SlowStream.writes == done # no write after the stream may be closed
def test_write_pcm_blocks_cancel_event():
from javis_slave.audio_io.stream import write_pcm_blocks
class Stream:
def write(self, block):
pass
async def run():
ev = asyncio.Event()
ev.set()
return await write_pcm_blocks(Stream(), np.zeros(1600, dtype=np.float32), 160, ev)
assert asyncio.run(run()) is True
# ── SLV-02 / SLV-09: Windows system control ─────────────────────────────────
def test_volume_controller_uses_current_pycaw_api(monkeypatch):
import types
from javis_slave import system_control
class Endpoint:
level = 0.25
def GetMasterVolumeLevelScalar(self):
return Endpoint.level
def SetMasterVolumeLevelScalar(self, level, ctx):
Endpoint.level = level
def GetMute(self):
return 1
class Device: # pycaw >= 2024 AudioDevice: has .EndpointVolume, no .Activate
EndpointVolume = Endpoint()
fake = types.ModuleType("pycaw.pycaw")
fake.AudioUtilities = types.SimpleNamespace(GetSpeakers=lambda: Device())
fake.IAudioEndpointVolume = object
monkeypatch.setitem(sys.modules, "pycaw", types.ModuleType("pycaw"))
monkeypatch.setitem(sys.modules, "pycaw.pycaw", fake)
monkeypatch.setattr(system_control.platform, "system", lambda: "Windows")
vol = system_control.VolumeController()
assert vol.get_volume() == 0.25
assert vol.set_volume(1.7) and Endpoint.level == 1.0
assert vol.is_muted() is True
@pytest.mark.skipif(sys.platform != "win32", reason="Windows exit-code handling")
def test_windows_power_runner_checks_exit_codes():
from javis_slave.system_control import _run_win
assert _run_win(["cmd", "/c", "exit 0"]) is True
assert _run_win(["cmd", "/c", "exit 5"]) is False
assert _run_win(["cmd", "/c", "exit 1116"], ok_codes=(0, 1116)) is True
+93
View File
@@ -0,0 +1,93 @@
"""Tests for javis-slave-mcp (needs mcp 1.x with FastMCP — run with javis-slave-mcp/.venv).
javis-slave-mcp\\.venv\\Scripts\\python.exe -m pytest tests/test_slave_mcp.py -q
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
import pytest
pytest.importorskip("mcp.server.fastmcp")
import httpx # noqa: E402
MCP_SRC = Path(__file__).resolve().parents[1] / "javis-slave-mcp" / "src"
if str(MCP_SRC) not in sys.path:
sys.path.insert(0, str(MCP_SRC))
from mcp.server.fastmcp.exceptions import ToolError # noqa: E402
from javis_slave_mcp import server as srv # noqa: E402
REAL_ID = "3f6c1a7e-0000-4000-8000-000000000001"
def test_env_parsing_skips_bad_entries():
slaves = srv._parse_env_devices(
" a = 10.0.0.1:8766 , bad, b=tok-123@host:9000, c=host2,,d=:1", "default-tok")
assert set(slaves) == {"a", "b", "c"}
assert (slaves["a"].host, slaves["a"].port, slaves["a"].token) == ("10.0.0.1", 8766, "default-tok")
assert (slaves["b"].host, slaves["b"].port, slaves["b"].token) == ("host", 9000, "tok-123")
assert (slaves["c"].host, slaves["c"].port) == ("host2", srv.DEFAULT_PORT)
assert "tok-123" not in repr(slaves["b"])
def _install(handler, token: str = "") -> list[httpx.Request]:
seen: list[httpx.Request] = []
def _record(request: httpx.Request) -> httpx.Response:
seen.append(request)
return handler(request)
client = srv.SlaveClient(device_id="localhost", host="127.0.0.1", token=token)
client._http = httpx.AsyncClient(transport=httpx.MockTransport(_record),
base_url=client.base_url(), headers=client.headers())
srv._slaves = {"localhost": client}
return seen
def _status_json() -> dict:
return {"device_id": REAL_ID, "device_name": "Kueche", "paired": True,
"engine_running": True, "audio_state": "idle"}
def test_tools_run_inside_the_server_loop_and_resolve_real_id():
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/mcp/v1/status":
return httpx.Response(200, json=_status_json())
if request.url.path == f"/mcp/v1/master/slaves/{REAL_ID}/ip":
return httpx.Response(200, json={"device_id": REAL_ID, "ip_address": "10.0.0.7"})
return httpx.Response(404, json={"detail": "nope"})
seen = _install(handler, token="secret-token")
# FastMCP awaits the tool on its running loop — the old asyncio.run() failed here.
result = asyncio.run(srv.mcp.call_tool("slave_get_ip_address", {"device_id": "localhost"}))
assert "10.0.0.7" in str(result)
assert [r.url.path for r in seen] == ["/mcp/v1/status", f"/mcp/v1/master/slaves/{REAL_ID}/ip"]
assert all(r.headers["X-Javis-Control"] == "1" for r in seen)
assert all(r.headers["Authorization"] == "Bearer secret-token" for r in seen)
def test_unauthorized_becomes_tool_error():
_install(lambda request: httpx.Response(401, json={"detail": "unauthorized"}))
with pytest.raises(ToolError, match="control token"):
asyncio.run(srv.mcp.call_tool("slave_get_status", {"device_id": "localhost"}))
def test_unknown_device_is_tool_error():
_install(lambda request: httpx.Response(200, json=_status_json()))
with pytest.raises(ToolError, match="not found"):
asyncio.run(srv.mcp.call_tool("slave_get_status", {"device_id": "nope"}))
def test_tool_annotations():
tools = {t.name: t for t in asyncio.run(srv.mcp.list_tools())}
for name in ("slave_shutdown", "slave_restart_system", "slave_sleep"):
assert tools[name].annotations.destructiveHint is True, name
for name in ("slave_get_status", "slave_get_volume", "slave_list_devices"):
assert tools[name].annotations.readOnlyHint is True, name
assert tools["slave_set_volume"].annotations.idempotentHint is True
assert tools["slave_increase_volume"].annotations.idempotentHint is False
+64
View File
@@ -0,0 +1,64 @@
"""Design-token checks for src/javis/gui/theme.py (WCAG 2.2 AA contrast)."""
from __future__ import annotations
import re
import pytest
from javis.gui import theme
# (foreground, background) pairs rendered as text: need 4.5:1 (SC 1.4.3).
TEXT_PAIRS = [
("text", "bg"), ("text", "surface"), ("text", "surface2"), ("text", "input"),
("muted", "bg"), ("muted", "surface"), ("muted", "surface2"),
("accent", "bg"), ("accent", "surface"),
("on_accent", "primary"), ("on_accent", "primary_hover"),
("success", "bg"), ("error", "bg"), ("error", "error_bg"), ("warn_fg", "bg"),
("tool_fg", "tool_bg"), ("tool_muted", "tool_bg"), ("code_fg", "code_bg"),
("link", "bg"),
]
# Control boundaries and focus indicators: need 3:1 (SC 1.4.11).
UI_PAIRS = [
("stroke_strong", "bg"), ("stroke_strong", "surface"),
("focus", "bg"), ("focus", "surface"), ("primary", "bg"), ("on_accent", "primary"),
]
@pytest.mark.parametrize("name", sorted(theme.THEMES))
@pytest.mark.parametrize("fg,bg", TEXT_PAIRS)
def test_text_contrast(name, fg, bg):
t = theme.THEMES[name]
assert theme.contrast_ratio(t[fg], t[bg]) >= 4.5, f"{name}: {fg} on {bg}"
@pytest.mark.parametrize("name", sorted(theme.THEMES))
@pytest.mark.parametrize("fg,bg", UI_PAIRS)
def test_ui_contrast(name, fg, bg):
t = theme.THEMES[name]
assert theme.contrast_ratio(t[fg], t[bg]) >= 3.0, f"{name}: {fg} vs {bg}"
def test_palettes_share_keys():
assert set(theme.PAL_DARK) == set(theme.PAL_LIGHT)
def test_accent_hue_kept():
# The redesign must keep the brand colours of the dark theme.
assert theme.PAL_DARK["accent"].lower() == "#da7756"
assert theme.PAL_DARK["bg"] == "#1e1e1e"
def test_contrast_math():
assert theme.contrast_ratio("#000000", "#ffffff") == pytest.approx(21.0)
assert theme.readable_on("#DA7756") == "#1e1e1e"
assert theme.readable_on("#3c3c3c") == "#ffffff"
@pytest.mark.parametrize("name", sorted(theme.THEMES))
def test_qss_is_well_formed(name):
qss = theme.build_qss(theme.THEMES[name], "Segoe UI")
assert qss.count("{") == qss.count("}")
# every palette value used in the sheet is a full hex colour
for hex_color in re.findall(r"#[0-9a-fA-F]+\b", qss):
assert len(hex_color) in (4, 7), hex_color
assert "variant=\"primary\"" in qss
+178
View File
@@ -0,0 +1,178 @@
"""Voice training pipeline: warm start/resume, piper compatibility fixes, data filters."""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
HIGH_ARCH = {
"resblock": "1", "resblock_kernel_sizes": (3, 7, 11),
"resblock_dilation_sizes": ((1, 3, 5), (1, 3, 5), (1, 3, 5)),
"upsample_rates": (8, 8, 2, 2), "upsample_initial_channel": 512,
"upsample_kernel_sizes": (16, 16, 4, 4),
}
def _voice_dir(root: Path) -> Path:
training = root / "v" / "training"
(training / "wavs").mkdir(parents=True)
(training / "metadata.csv").write_text("clip_00000|hallo|hallo\n", encoding="utf-8")
return root / "v"
class TrainingStartTests(unittest.TestCase):
def setUp(self) -> None:
try:
import torch # noqa: F401
except ImportError:
self.skipTest("torch not installed")
from javis.voices import piper_train
self.pt = piper_train
self.cmds: list[list[str]] = []
def fake_run(cmd, **kw):
self.cmds.append(cmd)
return 1 # stop after stage 1
patches = [patch.object(piper_train, "_run", fake_run),
patch.object(piper_train, "MIN_FREE_BYTES", 0)]
for p in patches:
p.start()
self.addCleanup(p.stop)
def _ckpt(self, path: Path, **hp) -> Path:
import torch
path.parent.mkdir(parents=True, exist_ok=True)
torch.save({"state_dict": {"w": torch.zeros(1)}, "hyper_parameters": hp}, path)
return path
def test_first_run_warm_starts_from_base_with_its_architecture(self) -> None:
import yaml
with tempfile.TemporaryDirectory() as d:
voice = _voice_dir(Path(d))
self._ckpt(voice / "training" / "piper_base" / "epoch=2665-step=1.ckpt",
dataset=[Path("x")], **HIGH_ARCH)
self.pt.run_pipeline(voice)
cmd = self.cmds[0]
self.assertIn("javis.voices.piper_compat", cmd)
self.assertIn("--model.warmstart_ckpt", cmd)
self.assertNotIn("--ckpt_path", cmd)
arch = yaml.safe_load(Path(cmd[cmd.index("--config") + 1]).read_text(encoding="utf-8"))
self.assertEqual(arch["model"]["resblock"], "1")
self.assertEqual(arch["model"]["upsample_rates"], [8, 8, 2, 2])
self.assertFalse((voice / "training" / "warmstart.pt").exists()) # removed after fit
def test_later_runs_resume_own_checkpoint_and_ignore_base(self) -> None:
with tempfile.TemporaryDirectory() as d:
voice = _voice_dir(Path(d))
base = self._ckpt(voice / "training" / "piper_base" / "epoch=1.ckpt", **HIGH_ARCH)
last = self._ckpt(voice / "training" / "piper_lightning" / "lightning_logs"
/ "version_0" / "checkpoints" / "last.ckpt", **HIGH_ARCH)
self.pt.run_pipeline(voice, base_checkpoint=base)
cmd = self.cmds[0]
self.assertEqual(cmd[cmd.index("--ckpt_path") + 1], str(last))
self.assertNotIn("--model.warmstart_ckpt", cmd)
def test_linux_checkpoint_with_posixpath_loads(self) -> None:
import pathlib
import torch
class FakePosixPath: # pickled as pathlib.PosixPath, like a Linux checkpoint
def __init__(self, p):
self.p = p
def __reduce__(self):
return (FakePosixPath, (self.p,))
FakePosixPath.__module__ = "pathlib"
FakePosixPath.__name__ = FakePosixPath.__qualname__ = "PosixPath"
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "linux.ckpt"
with patch.object(pathlib, "PosixPath", FakePosixPath):
torch.save({"hyper_parameters": {"dataset": [FakePosixPath("/ssd/x")]}}, p)
ckpt = self.pt._load_checkpoint(p)
self.assertEqual(str(ckpt["hyper_parameters"]["dataset"][0]), "/ssd/x")
class PiperCompatTests(unittest.TestCase):
def test_int_resblock_builds_high_quality_blocks(self) -> None:
try:
import javis.voices.piper_compat # noqa: F401 — applies the fixes
from piper.train.vits import models, modules
from pysilero_vad import SileroVoiceActivityDetector
except ImportError:
self.skipTest("piper training extras not installed")
g = models.Generator(192, 1, (3, 7, 11), ((1, 3, 5),) * 3, (8, 8, 2, 2), 64, (16, 16, 4, 4))
self.assertIsInstance(g.resblocks[0], modules.ResBlock1)
self.assertTrue(hasattr(SileroVoiceActivityDetector, "process_array"))
class TranscriptGateTests(unittest.TestCase):
def test_unreliable_transcripts_are_rejected(self) -> None:
from javis.voices.audiobook_ingest import _transcript_ok
def tr(**kw):
return SimpleNamespace(**{"avg_logprob": -0.2, "no_speech_prob": 0.01,
"compression_ratio": 1.4, **kw})
self.assertTrue(_transcript_ok(tr()))
self.assertTrue(_transcript_ok(tr(avg_logprob=None, no_speech_prob=None,
compression_ratio=None)))
self.assertFalse(_transcript_ok(tr(avg_logprob=-1.3)))
self.assertFalse(_transcript_ok(tr(no_speech_prob=0.9)))
self.assertFalse(_transcript_ok(tr(compression_ratio=3.1)))
class NarratorFilterTests(unittest.TestCase):
def test_dominant_speaker_scores_high(self) -> None:
from javis.voices.dataset_clean import narrator_scores
rng = np.random.default_rng(0)
a, b = np.eye(8)[0], np.eye(8)[1]
embs = np.array([v + rng.normal(0, 0.1, 8) for v in [a] * 70 + [b] * 30])
embs /= np.linalg.norm(embs, axis=1, keepdims=True)
s = narrator_scores(embs)
self.assertGreater(s[:70].min(), 0.8)
self.assertLess(s[70:].max(), 0.4)
def test_clean_dataset_filters_and_keeps_new_imports(self) -> None:
import soundfile as sf
from javis.voices.dataset_clean import clean_dataset
def embed(audio, sr):
# The first sample marks the speaker: 0.5 = narrator, else character voice.
return np.eye(4)[0] if audio[0] > 0.4 else np.eye(4)[1]
with tempfile.TemporaryDirectory() as d:
training = Path(d)
(training / "wavs").mkdir()
def add(stem: str, narrator: bool) -> str:
sf.write(str(training / "wavs" / f"{stem}.wav"),
np.full(100, 0.5 if narrator else 0.1, dtype=np.float32), 22050)
return f"{stem}|text {stem}|text {stem}\n"
lines = [add(f"clip_{i:05d}", narrator=i % 4 != 0) for i in range(8)]
(training / "metadata.csv").write_text("".join(lines), encoding="utf-8")
report = clean_dataset(training, embed, dry_run=True)
self.assertEqual((report["kept"], report["dropped"]), (6, 2))
self.assertEqual(len((training / "metadata.csv").read_text().splitlines()), 8)
clean_dataset(training, embed)
self.assertEqual(len((training / "metadata.csv").read_text().splitlines()), 6)
self.assertEqual(len((training / "metadata.unfiltered.csv").read_text().splitlines()), 8)
# A later import appends to metadata.csv; re-filtering must keep it.
with open(training / "metadata.csv", "a", encoding="utf-8") as f:
f.write(add("clip_00100", narrator=True))
report = clean_dataset(training, embed)
self.assertEqual((report["clips"], report["kept"]), (9, 7))
if __name__ == "__main__":
sys.exit(unittest.main())