#!/usr/bin/env python3 """Test dei contesti condivisi su directory e DB temporanei (non tocca i dati reali). Uso (dentro il container): docker exec loogle-mcp python /srv/scripts/test_shared_contexts.py """ import json import os import sys import tempfile TMP = tempfile.mkdtemp(prefix="shared-ctx-test-") os.environ["MCP_CONTEXT_ROOT"] = os.path.join(TMP, "context") os.environ["MCP_DB"] = os.path.join(TMP, "test.db") sys.path.insert(0, "/srv") from app.db import init_db # noqa: E402 from app.context import store # noqa: E402 from app.knowledge import indexer # noqa: E402 from app.mcp import tools # noqa: E402 init_db() queued = [] indexer.submit_background = lambda label, fn, *a, **k: queued.append((label, fn.__name__, a)) failures = 0 def claims(user, admin=False): scope = "context:read context:write" + (" admin" if admin else "") return {"sub": user, "scope": scope} def call(user, name, args, admin=False): out = tools.call_tool(name, args, claims(user, admin)) return json.loads(out["content"][0]["text"]) def check(label, cond): global failures failures += 0 if cond else 1 print(f"{'OK ' if cond else 'FAIL'} {label}") def raises(exc, fn, *a, **k): try: fn(*a, **k) except exc: return True except Exception as other: # noqa: BLE001 print(" eccezione inattesa:", type(other).__name__, other) return False # 1. Progetto personale di daniele, poi condiviso p = call("daniele", "create_project", {"title": "Stampa 3D X2D", "tags": ["stampa-3d"]}) pid = p["id"] call("daniele", "save_context", {"project_id": pid, "content": "## Dotazione\n- PLA 4 kg"}) check("lucia non vede il progetto personale di daniele", pid not in [x["id"] for x in call("lucia", "list_projects", {})["projects"]]) check("lucia non può leggerlo", raises(FileNotFoundError, call, "lucia", "get_project_context", {"project_id": pid})) m = call("daniele", "share_project", {"project_id": pid}) check("share_project -> shared/family/owner", m.get("shared") and m.get("shared_with") == "family" and m.get("owner") == "daniele") check("la cartella è passata nello spazio condiviso", os.path.isfile(os.path.join(TMP, "context", "_shared", "projects", pid, "context.md")) and not os.path.exists(os.path.join(TMP, "context", "daniele", "projects", pid))) check("reindicizzazione vettori accodata", any(q[1] == "_move_project_vectors" for q in queued)) # 2. Tutta la famiglia legge e aggiorna for u in ("lucia", "davide", "luca"): ids = {x["id"]: x for x in call(u, "list_projects", {})["projects"]} check(f"{u} vede il contesto condiviso", pid in ids and ids[pid].get("shared")) ctx = call("lucia", "get_project_context", {"project_id": pid}) check("lucia legge la storia (sessione di daniele)", "PLA 4 kg" in ctx["context_md"] and ctx["recent_sessions"]) rev = ctx["meta"].get("revision", 0) m = call("lucia", "save_context", {"project_id": pid, "mode": "replace", "content": "## Dotazione\n- PLA 4 kg\n- PETG 1 kg (comprato da Lucia)", "if_revision": rev}) check("lucia aggiorna, updated_by=lucia", m.get("updated_by") == "lucia") check("daniele vede la modifica di lucia", "PETG 1 kg" in call("daniele", "get_project_context", {"project_id": pid})["context_md"]) sessions = os.listdir(os.path.join(TMP, "context", "_shared", "projects", pid, "sessions")) check("il nome della sessione indica l'autore", any(s.endswith("-lucia.md") for s in sessions)) check("conflitto: if_revision superata -> errore e nessuna sovrascrittura", raises(store.ConflictError, store.save_context, "davide", pid, "x", "replace", if_revision=rev) and "PETG 1 kg" in call("davide", "get_project_context", {"project_id": pid})["context_md"]) check("indicizzazione del contesto completo accodata", any(q[1] == "index_project_context" for q in queued)) # 3. Permessi: archiviare / rendere personale check("lucia non può archiviare", raises(PermissionError, call, "lucia", "archive_project", {"project_id": pid})) check("lucia non può riportarlo personale", raises(PermissionError, call, "lucia", "share_project", {"project_id": pid, "shared": False})) check("admin può archiviare", call("davide", "archive_project", {"project_id": pid}, admin=True).get("archived") is True) call("daniele", "archive_project", {"project_id": pid, "archived": False}) m = call("daniele", "share_project", {"project_id": pid, "shared": False}) check("il proprietario lo riporta personale", not m.get("shared") and "owner" not in m) check("dopo: lucia non lo vede più", pid not in [x["id"] for x in call("lucia", "list_projects", {})["projects"]]) check("dopo: daniele lo ha di nuovo personale con la storia", "PETG 1 kg" in call("daniele", "get_project_context", {"project_id": pid})["context_md"]) # 4. Creazione diretta condivisa, ID unici tra i due spazi, risorse MCP s = call("lucia", "create_project", {"title": "Stampa 3D X2D", "shared": True}) check("create_project shared=true con ID diverso dal personale di daniele", s.get("shared") and s["id"] != pid) check("daniele raggiunge sia il suo personale sia il nuovo condiviso", call("daniele", "get_project_context", {"project_id": s["id"]})["meta"].get("shared") and not call("daniele", "get_project_context", {"project_id": pid})["meta"].get("shared")) call("luca", "create_project", {"title": "Omonimo"}) call("daniele", "create_project", {"title": "Omonimo"}) check("share_project rifiuta un ID usato da un personale di un altro utente", raises(ValueError, call, "daniele", "share_project", {"project_id": "omonimo"})) uris = [r["uri"] for r in tools.list_resources(claims("luca"))] check("risorsa condivisa visibile a luca", f"loogle://context/_shared/{s['id']}" in uris) res = tools.read_resource(f"loogle://context/_shared/{s['id']}", claims("luca")) check("luca legge la risorsa condivisa", res["text"].startswith("# Stampa 3D X2D")) print(f"Errori: {failures}") sys.exit(1 if failures else 0)