Files
NexusOS/tests/test_tui.py
T
AthenaandClaude Sonnet 5 1183ab5282 feat: direct tool invocation via /tool_name(arg=val) + wire Curry as real tools
Adds synapse/slash_commands.py: a chat message that's nothing but
/tool_name(arg=val, arg=val) dispatches straight through tools.dispatch(),
skipping model selection, RAG/playbook context assembly, and the ask-policy
approval round-trip entirely. A human typing this IS the approval - there's
no one else to ask - so it's a deliberate, reviewed bypass of the approval
step specifically, not of anything a tool validates internally (path
boundaries, size caps, Curry's own sandbox checks all still run). Argument
values parse via ast.literal_eval only: strings/numbers/bools/None/literal
containers, no names, no calls, no attribute access - a malformed or
hostile-looking argument fails to parse rather than executing anything.

Wired into chat_stream_endpoint (main.py) as an early short-circuit, before
any of the RAG/model-selection work that a slash-command doesn't need. Web
needed no changes (it already forwards raw text unchanged); the TUI
previously swallowed every leading "/" locally and never reached the backend
with it, so tui_app.py's _handle_slash now falls through to _start_chat for
anything shaped like a tool call while still handling its own local
meta-commands (/help, /model, /new, ...) exactly as before.

Also finally wires Curry in as ten real tools (curry_declare_constant,
curry_get_constant/_latest, curry_list_constants, curry_retire_constant,
curry_declare_function, curry_get_function, curry_list_functions,
curry_call_function, curry_retire_function) - deferred from the vendoring
pass. The five write/execute ones are ACTION tools in the same
always-ask-regardless-of-global-policy floor as edit_source
(ALWAYS_ASK_ACTION_TOOLS, generalized in tools.py from the old
self_edit-only ALWAYS_ASK_TOOLS so future tool families share one place to
register into). curry_call_function is gated as an action for the same
reason run_snippet is: it executes code, even sandboxed.

Fixed a real bug surfaced while wiring this up: curry_db is a long-lived
singleton holding one sqlite3 connection (unlike NexusOS's own memory store,
which opens/closes a fresh connection per call specifically to dodge this),
and sqlite3 forbids using a connection from a different thread than created
it. That's a non-issue in production (uvicorn's single event-loop thread),
but Starlette's TestClient runs the ASGI app through an anyio portal thread,
so it broke immediately under test. Fixed at the source (curry_core.py,
Curry.__init__) with check_same_thread=False, documented as a second
deliberate vendoring deviation alongside the PR #4 sandbox fix - there was
never real concurrent access here, just an overly strict same-thread
assertion tripping on a thread-identity change with only one logical caller.

