feat(ai): catalogue d'outils lecture & recherche (#79 phase C)
CI / lint (push) Successful in 59s
CI / security (push) Successful in 49s
CI / test (push) Successful in 1m24s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m24s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

This commit is contained in:
2026-09-11 20:33:15 -04:00
parent 7cf7d33b6d
commit 31b65ade5b
13 changed files with 1220 additions and 524 deletions
+153 -1
View File
@@ -28,7 +28,25 @@ from backend.tools.api import (
from backend.tools.registry import ToolSpec
from backend.tools.schemas import ListVaultsInput
BUILTIN_TOOLS = {"list_vaults", "list_directory", "read_file", "search_fulltext", "list_tags"}
BUILTIN_TOOLS = {
# Phase 0
"list_vaults",
"list_directory",
"read_file",
"search_fulltext",
"list_tags",
# Phase C — read/search catalogue
"list_all_files",
"read_file_raw",
"get_backlinks",
"list_backups",
"diff_backup",
"get_graph",
"search_advanced",
"search_paths",
"suggest_tags",
"list_recent",
}
def _ctx(vaults=None, **kwargs) -> ToolContext:
@@ -278,3 +296,137 @@ class TestToolAudit:
assert safe["vault"] == "V"
assert safe["path"] == "a.md"
assert safe["content"] == "<500 chars>"
# ═══════════════════════════════════════════════════════════════════
# Phase C — read/search catalogue
# ═══════════════════════════════════════════════════════════════════
def _vault_path() -> Path:
from backend.indexer import get_vault_data
return Path(get_vault_data("TestVault")["path"])
def _make_backup(rel_path: str, ts: int, content: str) -> Path:
from backend.services.backups import get_backup_dir
backup_dir = get_backup_dir("TestVault", rel_path)
backup_dir.mkdir(parents=True, exist_ok=True)
backup = backup_dir / f"{Path(rel_path).name}.{ts}.bak"
backup.write_text(content, encoding="utf-8")
return backup
class TestPhaseCTools:
def test_list_all_files(self, client):
result = call_tool("list_all_files", _ctx(), {"vault": "TestVault"})
assert result.ok
paths = {f["path"] for f in result.data["files"]}
assert "note1.md" in paths
assert "Projets/projet.md" in paths
def test_list_all_files_non_recursive(self, client):
result = call_tool(
"list_all_files", _ctx(), {"vault": "TestVault", "recursive": False}
)
paths = {f["path"] for f in result.data["files"]}
assert "note1.md" in paths
assert "Projets/projet.md" not in paths
def test_list_all_files_permission_denied(self, client):
ctx = ToolContext(user={"username": "limited", "vaults": ["OtherVault"]}, audit_enabled=False)
with pytest.raises(ToolPermissionError):
call_tool("list_all_files", ctx, {"vault": "TestVault"})
def test_read_file_raw(self, client):
result = call_tool("read_file_raw", _ctx(), {"vault": "TestVault", "path": "note1.md"})
assert result.ok
assert "Python" in result.data["raw"]
assert result.data["path"] == "note1.md"
def test_get_backlinks(self, client):
result = call_tool("get_backlinks", _ctx(), {"vault": "TestVault", "path": "note1.md"})
assert result.ok
assert all(b["vault"] == "TestVault" for b in result.data)
def test_get_backlinks_filters_inaccessible_vaults(self, client):
ctx = ToolContext(user={"username": "limited", "vaults": ["OtherVault"]}, audit_enabled=False)
with pytest.raises(ToolPermissionError):
call_tool("get_backlinks", ctx, {"vault": "TestVault", "path": "note1.md"})
def test_list_backups_empty(self, client):
result = call_tool("list_backups", _ctx(), {"vault": "TestVault", "path": "note1.md"})
assert result.ok
assert result.data["backups"] == []
def test_list_backups_and_diff(self, client):
backup = _make_backup("note1.md", 1_700_000_000, "old content\n")
try:
listed = call_tool("list_backups", _ctx(), {"vault": "TestVault", "path": "note1.md"})
timestamps = [b["timestamp"] for b in listed.data["backups"]]
assert 1_700_000_000 in timestamps
diff = call_tool(
"diff_backup",
_ctx(),
{"vault": "TestVault", "path": "note1.md", "version": 1_700_000_000},
)
assert diff.ok
assert diff.data["version"] == 1_700_000_000
assert diff.data["left_content"] == "old content\n"
assert "-old content" in diff.data["diff"]
finally:
backup.unlink(missing_ok=True)
def test_diff_backup_missing_version(self, client):
with pytest.raises(ToolNotFoundError):
call_tool(
"diff_backup",
_ctx(),
{"vault": "TestVault", "path": "note1.md", "version": 42},
)
def test_get_graph(self, client):
result = call_tool("get_graph", _ctx(), {"vault": "TestVault", "scope": "full", "depth": 2})
assert result.ok
node_paths = {n["path"] for n in result.data["nodes"]}
assert "note1.md" in node_paths
assert result.data["scope"] == "full"
def test_get_graph_missing_path(self, client):
with pytest.raises(ToolNotFoundError):
call_tool("get_graph", _ctx(), {"vault": "TestVault", "path": "nope"})
def test_search_advanced(self, client):
result = call_tool("search_advanced", _ctx(), {"q": "python"})
assert result.ok
assert len(result.data) >= 1
assert all(r["vault"] == "TestVault" for r in result.data)
def test_search_paths(self, client):
result = call_tool("search_paths", _ctx(), {"q": "note1"})
assert result.ok
assert any(r["path"] == "note1.md" for r in result.data)
def test_suggest_tags(self, client):
result = call_tool("suggest_tags", _ctx(), {"q": "py"})
assert result.ok
assert any(s["tag"] == "python" for s in result.data)
def test_suggest_tags_specific_vault_denied(self, client):
ctx = ToolContext(user={"username": "limited", "vaults": ["OtherVault"]}, audit_enabled=False)
with pytest.raises(ToolPermissionError):
call_tool("suggest_tags", ctx, {"q": "py", "vault": "TestVault"})
def test_list_recent_modified(self, client):
result = call_tool("list_recent", _ctx(), {"mode": "modified"})
assert result.ok
assert result.data["mode"] == "modified"
assert any(f["path"] == "note1.md" for f in result.data["files"])
def test_list_recent_filters_by_vault(self, client):
ctx = ToolContext(user={"username": "limited", "vaults": ["OtherVault"]}, audit_enabled=False)
result = call_tool("list_recent", ctx, {"mode": "modified"})
assert result.data["files"] == []