Files
flowdeck/tests/test_v751_plus_menu.py
T
bruno 64c85caffc
FlowDeck CI / lint (push) Successful in 2m13s
FlowDeck CI / test (push) Failing after 3h10m19s
FlowDeck CI / docker (push) Skipped
feat: menu + — Mémoire de l'agent — phase 4/8 (v7.54.0)
- Le moteur n'envoie jamais l'historique (system + objectif courant seulement)
  : chaque run repartait de zéro. Nouveau service app/services/agent_memory.py —
  une ligne résumé par conversation (table agent_memory, migration 31),
  écrite en fin de run et ré-injectée au contexte du run suivant.
- context_for() : bloc « ## Mémoire de la conversation », budget 4000 car.
  (troncature gauche + marqueur). remember() : 1 note/échange (180 → 700 car.),
  20 notes max, upsert, erreurs journalisées — un run ne peut pas échouer à
  cause de la mémoire. Toggle OFF = ni lecture ni écriture.
- Toggle « Mémoire » dans le menu + : entrée racine à l'état réel (🧠/💭 +
  libellé), PATCH /api/agent/conversations/{id} avec memory_enabled ajouté au
  whitelist, état lu à l'ouverture et à la création, défaut AGENT_MEMORY_DEFAULT
  (settings.agent_memory_default) + memory_enabled dans la réponse de création.
- Décision : mémoire d'espace (lignes workspace_id NULL) remise — YAGNI, rien
  ne l'écrivait ; noté ponytail: dans le service avec le chemin de montée.
- Tests : tests/test_v754_memory.py (8) dont 2 runs réels du moteur (FakeLLM
  capture le prompt : mémoire présente ON / absente OFF) ; test_v751 et
  test_v753 adaptés (2 sections « bientôt » restantes). Suite complète 1293
  verts (-n auto, 2e run ; 1 échec intermittent sur test_v56_import URL au
  1er run, vert isolé + vert au run suivant = flake préexistant), ruff 0,
  eslint 0 problème. OpenAPI 516 chemins / 7.54.0.
2026-10-06 22:53:15 -04:00

128 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""v7.51.0 — Menu + de l'assistant : sections, parcours fichiers/répertoires,
jeton ``folder:<id>`` 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:<id> = 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
# phases 1-4 livrées → il reste 2 sections « bientôt » (connecteurs, plugins)
assert root.count("disabled:true") == 2
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:<id> é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