Backup automatico script del 2026-09-27 07:00
This commit is contained in:
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)
|
||||