- Sous-menu « Compétences-skills » : 11 skills builtin + skills enregistrés,
libellés affichés « Deep research » (/research) et « Skill-creator »
(/create-new-skill) via FD_SKILL_ALIAS (slugs intacts) ; le clic épingle le
skill au contexte, même chemin que la palette /.
- « Gérer les compétences » : section du menu + formulaire intégré (nom,
description, prompt) — création, édition, suppression (chip épinglé nettoyé),
export JSON téléchargé, import fichier avec overwrite (pas de doublon).
- « Parcourir les compétences » : section galerie filtrable (nom/description/
slug), installation en 1 clic (installGallerySkill réutilisé).
- PATCH /api/agent/skills/{skill_id} créé : édition partielle, whitelist de
colonnes, 400/401/404/409. OpenAPI régénéré : 515 chemins, version 7.52.0.
- En-tête ← + titre de section dans le menu (navigation à 3 niveaux).
- Fix : épingler un skill enregistré n'efface plus le message déjà tapé
(_pinDbSkill ne vide le composer que s'il est vide).
- Tests : tests/test_v752_skills_menu.py (7) + test_v751 adapté. Suite
complète 1280 verts (-n auto), ruff 0, eslint 0 problème.
115 lines
4.8 KiB
Python
115 lines
4.8 KiB
Python
"""v7.52.0 — Menu + : Compétences-skills (« Gérer » + « Parcourir »).
|
||
|
||
Couvre le nouveau ``PATCH /api/agent/skills/{id}``, l'import sans doublon et le
|
||
câblage du sous-menu (sections, alias affichés, formulaire)."""
|
||
|
||
from pathlib import Path
|
||
|
||
from conftest import anon_csrf
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
JS_PATH = ROOT / "static" / "js" / "agent_panel_2.js"
|
||
PANEL_HTML = ROOT / "app" / "templates" / "agent_panel.html"
|
||
|
||
NEW_SKILL = {
|
||
"name": "synthese-reunion",
|
||
"description": "Synthèse de réunion",
|
||
"prompt_template": "Résume cette réunion en décisions et actions.",
|
||
}
|
||
|
||
|
||
def _create(client, **overrides):
|
||
body = {**NEW_SKILL, **overrides}
|
||
r = client.post("/api/agent/skills", json=body)
|
||
assert r.status_code == 200, r.text
|
||
return r.json()["id"]
|
||
|
||
|
||
def test_skill_patch_updates_fields(client):
|
||
sid = _create(client)
|
||
r = client.patch(f"/api/agent/skills/{sid}", json={
|
||
"name": "synthese-v2", "description": "Version 2",
|
||
"prompt_template": "Résume en 5 points.",
|
||
})
|
||
assert r.status_code == 200, r.text
|
||
assert r.json()["status"] == "updated"
|
||
|
||
skills = {s["id"]: s for s in client.get("/api/agent/skills").json()["skills"]}
|
||
assert skills[sid]["name"] == "synthese-v2"
|
||
assert skills[sid]["description"] == "Version 2"
|
||
assert skills[sid]["prompt_template"] == "Résume en 5 points."
|
||
|
||
|
||
def test_skill_patch_partial_and_validation(client):
|
||
sid = _create(client)
|
||
# mise à jour partielle : un seul champ ne touche pas les autres
|
||
assert client.patch(f"/api/agent/skills/{sid}", json={"description": "Seul champ"}).status_code == 200
|
||
skills = {s["id"]: s for s in client.get("/api/agent/skills").json()["skills"]}
|
||
assert skills[sid]["description"] == "Seul champ"
|
||
assert skills[sid]["prompt_template"] == NEW_SKILL["prompt_template"]
|
||
|
||
assert client.patch(f"/api/agent/skills/{sid}", json={}).status_code == 400
|
||
assert client.patch(f"/api/agent/skills/{sid}", json={"name": " "}).status_code == 400
|
||
assert client.patch("/api/agent/skills/999999", json={"name": "ghost"}).status_code == 404
|
||
|
||
|
||
def test_skill_patch_requires_session(client):
|
||
sid = _create(client)
|
||
anon_csrf(client) # CSRF valide mais plus de session → on veut le 401 de la route
|
||
assert client.patch(f"/api/agent/skills/{sid}", json={"name": "pirate"}).status_code == 401
|
||
|
||
|
||
def test_import_skill_has_no_duplicate(client):
|
||
payload = {"name": "skill-importe", "prompt_template": "Fais X.", "description": "import"}
|
||
first = client.post("/api/agent/skills/import", json={"payload": payload})
|
||
assert first.status_code == 200 and first.json()["status"] == "imported"
|
||
|
||
# même payload, sans overwrite → 409 (pas de doublon silencieux)
|
||
again = client.post("/api/agent/skills/import", json={"payload": payload})
|
||
assert again.status_code == 409
|
||
# avec overwrite → mise à jour de la même ligne
|
||
over = client.post("/api/agent/skills/import", json={"payload": payload, "overwrite": True})
|
||
assert over.status_code == 200 and over.json()["status"] == "updated"
|
||
|
||
names = [s["name"] for s in client.get("/api/agent/skills").json()["skills"]]
|
||
assert names.count("skill-importe") == 1
|
||
|
||
|
||
def test_export_then_roundtrip_import(client):
|
||
sid = _create(client)
|
||
doc = client.get(f"/api/agent/skills/{sid}/export").json()
|
||
client.delete(f"/api/agent/skills/{sid}")
|
||
back = client.post("/api/agent/skills/import", json={"payload": doc, "overwrite": True})
|
||
assert back.status_code == 200, back.text
|
||
assert back.json()["name"] == NEW_SKILL["name"]
|
||
|
||
|
||
def test_skills_menu_sections_enabled(client):
|
||
"""Sous-menu Compétences actif : alias affichés, Gérer + Parcourir, CRUD branché."""
|
||
src = JS_PATH.read_text(encoding="utf-8")
|
||
root = src.split("var FD_PLUS_MENU = [", 1)[1].split("];", 1)[0]
|
||
assert "action:'skills'" in root
|
||
assert "bientôt (phase 2)" not in root
|
||
|
||
alias = src.split("var FD_SKILL_ALIAS = ", 1)[1].split(";", 1)[0]
|
||
assert "research" in alias and "Deep research" in alias
|
||
assert "create-new-skill" in alias and "Skill-creator" in alias
|
||
|
||
for action in ("'skills-manage'", "'skills-gallery'", "'skill-new'",
|
||
"'skill-edit'", "'skill-install'", "'skill-import'"):
|
||
assert action in src, action
|
||
for method in ("saveSkill()", "deleteSkill()", "exportSkill()", "importSkillFile"):
|
||
assert method in src, method
|
||
# PATCH utilisé pour l'édition (pas de POST qui recréerait la ligne)
|
||
assert "method: f.id ? 'PATCH' : 'POST'" in src
|
||
|
||
|
||
def test_skills_menu_markup_rendered(client):
|
||
html = PANEL_HTML.read_text(encoding="utf-8")
|
||
assert "fd-plus-form" in html
|
||
assert 'x-ref="skillImport"' in html
|
||
assert "Gérer les compétences" in JS_PATH.read_text(encoding="utf-8")
|
||
resp = client.get("/accounts")
|
||
assert resp.status_code == 200
|
||
assert "fd-plus-form" in resp.text
|