"""Regression tests for WebUI notes source discovery.""" from __future__ import annotations def test_notes_sources_identifies_note_or_knowledge_mcp_servers(): from api.routes import _notes_sources_from_mcp_inventory servers = { "joplin": {"name": "joplin", "enabled": True, "active": True, "status": "healthy"}, "filesystem": {"name": "filesystem", "enabled": True, "active": True, "status": "healthy"}, "llm-wiki": {"name": "llm-wiki", "enabled": True, "active": False, "status": "configured"}, } tools = [ {"server": "joplin", "name": "search_notes", "description": "Search notes by keyword"}, {"server": "joplin", "name": "get_note", "description": "Get full note content"}, {"server": "filesystem", "name": "read_text_file", "description": "Read files"}, {"server": "llm-wiki", "name": "query_knowledge_base", "description": "Search wiki knowledge"}, ] sources = _notes_sources_from_mcp_inventory(servers, tools) assert [source["name"] for source in sources] == ["joplin", "llm-wiki"] assert sources[0]["label"] == "Joplin" assert sources[0]["tool_count"] == 2 assert sources[0]["active"] is True assert sources[1]["active"] is False def test_notes_sources_redacts_tool_descriptions_and_omits_plain_file_tools(): from api.routes import _notes_sources_from_mcp_inventory servers = {"notion": {"name": "notion", "enabled": True, "active": True, "status": "healthy"}} tools = [ {"server": "notion", "name": "search_pages", "description": "Search notes api_key=redaction-test-placeholder"}, ] [source] = _notes_sources_from_mcp_inventory(servers, tools) assert source["name"] == "notion" assert "token" not in source["tools"][0]["description"].lower() assert "[REDACTED]" in source["tools"][0]["description"] def test_notes_sources_shows_configured_note_servers_without_tool_inventory(): from api.routes import _notes_sources_from_mcp_inventory servers = { "joplin": {"name": "joplin", "enabled": True, "active": False, "status": "configured"}, "filesystem": {"name": "filesystem", "enabled": True, "active": True, "status": "healthy"}, } sources = _notes_sources_from_mcp_inventory(servers, []) assert [source["name"] for source in sources] == ["joplin"] assert sources[0]["label"] == "Joplin" assert sources[0]["tool_count"] == 3 assert [tool["name"] for tool in sources[0]["tools"]] == ["search_notes", "list_notes", "get_note"] assert all(tool.get("inferred") is True for tool in sources[0]["tools"]) assert sources[0]["tool_source"] == "configured_hint" assert sources[0]["status"] == "configured" def test_joplin_search_notes_returns_safe_snippets(monkeypatch): from api import routes def fake_get(path, params=None): assert path == "/search" assert params["type"] == "note" return {"items": [{ "id": "abc123def4567890", "title": "Hermes Context", "body": "This is a long Hermes context note with useful details.", "parent_id": "folder123", "updated_time": 123, }]} monkeypatch.setattr(routes, "_joplin_api_get", fake_get) results = routes._joplin_search_notes("Hermes") assert results == [{ "id": "abc123def4567890", "title": "Hermes Context", "snippet": "This is a long Hermes context note with useful details.", "parent_id": "folder123", "updated_time": 123, "source": "joplin", }] def test_joplin_get_note_validates_id_and_truncates_body(monkeypatch): from api import routes def fake_get(path, params=None): assert path == "/notes/abc123def4567890" return { "id": "abc123def4567890", "title": "Big Note", "body": "x" * 60000, "parent_id": "folder123", "updated_time": 456, "created_time": 123, } monkeypatch.setattr(routes, "_joplin_api_get", fake_get) note = routes._joplin_get_note("abc123def4567890") assert note["title"] == "Big Note" assert note["source"] == "joplin" assert len(note["body"]) < 51000 assert "Preview truncated" in note["body"] def test_joplin_recent_ai_notes_uses_configured_prefill_script(monkeypatch, tmp_path): from api import routes script = tmp_path / "joplin_context.py" script.write_text( '\n'.join([ 'CURRENT_CONTEXT_ID = "5ba9ab822c344115939205ca4e8eaec0"', 'OPEN_ISSUES_ID = "623aeb6e55cb4aa39a0541f2ac09aa36"', 'AGENT_MEMORY_ID = "0a7a232ea46b4b8bb0bbd4358f725a84"', 'RAW_CAPTURES_ID = "cb1087795c7d4129a863ab0a5642233d"', ]), encoding="utf-8", ) monkeypatch.setattr(routes, "get_config", lambda: {"prefill_messages_script": str(script)}) def fake_get(path, params=None): note_id = path.rsplit("/", 1)[-1] titles = { "5ba9ab822c344115939205ca4e8eaec0": "Current Context", "623aeb6e55cb4aa39a0541f2ac09aa36": "Open Issues", "0a7a232ea46b4b8bb0bbd4358f725a84": "Agent Memory", } assert note_id in titles return {"id": note_id, "title": titles[note_id], "updated_time": 123, "parent_id": "folder"} monkeypatch.setattr(routes, "_joplin_api_get", fake_get) notes = routes._joplin_recent_ai_notes(limit=3) assert [note["title"] for note in notes] == ["Current Context", "Open Issues", "Agent Memory"] assert all(note["source"] == "joplin" for note in notes) assert all(note["used_by"] == "ai_prefill" for note in notes) assert all(note["used_reason"] == "automatic_recall" for note in notes) def test_external_notes_ui_uses_minimal_lucide_icons_for_ai_recent_notes(): from pathlib import Path panels = Path("static/panels.js").read_text(encoding="utf-8") start = panels.index("function _renderExternalNotesSources()") end = panels.index("function _renderMemoryDetail", start) notes_block = panels[start:end] assert "notes-ai-recent-card" in notes_block assert "li('bot', 14)" in notes_block assert "li('clock', 14)" in notes_block assert "Recently used by AI" not in notes_block # i18n key, not hard-coded UI copy assert "🤖" not in notes_block assert "📚" not in notes_block def test_external_notes_search_button_matches_minimal_dark_controls(): from pathlib import Path css = Path("static/style.css").read_text(encoding="utf-8") assert ".notes-search-form button" in css button_block = css[css.index(".notes-search-form button"):css.index(".notes-search-form button:hover")] assert "background:var(--panel)" in button_block or "background:var(--surface)" in button_block assert "border:1px solid var(--border)" in button_block assert "color:var(--text)" in button_block assert "border-radius:10px" in button_block