"""v7.51.0 — Menu + de l'assistant : sections, parcours fichiers/répertoires, jeton ``folder:`` résolu par ContextBuilder.""" from pathlib import Path from app.db import get_conn from app.services.context_builder import ContextBuilder ROOT = Path(__file__).resolve().parent.parent JS_PATH = ROOT / "static" / "js" / "agent_panel_2.js" def _uid() -> int: with get_conn() as conn: row = conn.execute("SELECT id FROM users LIMIT 1").fetchone() return row["id"] if row else 1 def _folder_with_children(n_children: int = 2, body: str = "Contenu enfant.") -> int: with get_conn() as conn: cur = conn.execute( "INSERT INTO pages (workspace, title, content, content_format, parent_section) " "VALUES ('ws', 'Dossier Specs', '', 'blocks', 'Workspace')" ) fid = cur.lastrowid for i in range(n_children): conn.execute( "INSERT INTO pages (workspace, title, content, content_format, " "parent_section, parent_id) VALUES ('ws', ?, ?, 'markdown', 'Private', ?)", (f"Spec {i}", body * 10 if i else body, fid), ) conn.commit() return fid def test_folder_mention_resolves_children(client): """folder: = titre du dossier + contenu de ses documents directs.""" fid = _folder_with_children() ctx = ContextBuilder(_uid()).build(mentions=[f"folder:{fid}"]) assert "Dossier Specs" in ctx assert "Contenu enfant." in ctx def test_folder_mention_truncates_on_budget(client): """Le contenu d'un dossier est borné (~12k) : un gros dossier n'explose pas le prompt.""" fid = _folder_with_children(n_children=12, body="X" * 800) ctx = ContextBuilder(_uid()).build(mentions=[f"folder:{fid}"]) assert "tronqué" in ctx assert len(ctx) < 20000 def test_folder_mention_unknown_id(client): ctx = ContextBuilder(_uid()).build(mentions=["folder:999999"]) assert "introuvable" in ctx def test_browse_contract_nav_menu(client): """Contrat que le parcours du menu + consomme : dossier = icon 'folder', un niveau par appel via parent_id.""" with get_conn() as conn: conn.execute("INSERT OR IGNORE INTO users (id, login) VALUES (1, 'tester')") cur = conn.execute("INSERT INTO workspaces (name, owner_id) VALUES ('BrowseWS', 1)") ws_id = cur.lastrowid cur = conn.execute( "INSERT INTO pages (workspace, workspace_id, title, content, content_format, " "parent_section, parent_id) VALUES ('BrowseWS', ?, 'Dossier A', '', 'blocks', " "'Workspace', NULL)", (ws_id,), ) folder_id = cur.lastrowid conn.execute( "INSERT INTO pages (workspace, workspace_id, title, content, content_format, " "parent_section, parent_id) VALUES ('BrowseWS', ?, 'Note enfant', '', 'markdown', " "'Private', ?)", (ws_id, folder_id), ) conn.commit() root = client.get(f"/api/nav/menu?workspace_id={ws_id}").json()["items"] names = {i["name"]: i for i in root} assert names["Dossier A"]["icon"] == "folder" kids = client.get(f"/api/nav/menu?workspace_id={ws_id}&parent_id={folder_id}").json()["items"] kid = {i["name"]: i for i in kids}["Note enfant"] assert kid["icon"] != "folder" assert kid["url"] == f"/pages/{kid['id']}" def test_agent_panel_renders_plus_menu(client): """La page rend bien le menu + (markup + bouton).""" resp = client.get("/accounts") assert resp.status_code == 200 html = resp.text assert "fd-ap-plus-menu" in html assert "fd-ap-add" in html assert "Menu d'ajout au contexte" in html def test_plus_menu_sections_and_disabled_states(): """Les 6 sections du menu + existent ; seules les sections déjà livrées sont actives (le menu ne promet rien que le code ne fait).""" src = JS_PATH.read_text(encoding="utf-8") root = src.split("var FD_PLUS_MENU = [", 1)[1].split("];", 1)[0] for key in ("'files'", "'skills'", "'connectors'", "'canvases'", "'plugins'", "'memory'"): assert f"key:{key}" in root, key assert "action:'files'" in root and "action:'skills'" in root # v7.58.0 : menu complet — plus aucune section « bientôt » (plugins vivant) assert root.count("disabled:true") == 0 files = src.split("var FD_PLUS_FILES = [", 1)[1].split("];", 1)[0] assert "action:'search'" in files and "action:'browse'" in files assert "folder:" in src and "document:" in src def test_folder_chip_token_reaches_run_payload(): """Le jeton folder: épinglé part dans payload.mentions (contrat front→agent).""" src = JS_PATH.read_text(encoding="utf-8") # composerMentions() pioche dans contextChips[].token : le parcours doit # produire des items avec token folder:/document:. assert "token: 'folder:' + cur.id" in src assert "token: (isFolder ? 'folder:' : 'document:') + x.id" in src # ... et le backend sait les résoudre. cb = (ROOT / "app" / "services" / "context_builder.py").read_text( encoding="utf-8" ) assert 'm.startswith("folder:")' in cb assert "def _single_folder" in cb