"""Smoke test for the Synapse backend's core wiring. Run from nexus-core/ with the Promethean venv active: pytest -q Or the whole gate (tests + frontend lint): bin/check.sh Deliberately tiny (ponytail): it guards the things v1 promises - app wiring, model defaults, playbook ordering, persistence - without needing a running Ollama service or network. Not a full suite. """ from pathlib import Path from fastapi.testclient import TestClient import pytest from synapse.main import app from synapse.memory.store import MemoryItem, PersistentMemoryStore from synapse.nexus_config import DEFAULT_CHAT_MODEL, DEFAULT_MEMORY_MODEL from synapse.ollama_manager import OllamaManager from synapse import ollama_manager from synapse.icons.compositor import _is_allowed_path from synapse.playbook_manager import PlaybookManager from synapse.playbooks.store import PlaybookFileStore, PlaybookItem REPO_ROOT = Path(__file__).resolve().parent.parent def test_app_builds_and_status_responds(): # No `with` → startup event does not run; `ollama` stays the module-level # None and /status handles it. Pure liveness check. Not `/` — since the # single-process change that path serves the built web UI, not JSON. resp = TestClient(app).get("/status") assert resp.status_code == 200 assert resp.json()["status"] == "online" def test_keep_alive_pins_the_model(): # Default keeps the model resident so chats skip cold reloads... assert PersistentMemoryStore._SETTINGS_DEFAULTS["keep_alive"] == "30m" mgr = OllamaManager() assert mgr._apply_keep_alive({"model": "x"}) == {"model": "x", "keep_alive": "30m"} # ...and an empty value omits the field (falls back to Ollama's default). mgr.keep_alive = "" assert "keep_alive" not in mgr._apply_keep_alive({"model": "x"}) def test_auto_model_remap(monkeypatch): import asyncio from synapse import main cfg = {"model": "", "auto_chat_model": "chatX", "auto_code_model": "coderY"} monkeypatch.setattr(main.store, "get_settings", lambda: cfg) # code intent ("function") routes to the code remap; chat intent to the chat remap assert asyncio.run(main._auto_select_model("write a function to sort a list")) == "coderY" assert asyncio.run(main._auto_select_model("how are you today")) == "chatX" # an explicit pin beats the remap cfg["model"] = "pinnedZ" assert asyncio.run(main._auto_select_model("debug this code")) == "pinnedZ" def test_hardware_fit_logic(): from synapse import hardware assert hardware._fit(2.5, 4.0, 16.0) == "gpu" # 2.5+1 <= 4 -> fits GPU assert hardware._fit(4.7, 4.0, 16.0) == "ram" # too big for 4GB GPU, fits RAM assert hardware._fit(4.7, None, 16.0) == "ram" # VRAM unknown -> RAM assert hardware._fit(40.0, 4.0, 16.0) == "no" # too big everywhere rec = hardware.recommend() assert "hardware" in rec and all("fit" in m for m in rec["models"]) def test_stt_status_endpoint(): # Reports whether local Whisper is installed; wiring must respond either way. resp = TestClient(app).get("/stt/status") assert resp.status_code == 200 assert isinstance(resp.json()["available"], bool) def test_num_ctx_option_only_when_positive(): # 0 / None -> omit num_ctx so Ollama uses the model default; positive -> set it. from synapse.ollama_manager import _chat_options assert "num_ctx" not in _chat_options(None, None, 0) assert "num_ctx" not in _chat_options(None, None, None) assert _chat_options(None, None, 8192)["num_ctx"] == 8192 def test_default_models_have_one_source_of_truth(): # The curator default is the config constant, not a copy of it. assert PersistentMemoryStore._SETTINGS_DEFAULTS["memory_model"] == DEFAULT_MEMORY_MODEL # And the Windows installer reads the constants instead of hardcoding a tag, # which is what used to let installer and backend drift onto different models. ps1 = (REPO_ROOT / "install-windows.ps1").read_text(encoding="utf-8") assert "DEFAULT_CHAT_MODEL" in ps1 for hardcoded in (DEFAULT_CHAT_MODEL, DEFAULT_MEMORY_MODEL, "qwen", "llama3.1"): assert hardcoded not in ps1, f"install-windows.ps1 hardcodes {hardcoded!r}" def test_windows_scripts_stay_ascii(): # PowerShell 5.1 decodes BOM-less files as ANSI: one stray Unicode dash # eats a quote and the whole script dies at parse time. cmd.exe is worse # still - it decodes by the console codepage. Globbed rather than listed by # name so a newly added script is covered without editing this test. skip = {"Promethean", "node_modules", ".git", "dist"} ps1s = [p for pat in ("*.ps1", "*.cmd") for p in REPO_ROOT.rglob(pat) if not skip & set(p.parts)] assert ps1s, "no .ps1/.cmd files found - did the Windows path move?" for path in ps1s: raw = path.read_bytes() bad = [(i, b) for i, b in enumerate(raw) if b > 0x7F] assert not bad, f"{path.relative_to(REPO_ROOT)} has non-ASCII bytes at {bad[:3]}" def test_ncp_is_registered_on_path_not_in_a_shell_profile(): """A `function ncp` in a shell profile is invisible to cron, .desktop Exec lines, cmd.exe and Task Scheduler - and on Windows the default Restricted execution policy blocks the profile outright. Both installers must put ncp on PATH; the profile wiring only survives as a no-sudo fallback.""" linux = (REPO_ROOT / "bin" / "restore-linux.sh").read_text(encoding="utf-8") runtime = linux.split('if [ "$stage" = "runtime" ]; then')[1].split("\n exit 0\nfi")[0] assert "/usr/local/bin/ncp" in runtime ps1 = (REPO_ROOT / "install-windows.ps1").read_text(encoding="utf-8") assert "ncp.cmd" in ps1, "installer must register the .cmd shim, not a profile function" # setx truncates PATH at 1024 characters and has permanently broken machines. # Comments stripped so the code that explains the ban does not trip it. code = "\n".join(ln for ln in ps1.splitlines() if not ln.strip().startswith("#")) assert "setx" not in code.lower() assert 'SetEnvironmentVariable("Path"' in code # The model pull is the only multi-GB step and the only one the script # invites a Ctrl+C on -- which in PS 5.1 kills the whole script. Anything # after it is lost, so it has to come last. It used to sit in the middle, # and skipping the download silently skipped the ncp registration too. assert ps1.index("ollama pull") > ps1.index("Registering the ncp command"), \ "model pull must come after ncp registration - a Ctrl+C there aborts the installer" assert ps1.index("ollama pull") > ps1.index("Creating desktop shortcut"), \ "model pull must come after the desktop shortcut" # And it must not TELL anyone to press Ctrl+C: in PS 5.1 that kills the # script, so the advertised way to skip the download was also the way to # abort the install. Skipping is a prompt now. Comments stripped so the # comment explaining this does not trip the check. assert "Ctrl+C" not in code, "installer must not offer Ctrl+C as a skip" def test_no_ps1_shadows_the_ncp_path_shim(): """PowerShell resolves ExternalScript (.ps1) ahead of Application (.cmd), so an ncp.ps1 sitting next to ncp.cmd wins in PowerShell and drags the execution policy back in - the exact thing the .cmd exists to avoid. Observed on the Windows VM: `Get-Command ncp -All` listed ncp.ps1 first, from the same directory the installer had just put on PATH.""" shim = REPO_ROOT / "management" / "ncp.cmd" assert shim.exists(), "the Windows PATH shim is missing" twin = shim.with_suffix(".ps1") assert not twin.exists(), f"{twin.name} shadows {shim.name} in PowerShell" def test_first_playbook_is_the_system_prompt(tmp_path, monkeypatch): store = PlaybookFileStore(tmp_path) store.add_playbook(PlaybookItem(id="ctx", title="Reference", goal="ref goal", instructions="ref instructions", order=1)) store.add_playbook(PlaybookItem(id="main", title="Main", goal="Be useful.", instructions="Answer briefly.", order=0)) monkeypatch.setattr("synapse.playbook_manager.playbook_store", store) assert PlaybookManager.get_main_playbook().id == "main" assert [p.id for p in PlaybookManager.get_context_playbooks()] == ["ctx"] assert PlaybookManager.get_system_prompt() == "Be useful.\n\nAnswer briefly." def test_settings_round_trip_over_defaults(tmp_path): store = PersistentMemoryStore(tmp_path / "memory.db") store.update_settings({"model": "some-model:8b"}) settings = store.get_settings() assert settings["model"] == "some-model:8b" # written value wins assert settings["memory_model"] == DEFAULT_MEMORY_MODEL # untouched keys still default def test_past_conversations_are_searchable(tmp_path): # The chat system prompt is built from these snippets, so a broken search # silently drops the assistant's recall of past chats. store = PersistentMemoryStore(tmp_path / "memory.db") store.create_conversation("c1") store.add_message("c1", "user", "how do I mount the Wingdrive?") store.add_message("c1", "assistant", "use rsync over ssh") assert store.search_conversations("wingdrive") # case-insensitive substring assert store.search_conversations("nothing here") == [] assert store.search_conversations(" ") == [] def test_memory_store_reads_changes_from_another_instance(tmp_path): db_path = tmp_path / "memory.db" writer = PersistentMemoryStore(db_path) reader = PersistentMemoryStore(db_path) writer.add(MemoryItem(id="fresh", text="written by another process")) assert reader.get("fresh").text == "written by another process" def test_icon_source_requires_real_allowed_file_boundary(tmp_path): allowed = tmp_path / "icons" allowed.mkdir() source = allowed / "app.svg" source.write_text("") sibling = allowed.parent / "icons-other" sibling.mkdir() (sibling / "app.svg").write_text("") old_roots = list(__import__("synapse.icons.compositor", fromlist=["_ALLOWED_ROOTS"])._ALLOWED_ROOTS) module = __import__("synapse.icons.compositor", fromlist=["_ALLOWED_ROOTS"]) module._ALLOWED_ROOTS[:] = [str(allowed)] try: assert _is_allowed_path(str(source)) assert not _is_allowed_path(str(sibling / "app.svg")) assert not _is_allowed_path(str(allowed / "missing.svg")) finally: module._ALLOWED_ROOTS[:] = old_roots def test_ollama_stream_propagates_transport_errors(monkeypatch): class FailingResponse: async def __aenter__(self): return self async def __aexit__(self, *args): return False def raise_for_status(self): raise RuntimeError("Ollama unavailable") class FailingClient: def __init__(self, **kwargs): pass async def __aenter__(self): return self async def __aexit__(self, *args): return False def stream(self, *args, **kwargs): return FailingResponse() monkeypatch.setattr(ollama_manager.httpx, "AsyncClient", FailingClient) async def consume(): async for _ in OllamaManager()._chat_stream([], "model", 0): pass with pytest.raises(RuntimeError, match="Ollama unavailable"): import asyncio asyncio.run(consume()) def _load_sync(): """bin/ isn't a package - load the cross-platform sync core by path.""" import importlib.util spec = importlib.util.spec_from_file_location("sync", REPO_ROOT / "bin" / "sync.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def test_sync_compare_detects_direction(tmp_path): """The guard that stops a stale box from overwriting the other's chats. Both backup and restore refuse to run when this says the wrong thing, so a silent break here loses conversation history.""" import sqlite3 as sq sync = _load_sync() db, dump = tmp_path / "memory.db", tmp_path / "memory.db.sql" def write(rows): db.unlink(missing_ok=True) with sq.connect(db) as conn: conn.executescript( "create table conversations (id text primary key, updated_at text);" "create table memory (id text primary key);" ) conn.executemany("insert into conversations values (?, ?)", rows) assert sync.compare(db, dump) == "no-dump" write([("a", "1")]) assert sync.compare(db, dump) == "no-dump" # Dump matches the live DB exactly. sync.DB, sync.DB_SQL = db, dump assert sync.dump_db() assert sync.compare(db, dump) == "same" # A newer message bumps updated_at -> this box is ahead of the backup. write([("a", "2")]) assert sync.compare(db, dump) == "local-ahead" # The backup holds a conversation this box never saw. write([]) assert sync.compare(db, dump) == "local-behind" # Each side has something the other lacks. write([("b", "1")]) assert sync.compare(db, dump) == "diverged" db.unlink() assert sync.compare(db, dump) == "no-live" def test_restore_stages_match_the_script(): """sync.py invokes restore-linux.sh stages by name with check=False, so a rename on one side alone fails silently - and the runtime stage is what installs Ollama and the ncp alias. Keep the two in agreement.""" import re called = set(re.findall( r'linux_stage\(\s*"restore-linux\.sh"\s*,\s*"(\w+)"', (REPO_ROOT / "bin" / "sync.py").read_text())) script = (REPO_ROOT / "bin" / "restore-linux.sh").read_text() handled = set(re.search(r"case \"\$stage\" in\s*\n\s*([\w|]+)\)", script).group(1).split("|")) assert called, "no restore-linux.sh stages found in sync.py" assert called <= handled, f"sync.py calls unhandled stage(s): {called - handled}" def test_desktop_stage_is_the_only_one_touching_home(): """--no-desktop is only a real safety valve if the $HOME writes all live in the desktop stage. A test clone runs prep + runtime unconditionally.""" script = (REPO_ROOT / "bin" / "restore-linux.sh").read_text() runtime = script.split('if [ "$stage" = "runtime" ]; then')[1].split("\n exit 0\nfi")[0] # Shell wiring is the one deliberate exception: registering ncp in ~/.bashrc # and in the PowerShell profile is how you launch NexusOS at all, and each is # a grep-guarded no-op on re-run. Desktop config (xfconf, plank, themes, # os-release) must stay in the desktop stage so --no-desktop really is safe. shell_wiring = (".bashrc",) home_writes = [ln for ln in runtime.splitlines() if "$HOME" in ln and not any(w in ln for w in shell_wiring)] assert not home_writes, f"runtime stage writes to $HOME: {home_writes}"