Files
NexusOS/tests/test_tools.py
T
jonandClaude Opus 4.8 ba6a4ac4e4 feat: workspaces, agentic action tools, local Whisper STT, vec recall
- Projects/workspaces: documents grouped into projects; chat RAG scopes to the
  active project. Switcher in the Documents page.
- Agentic action tools: web_search, fetch_url, and remember (first write tool),
  allowlist-gated per playbook.
- Local Whisper STT (faster-whisper, no torch): on-device dictation replacing
  the browser Web Speech API. POST /stt + GET /stt/status; browser fallback.
- Vector index extended to conversation recall (message_vectors), with the
  brute-force cosine scan kept as the fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:22:46 -05:00

93 lines
3.4 KiB
Python

"""Tool-using playbook loop — the read-only MVP.
Run from nexus-core/ with the Promethean venv active: pytest -q
Hermetic: a fake manager stands in for Ollama, so no network/model is needed.
Guards the two pieces that would silently break the feature: the allowlist
filter and the tool-call loop's terminate-on-content behaviour.
"""
import asyncio
from synapse import tools
from synapse.chat import _run_tool_loop
def test_schemas_for_drops_unknown_names():
schemas = tools.schemas_for(["search_memory", "not_a_tool"])
names = [s["function"]["name"] for s in schemas]
assert names == ["search_memory"]
assert tools.schemas_for([]) == []
async def _drain(gen):
return [s async for s in gen]
def test_remember_writes_a_memory_fact(tmp_path, monkeypatch):
# The `remember` action tool persists a fact through the store.
import synapse.memory.store as store_mod
from synapse.memory.store import PersistentMemoryStore
fresh = PersistentMemoryStore(tmp_path / "m.db")
monkeypatch.setattr(tools, "store", fresh)
out = asyncio.run(tools.dispatch("remember", {"text": "user likes tea", "section": "Prefs"}))
assert "user likes tea" in out
assert any(it.text == "user likes tea" for it in fresh.all())
def test_action_tools_registered():
for name in ("web_search", "fetch_url", "remember"):
assert name in tools.REGISTRY
names = [s["function"]["name"] for s in tools.schemas_for(["web_search", "remember", "nope"])]
assert names == ["web_search", "remember"]
class _FakeManager:
"""Returns a tool_call on the first chat() call, plain content after."""
def __init__(self):
self.calls = 0
async def chat(self, **_):
self.calls += 1
if self.calls == 1:
return {
"role": "assistant",
"tool_calls": [
{"function": {"name": "search_memory", "arguments": {"query": "gpu"}}}
],
}
return {"role": "assistant", "content": "here is the answer"}
def test_tool_loop_runs_tool_then_stops(monkeypatch):
async def fake_dispatch(name, args):
assert name == "search_memory"
assert args == {"query": "gpu"}
return '[{"section": "GPU", "text": "Vega 20 4GB"}]'
monkeypatch.setattr(tools, "dispatch", fake_dispatch)
messages = [{"role": "user", "content": "what gpu do i have?"}]
schemas = tools.schemas_for(["search_memory"])
statuses = asyncio.run(_drain(
_run_tool_loop(_FakeManager(), messages, "m", schemas, None, None)
))
# one status sentinel per tool run
assert statuses == ["__status__search_memory"]
# messages mutated in place: user -> assistant(tool_calls) -> tool(result);
# the final content turn is NOT appended (the streaming turn regenerates it).
assert [m["role"] for m in messages] == ["user", "assistant", "tool"]
assert "Vega 20" in messages[-1]["content"]
def test_tool_loop_degrades_when_model_returns_no_dict():
class _NoToolManager:
async def chat(self, **_):
return None # model can't do tools / errored
messages = [{"role": "user", "content": "hi"}]
before = list(messages)
statuses = asyncio.run(_drain(_run_tool_loop(_NoToolManager(), messages, "m", [{}], None, None)))
assert statuses == [] # no tool ran
assert messages == before # untouched -> falls back to a plain stream