Initial commit: NexusOS - local AI assistant platform
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
"""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_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_install_windows_stays_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.
|
||||
for name in ("install-windows.ps1", "launch_nexus.ps1"):
|
||||
raw = (REPO_ROOT / name).read_bytes()
|
||||
bad = [(i, b) for i, b in enumerate(raw) if b > 0x7F]
|
||||
assert not bad, f"{name} has non-ASCII bytes at {bad[:3]}"
|
||||
|
||||
|
||||
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("<svg />")
|
||||
sibling = allowed.parent / "icons-other"
|
||||
sibling.mkdir()
|
||||
(sibling / "app.svg").write_text("<svg />")
|
||||
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"
|
||||
Reference in New Issue
Block a user