forked from enderofwings/NexusOS
feat: sync with upstream — v1.2.0, in-app updates, Projects, modules
Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
+69
-18
@@ -19,7 +19,7 @@ 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 import playbook_manager
|
||||
from synapse.playbooks.store import PlaybookFileStore, PlaybookItem
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -162,9 +162,9 @@ def test_first_playbook_is_the_system_prompt(tmp_path, monkeypatch):
|
||||
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."
|
||||
assert playbook_manager.get_main_playbook().id == "main"
|
||||
assert [p.id for p in playbook_manager.get_context_playbooks()] == ["ctx"]
|
||||
assert playbook_manager.get_system_prompt() == "Be useful.\n\nAnswer briefly."
|
||||
|
||||
|
||||
def test_settings_round_trip_over_defaults(tmp_path):
|
||||
@@ -180,10 +180,10 @@ def test_past_conversations_are_searchable(tmp_path):
|
||||
# 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", "user", "how do I mount the backup drive?")
|
||||
store.add_message("c1", "assistant", "use rsync over ssh")
|
||||
|
||||
assert store.search_conversations("wingdrive") # case-insensitive substring
|
||||
assert store.search_conversations("BACKUP drive") # case-insensitive substring
|
||||
assert store.search_conversations("nothing here") == []
|
||||
assert store.search_conversations(" ") == []
|
||||
|
||||
@@ -271,10 +271,10 @@ def test_sync_compare_detects_direction(tmp_path):
|
||||
def write(rows):
|
||||
db.unlink(missing_ok=True)
|
||||
with sq.connect(db) as conn:
|
||||
# updated_at REAL, matching the production schema in store.py. A TEXT
|
||||
# column here hid a real TypeError for months: the comparison in
|
||||
# _extra() ran str-vs-str in the test and str-vs-float in the field.
|
||||
conn.executescript(
|
||||
# updated_at REAL, matching the production schema in store.py. A TEXT
|
||||
# column here hid a real TypeError for months: the comparison in
|
||||
# _extra() ran str-vs-str in the test and str-vs-float in the field.
|
||||
"create table conversations (id text primary key, updated_at real not null);"
|
||||
"create table memory (id text primary key);"
|
||||
)
|
||||
@@ -352,6 +352,15 @@ def test_genmon_configs_are_written_with_the_panel_down():
|
||||
assert quit_at < copy_at < start_at, "genmon rc copy must happen with the panel stopped"
|
||||
|
||||
|
||||
def test_plank_is_actually_launched():
|
||||
"""Restoring ~/.config/plank only brings back the dock's launchers - nothing
|
||||
in it starts Plank. The primary-follow watcher is what launches and revives
|
||||
it, so it needs an autostart entry or a fresh box has no dock at all."""
|
||||
desktop = REPO_ROOT / "management" / "autostart" / "plank.desktop"
|
||||
assert "plank-primary-watch.sh" in desktop.read_text()
|
||||
assert "plank.desktop" in (REPO_ROOT / "bin" / "panel" / "install.sh").read_text()
|
||||
|
||||
|
||||
_VULKANINFO_IGPU_AND_LLVMPIPE = """\
|
||||
Devices:
|
||||
========
|
||||
@@ -455,30 +464,72 @@ def test_dump_round_trips_a_db_holding_vec_tables(tmp_path):
|
||||
def test_curator_drops_fabricated_facts():
|
||||
"""The curator model invents two classes of fact no prompt wording stopped
|
||||
(verified against mistral:7b), and both reached the real memory DB: absence
|
||||
claims read off the existing-memory block ("Jon does not have any pets",
|
||||
claims read off the existing-memory block ("the user does not have any pets",
|
||||
which contradicted four cats on file) and specifics lifted from the
|
||||
ASSISTANT's reply ("Jon's main development machine is a MacBook Pro", from
|
||||
ASSISTANT's reply ("the user's main development machine is a MacBook Pro", from
|
||||
the user message "What am I developing on?"). Deterministic guard, so it
|
||||
holds whatever the model does."""
|
||||
from synapse.memory.extractor import _reject_reason
|
||||
|
||||
# Absence claims are never facts.
|
||||
assert _reject_reason("Jon does not have any pets", "do i have any pets?")
|
||||
assert _reject_reason("Jon's favorite episode is unknown", "what's my favorite episode?")
|
||||
assert _reject_reason("Jon has not specified an interest", "tell me about stargate")
|
||||
assert _reject_reason("the user does not have any pets", "do i have any pets?")
|
||||
assert _reject_reason("the user's favorite episode is unknown", "what's my favorite episode?")
|
||||
assert _reject_reason("the user has not specified an interest", "tell me about stargate")
|
||||
|
||||
# Specifics the user never typed came from the assistant.
|
||||
assert _reject_reason("Jon's main dev machine is a MacBook Pro", "What am I developing on?")
|
||||
assert _reject_reason("the user's main dev machine is a MacBook Pro", "What am I developing on?")
|
||||
|
||||
# ...but the same shape grounded in the user's own words must survive.
|
||||
assert _reject_reason(
|
||||
"Jon owns a 2000 Ford Ranger with a 3.0L V6",
|
||||
"the user owns a 2000 Ford Ranger with a 3.0L V6",
|
||||
"i also have a 2000 Ford Ranger, it's a five-speed with a 3.0L V6") is None
|
||||
assert _reject_reason(
|
||||
"Jon has a beagle named Biscuit",
|
||||
"the user has a beagle named Biscuit",
|
||||
"i just adopted a dog named Biscuit, he's a beagle") is None
|
||||
# A fact carrying no proper nouns or numbers can't be grounding-checked;
|
||||
# the prompt owns that case, so the guard must let it through.
|
||||
assert _reject_reason(
|
||||
"Jon prefers short answers over long explanations",
|
||||
"the user prefers short answers over long explanations",
|
||||
"i really prefer short answers over long explanations") is None
|
||||
|
||||
|
||||
def test_update_check_reports_behind_and_survives_git_failure(monkeypatch):
|
||||
from synapse import main
|
||||
# Fake git so the test never touches the network. Behind → the remote
|
||||
# VERSION file, not this checkout's, is what the UI advertises.
|
||||
calls = {
|
||||
("rev-list", "--count", "HEAD..origin/main"): "3",
|
||||
("show", "origin/main:VERSION"): "9.9.9\n",
|
||||
("log", "-1", "--format=%h %s", "origin/main"): "abc1234 feat: thing",
|
||||
}
|
||||
monkeypatch.setattr(main, "_git", lambda *a, **kw: calls.get(a, ""))
|
||||
body = TestClient(app).get("/update/check").json()
|
||||
assert body["behind"] == 3 and body["remote_version"] == "9.9.9"
|
||||
|
||||
# An unreachable remote must not 500 the sidebar.
|
||||
def boom(*a, **kw):
|
||||
raise RuntimeError("could not resolve host")
|
||||
monkeypatch.setattr(main, "_git", boom)
|
||||
body = TestClient(app).get("/update/check").json()
|
||||
assert body["behind"] == 0 and "could not resolve host" in body["error"]
|
||||
|
||||
|
||||
def test_update_apply_spawns_detached_and_refuses_a_second_run(monkeypatch):
|
||||
import subprocess
|
||||
from synapse import main
|
||||
seen = {}
|
||||
|
||||
def fake_popen(argv, **kw):
|
||||
seen["argv"], seen["kw"] = argv, kw
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(main, "_update_running", False)
|
||||
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
||||
client = TestClient(app)
|
||||
assert client.post("/update/apply").json()["started"] is True
|
||||
assert seen["argv"][-2:] == [str(REPO_ROOT / "management" / "ncp.py"), "upgrade"]
|
||||
# Detached, or `ncp upgrade` dies with the backend it is about to stop.
|
||||
assert seen["kw"].get("start_new_session") or seen["kw"].get("creationflags")
|
||||
|
||||
# Double-click must not launch a second pull/rebuild over the first.
|
||||
assert client.post("/update/apply").json()["started"] is False
|
||||
|
||||
Reference in New Issue
Block a user