"""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