Backup automatico script del 2026-09-27 07:00

This commit is contained in:
daniele committed 2026-09-27 07:00:02 +02:00
1 parent f347badf3b
commit 09dcbbdadb
24 files changed
+1164 -193

No files matched your search

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Test di concorrenza MCP: N chiamate parallele + sonde /health e ping.
Uso (dentro il container):
docker exec loogle-mcp python /srv/scripts/test_mcp_concurrency.py [N] [tool]
Misura se una chiamata lenta blocca le altre (event loop bloccato) e la
latenza delle ricerche semantiche.
"""
import concurrent.futures as cf
import json
import os
import sys
import time
import httpx
sys.path.insert(0, "/srv")
from app.db import get_conn # noqa: E402
from app.jwt_utils import create_access_token # noqa: E402
BASE = os.environ.get("TEST_BASE", "http://127.0.0.1:8700")
N = int(sys.argv[1]) if len(sys.argv) > 1 else 4
TOOL = sys.argv[2] if len(sys.argv) > 2 else "search_knowledge"
CLIENT_TIMEOUT = 150.0
row = get_conn().execute("SELECT id, username FROM users WHERE username='daniele'").fetchone()
token, _ = create_access_token(
{"id": row["id"], "username": row["username"]},
"context:read knowledge:read gitea:read admin",
"loadtest",
)
HEADERS = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def rpc(method: str, params: dict, req_id: int) -> tuple[float, str]:
t0 = time.monotonic()
try:
r = httpx.post(
f"{BASE}/mcp",
headers=HEADERS,
json={"jsonrpc": "2.0", "id": req_id, "method": method, "params": params},
timeout=CLIENT_TIMEOUT,
)
body = r.json()
if "error" in body:
status = f"ERR {body['error'].get('code')}: {body['error'].get('message', '')[:90]}"
else:
text = body["result"]["content"][0]["text"] if method == "tools/call" else "{}"
payload = json.loads(text)
n = len(payload.get("results", [])) if isinstance(payload, dict) else "?"
status = f"OK results={n}"
except Exception as exc: # noqa: BLE001
status = f"EXC {type(exc).__name__}: {exc}"[:100]
return time.monotonic() - t0, status
def health() -> float:
t0 = time.monotonic()
try:
httpx.get(f"{BASE}/health", timeout=CLIENT_TIMEOUT)
except Exception: # noqa: BLE001
return -1.0
return time.monotonic() - t0
def main() -> None:
queries = ["failover keepalived", "irrigazione zone", "turni medici", "backup nas",
"runbook", "rete vlan", "home assistant", "paperless"]
t_start = time.monotonic()
with cf.ThreadPoolExecutor(max_workers=N + 2) as pool:
futs = {
pool.submit(rpc, "tools/call",
{"name": TOOL, "arguments": {"query": queries[i % len(queries)], "limit": 3}},
i + 1): f"{TOOL}#{i + 1}"
for i in range(N)
}
time.sleep(0.5)
h = pool.submit(health)
p = pool.submit(rpc, "tools/call", {"name": "ping", "arguments": {}}, 999)
print(f"/health durante il carico: {h.result():.2f}s")
pt, ps = p.result()
print(f"ping durante il carico: {pt:.2f}s {ps}")
for fut in cf.as_completed(futs):
dt, status = fut.result()
print(f"{futs[fut]:<24} {dt:6.2f}s {status}")
print(f"Totale: {time.monotonic() - t_start:.2f}s (N={N})")
if __name__ == "__main__":
main()
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Conformità Streamable HTTP / lifecycle MCP: negoziazione versione, 202 per notifiche, header.
Uso (dentro il container):
docker exec loogle-mcp python /srv/scripts/test_mcp_protocol.py
"""
import os
import sys
import httpx
sys.path.insert(0, "/srv")
from app.db import get_conn # noqa: E402
from app.jwt_utils import create_access_token # noqa: E402
BASE = os.environ.get("TEST_BASE", "http://127.0.0.1:8700") + "/mcp"
row = get_conn().execute("SELECT id, username FROM users WHERE username='daniele'").fetchone()
token, _ = create_access_token(dict(row), "context:read", "protocol-test")
AUTH = {"Authorization": f"Bearer {token}"}
failures = 0
def check(label: str, cond: bool, detail: str = "") -> None:
global failures
failures += 0 if cond else 1
print(f"{'OK ' if cond else 'FAIL'} {label} {detail}")
def init(version):
params = {"protocolVersion": version, "capabilities": {}, "clientInfo": {"name": "protocol-test"}}
r = httpx.post(BASE, json={"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": params})
return r.json()["result"]["protocolVersion"]
for requested, expected in [("2025-06-18", "2025-06-18"), ("2025-03-26", "2025-03-26"),
("2024-11-05", "2024-11-05"), ("2099-01-01", "2025-06-18")]:
got = init(requested)
check(f"initialize {requested}", got == expected, f"-> {got}")
r = httpx.post(BASE, json={"jsonrpc": "2.0", "method": "notifications/initialized"})
check("notifica non autenticata initialized -> 202", r.status_code == 202 and not r.content, str(r.status_code))
r = httpx.post(BASE, headers=AUTH, json={"jsonrpc": "2.0", "method": "notifications/cancelled",
"params": {"requestId": 5}})
check("notifica cancelled -> 202", r.status_code == 202, str(r.status_code))
r = httpx.post(BASE, json={"jsonrpc": "2.0", "method": "notifications/cancelled"})
check("notifica non autenticata non ammessa -> 401", r.status_code == 401, str(r.status_code))
r = httpx.post(BASE, headers=AUTH, json=[{"jsonrpc": "2.0", "method": "notifications/initialized"},
{"jsonrpc": "2.0", "id": 7, "method": "ping"}])
body = r.json()
check("batch notifica+request -> solo 1 risposta", r.status_code == 200 and len(body) == 1 and body[0]["id"] == 7,
str(body))
r = httpx.post(BASE, headers={**AUTH, "MCP-Protocol-Version": "2025-06-18"},
json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
check("header versione valida -> 200", r.status_code == 200 and "tools" in r.json()["result"], str(r.status_code))
r = httpx.post(BASE, headers={**AUTH, "MCP-Protocol-Version": "1999-01-01"},
json={"jsonrpc": "2.0", "id": 3, "method": "tools/list"})
check("header versione non supportata -> 400", r.status_code == 400, str(r.status_code))
r = httpx.post(BASE, json={"jsonrpc": "2.0", "id": 4, "method": "tools/list"})
check("request senza token -> 401 + WWW-Authenticate",
r.status_code == 401 and "resource_metadata" in r.headers.get("www-authenticate", ""), str(r.status_code))
r = httpx.post(BASE, headers=AUTH, json=[])
check("batch vuoto -> 400", r.status_code == 400, str(r.status_code))
print(f"Errori: {failures}")
sys.exit(1 if failures else 0)
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Regressione: chiama ogni tool MCP senza argomenti obbligatori (sola lettura) e ne misura la durata.
Uso (dentro il container):
docker exec loogle-mcp python /srv/scripts/test_mcp_readonly_tools.py
"""
import json
import os
import sys
import time
import httpx
sys.path.insert(0, "/srv")
from app.db import get_conn # noqa: E402
from app.jwt_utils import create_access_token # noqa: E402
BASE = os.environ.get("TEST_BASE", "http://127.0.0.1:8700")
# Tool senza argomenti obbligatori ma con effetti collaterali: esclusi
SKIP = {"reindex_apps"}
row = get_conn().execute("SELECT id, username FROM users WHERE username='daniele'").fetchone()
scope = ("context:read knowledge:read gitea:read home:read irrigation:read turni:read admin")
token, _ = create_access_token({"id": row["id"], "username": row["username"]}, scope, "regression")
HEADERS = {"Authorization": f"Bearer {token}"}
def rpc(method: str, params: dict) -> dict:
r = httpx.post(f"{BASE}/mcp", headers=HEADERS, timeout=120,
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params})
return r.json()
tools = rpc("tools/list", {})["result"]["tools"]
failures = 0
for tool in tools:
if tool["inputSchema"].get("required") or tool["name"] in SKIP:
continue
t0 = time.monotonic()
body = rpc("tools/call", {"name": tool["name"], "arguments": {}})
dt = time.monotonic() - t0
if "error" in body:
status = f"ERR {body['error']['message'][:80]}"
failures += 1
else:
status = f"OK {len(body['result']['content'][0]['text'])} bytes"
print(f"{tool['name']:<30} {dt:6.2f}s {status}")
print(f"Tool totali: {len(tools)}, errori: {failures}")
@@ -0,0 +1,115 @@
#!/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)
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.