Verified: 244 backend tests pass (18 new for the parser + endpoint wiring +
curry tool registration, 4 new for the TUI passthrough); the 12 pre-existing
C/C++/Rust toolchain failures are unrelated and unchanged. Confirmed by hand
over the real HTTP endpoint: successful dispatch, zero tool_request events
(approval bypass working as designed), a format()-dunder exploit attempt
still rejected by the vendored sandbox fix even through the new tool
registration, malformed arguments rejected before ever reaching dispatch,
and an unknown tool name rejected cleanly. Wheel rebuilt and content-checked
(bin/check.sh's gate now also asserts slash_commands.py ships).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 08:24:55 -05:00

387 lines
12 KiB
Python

"""TUI helpers and headless App.run_test coverage."""
from __future__ import annotations
import asyncio
import threading
import pytest
from rich.text import Text
from nexusos_cli.tui_app import (
_compact_status,
_deny_tool_request,
_escape,
_status_line,
format_assistant_line,
format_user_line,
)
class _ApprovalResponse:
def raise_for_status(self):
return None
class _ApprovalClient:
calls = []
def __init__(self, **kwargs):
self.kwargs = kwargs
def __enter__(self):
return self
def __exit__(self, *args):
return None
def post(self, path, *, json):
self.calls.append((path, json, self.kwargs))
return _ApprovalResponse()
def test_status_line_mentions_services():
snap = {
"version": "1.0.0",
"services": {
"backend": {"running": True},
"memory": {"running": False},
"provider": {"reachable": True},
},
"api": {"online": True, "action_tool_policy": "ask"},
"host": {"cpu_pct": 10.0},
"toolchains": [{"lang": "python", "ready": True}],
}
line = _status_line(snap)
assert "backend=UP" in line
assert "memory=DOWN" in line
assert "provider=UP" in line
assert "tools=ask" in line
assert "run=python" in line
def test_compact_status_handles_api_down():
snap = {
"host": {},
"api": {"online": False},
"recent_tools": [],
}
assert "api DOWN" in _compact_status(snap)
def test_escape_preserves_code_brackets_in_display():
raw = "idx = arr[i] and rng = [a-z]+"
plain = Text.from_markup(format_assistant_line(raw)).plain
assert "arr[i]" in plain
assert "[a-z]+" in plain
# Unescaped markup would drop the bracket contents.
assert plain != "nexus> idx = arr and rng = +"
def test_closing_tag_in_model_output_does_not_raise():
raw = "close with [/] please"
plain = Text.from_markup(format_assistant_line(raw)).plain
assert "[/]" in plain
def test_user_line_escapes_markup():
plain = Text.from_markup(format_user_line("use [bold] please")).plain
assert "[bold]" in plain
def test_finish_stream_markup_does_not_wedge_busy():
"""A stray '[/]' used to raise before _busy=False and lock the TUI forever."""
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
async def _run():
async with app.run_test():
app._busy = True
app._finish_stream("see [/] and arr[i]")
assert app._busy is False
assert app.history[-1]["content"] == "see [/] and arr[i]"
asyncio.run(_run())
def test_stream_error_remains_visible_after_finish():
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
async def _run():
async with app.run_test():
app._busy = True
app._show_error("[red]Backend not reachable[/]")
app._finish_stream("")
log = app.query_one("#log")
assert any("Backend not reachable" in line.text for line in log.lines)
assert app._busy is False
asyncio.run(_run())
@pytest.mark.parametrize("key", ["ctrl+c", "ctrl+d"])
def test_priority_exit_bindings_reach_app_while_prompt_is_focused(key):
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
async def _run():
async with app.run_test() as pilot:
assert app.is_running
await pilot.press(key)
await pilot.pause()
assert not app.is_running
asyncio.run(_run())
def test_tool_request_is_denied_with_stream_token():
_ApprovalClient.calls.clear()
names = _deny_tool_request(
api_url="http://localhost:8000",
conversation_id="conversation-1",
payload='{"token":"secret","actions":[{"name":"run_snippet"}]}',
client_factory=_ApprovalClient,
)
assert names == ["run_snippet"]
path, body, client_kwargs = _ApprovalClient.calls[-1]
assert path == "/chat/approve"
assert body == {
"conversation_id": "conversation-1",
"token": "secret",
"decisions": {"run_snippet": False},
}
assert client_kwargs["base_url"] == "http://localhost:8000"
def test_inflight_tool_denial_uses_original_conversation_id(monkeypatch):
pytest.importorskip("textual")
import nexusos_cli.tui_app as tui_app
stream_started = threading.Event()
release_stream = threading.Event()
denied_for = []
class _StreamResponse:
status_code = 200
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
async def aiter_lines(self):
stream_started.set()
await asyncio.to_thread(release_stream.wait, 2)
yield "event: tool_request"
yield 'data: {"token":"secret","actions":[{"name":"run_snippet"}]}'
yield ""
yield "event: done"
yield "data: {}"
class _StreamClient:
def __init__(self, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
def stream(self, *args, **kwargs):
return _StreamResponse()
def _capture_denial(*, conversation_id, **kwargs):
denied_for.append(conversation_id)
return ["run_snippet"]
monkeypatch.setattr(tui_app.httpx, "AsyncClient", _StreamClient)
monkeypatch.setattr(tui_app, "_deny_tool_request", _capture_denial)
app = tui_app.NexusTUI.build_app(api_url="http://127.0.0.1:9")
async def _run():
async with app.run_test():
app._start_chat("run it")
assert await asyncio.to_thread(stream_started.wait, 2)
original_id = app.conversation_id
app._handle_slash("/new")
assert app.conversation_id is None
release_stream.set()
for _ in range(200):
if not app._busy:
break
await asyncio.sleep(0.01)
assert app._busy is False
assert denied_for == [original_id]
asyncio.run(_run())
def test_interrupt_cancels_silent_stream_and_accepts_next_message(monkeypatch):
pytest.importorskip("textual")
import nexusos_cli.tui_app as tui_app
first_stream_started = threading.Event()
class _StreamResponse:
status_code = 200
def __init__(self, call_number):
self.call_number = call_number
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
async def aiter_lines(self):
if self.call_number == 1:
first_stream_started.set()
await asyncio.Event().wait()
yield "data: \"READY\""
yield ""
yield "event: done"
yield "data: {}"
class _StreamClient:
calls = 0
def __init__(self, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return None
def stream(self, *args, **kwargs):
type(self).calls += 1
return _StreamResponse(type(self).calls)
monkeypatch.setattr(tui_app.httpx, "AsyncClient", _StreamClient)
app = tui_app.NexusTUI.build_app(api_url="http://127.0.0.1:9")
async def _wait_until_idle():
for _ in range(100):
if not app._busy:
return
await asyncio.sleep(0.01)
pytest.fail("stream did not become idle within one second")
async def _run():
async with app.run_test() as pilot:
app._start_chat("first")
assert await asyncio.to_thread(first_stream_started.wait, 2)
await pilot.press("ctrl+c")
await _wait_until_idle()
log = app.query_one("#log")
assert any("interrupt requested" in line.text for line in log.lines)
assert not any("ReadTimeout" in line.text for line in log.lines)
app._start_chat("second")
await _wait_until_idle()
assert app.history[-1] == {
"role": "assistant",
"content": "READY",
}
asyncio.run(_run())
def test_escape_round_trip_helper():
assert "[" in _escape("x[y]") or "\\[" in _escape("x[y]")
def test_slash_tool_call_shape_forwards_to_start_chat(monkeypatch):
"""/tool_name(arg=val) isn't a local meta-command — it must reach the
backend (synapse/slash_commands.py + chat_stream_endpoint dispatch it),
not fall into the generic 'unknown command' branch."""
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
calls: list[str] = []
monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))
async def _run():
async with app.run_test():
text = '/curry_call_function(name="double", version=1, args={"x": 21})'
app._handle_slash(text)
assert calls == [text]
log = app.query_one("#log")
assert not any("unknown command" in line.text for line in log.lines)
asyncio.run(_run())
def test_slash_malformed_tool_call_still_forwards_for_the_backend_error(monkeypatch):
"""Even a malformed /tool(...) is forwarded rather than swallowed locally
— the backend's parser gives a clearer, more specific error than the
TUI's generic 'unknown command' would."""
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
calls: list[str] = []
monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))
async def _run():
async with app.run_test():
text = "/curry_call_function(x=__import__('os'))"
app._handle_slash(text)
assert calls == [text]
asyncio.run(_run())
def test_slash_local_meta_commands_still_handled_locally(monkeypatch):
"""A known local command must still be handled in-TUI, never forwarded —
the new tool-call passthrough is strictly the fallback branch."""
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
calls: list[str] = []
monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))
async def _run():
async with app.run_test():
app._handle_slash("/help")
assert calls == []
log = app.query_one("#log")
assert any("this list" in line.text for line in log.lines)
asyncio.run(_run())
def test_slash_unknown_bare_command_still_rejected(monkeypatch):
"""A genuinely unknown command (no parens, not a local command) keeps the
existing 'unknown command' behavior rather than silently forwarding
anything that starts with /."""
pytest.importorskip("textual")
from nexusos_cli.tui_app import NexusTUI
app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
calls: list[str] = []
monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))
async def _run():
async with app.run_test():
app._handle_slash("/frobnicate")
assert calls == []
log = app.query_one("#log")
assert any("unknown command" in line.text for line in log.lines)
asyncio.run(_run())