Backup automatico script del 2026-08-23 12:04
This commit is contained in:
1 parent
11823447a2
commit
795a15a7b6
66 files changed
+8546
No files matched your search
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
echo "Attendo health..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://127.0.0.1:8700/health >/dev/null; then
|
||||
echo "OK — loogle-mcp attivo su :8700"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Health check fallito — vedi docker logs loogle-mcp" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
mkdir -p /data /data/context
|
||||
|
||||
# Chiave SSH per thermal gate (permessi OpenSSH)
|
||||
if [ -f /run/secrets/ds920_ssh_key ]; then
|
||||
mkdir -p /root/.ssh
|
||||
cp /run/secrets/ds920_ssh_key /root/.ssh/ds920_key
|
||||
chmod 600 /root/.ssh/ds920_key
|
||||
export DS920_SSH_KEY=/root/.ssh/ds920_key
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Probe temperatura/load CPU DS920 — HTTP GET /thermal su :9191.
|
||||
|
||||
Avvio (sul DS920, utente daniely):
|
||||
nohup python3 ds920_thermal_probe.py >> /tmp/thermal-probe.log 2>&1 &
|
||||
|
||||
O via Task Scheduler Synology all'avvio.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
|
||||
PORT = int(os.environ.get("THERMAL_PROBE_PORT", "9191"))
|
||||
HWMON = os.environ.get("THERMAL_HWMON", "/sys/class/hwmon/hwmon0")
|
||||
|
||||
|
||||
def read_metrics() -> dict:
|
||||
temps = []
|
||||
try:
|
||||
for name in sorted(os.listdir(HWMON)):
|
||||
if name.startswith("temp") and name.endswith("_input"):
|
||||
with open(os.path.join(HWMON, name), encoding="utf-8") as f:
|
||||
temps.append(int(f.read().strip()) / 1000.0)
|
||||
except OSError:
|
||||
pass
|
||||
load1, load5, load15 = os.getloadavg()
|
||||
nproc = os.cpu_count() or 4
|
||||
return {
|
||||
"ok": True,
|
||||
"host": os.uname().nodename,
|
||||
"cpu_temp_c": max(temps) if temps else None,
|
||||
"temps_c": temps,
|
||||
"load1": load1,
|
||||
"load5": load5,
|
||||
"load15": load15,
|
||||
"nproc": nproc,
|
||||
"cpu_target_load": round(nproc * 0.75, 2),
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args) -> None: # noqa: A003
|
||||
return
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if self.path.split("?")[0] not in ("/", "/thermal", "/health"):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
body = json.dumps(read_metrics()).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
||||
print(f"thermal probe listening on 0.0.0.0:{PORT}", flush=True)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Grant view permissions on non-personal Paperless documents to family users."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
|
||||
PAPERLESS_URL = os.environ.get("PAPERLESS_URL", "https://docs.loogle.it").rstrip("/")
|
||||
ADMIN_TOKEN = os.environ.get("PAPERLESS_API_TOKEN_DANIELE") or os.environ.get("PAPERLESS_API_TOKEN", "")
|
||||
FAMILY_USER_IDS = [4, 5, 6] # lucia, davide, luca
|
||||
PERSONAL_TAG_NAMES = {"personal", "privato", "private"}
|
||||
BATCH_SIZE = 50
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not ADMIN_TOKEN:
|
||||
print("Missing PAPERLESS_API_TOKEN_DANIELE", file=sys.stderr)
|
||||
return 1
|
||||
headers = {"Authorization": f"Token {ADMIN_TOKEN}", "Content-Type": "application/json"}
|
||||
|
||||
tags_resp = httpx.get(f"{PAPERLESS_URL}/api/tags/", headers=headers, timeout=60)
|
||||
tags_resp.raise_for_status()
|
||||
personal_tag_ids = {
|
||||
t["id"]
|
||||
for t in tags_resp.json().get("results", [])
|
||||
if (t.get("name") or "").lower() in PERSONAL_TAG_NAMES
|
||||
}
|
||||
|
||||
doc_ids: list[int] = []
|
||||
page = 1
|
||||
while True:
|
||||
resp = httpx.get(
|
||||
f"{PAPERLESS_URL}/api/documents/",
|
||||
headers=headers,
|
||||
params={"page": page, "page_size": 100},
|
||||
timeout=60,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
for doc in data.get("results", []):
|
||||
if set(doc.get("tags") or []) & personal_tag_ids:
|
||||
continue
|
||||
doc_ids.append(doc["id"])
|
||||
if not data.get("next"):
|
||||
break
|
||||
page += 1
|
||||
|
||||
print(f"Documenti da condividere (senza tag personali): {len(doc_ids)}")
|
||||
|
||||
updated = 0
|
||||
for i in range(0, len(doc_ids), BATCH_SIZE):
|
||||
chunk = doc_ids[i : i + BATCH_SIZE]
|
||||
payload = {
|
||||
"documents": chunk,
|
||||
"method": "set_permissions",
|
||||
"parameters": {
|
||||
"set_permissions": {
|
||||
"view": {"users": FAMILY_USER_IDS, "groups": []},
|
||||
"change": {"users": [], "groups": []},
|
||||
},
|
||||
"merge": True,
|
||||
},
|
||||
}
|
||||
resp = httpx.post(
|
||||
f"{PAPERLESS_URL}/api/documents/bulk_edit/",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=120,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
updated += len(chunk)
|
||||
print(f" Aggiornati {updated}/{len(doc_ids)}")
|
||||
|
||||
for name, env_key in [
|
||||
("lucia", "PAPERLESS_API_TOKEN_LUCIA"),
|
||||
("davide", "PAPERLESS_API_TOKEN_DAVIDE"),
|
||||
("luca", "PAPERLESS_API_TOKEN_LUCA"),
|
||||
]:
|
||||
token = os.environ.get(env_key, "")
|
||||
if not token:
|
||||
continue
|
||||
count = httpx.get(
|
||||
f"{PAPERLESS_URL}/api/documents/?page_size=1",
|
||||
headers={"Authorization": f"Token {token}"},
|
||||
timeout=30,
|
||||
).json().get("count", 0)
|
||||
print(f"{name}: documenti visibili = {count}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Indicizzazione Gitea one-shot — eseguire con indexer fermo per evitare OOM."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
env_path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
with open(env_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
if key.strip() and key.strip() not in os.environ:
|
||||
os.environ[key.strip()] = value.strip().strip("'").strip('"')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Indicizza repo Gitea nel vector store")
|
||||
parser.add_argument("--repo", help="owner/name (default: tutti i repo configurati)")
|
||||
parser.add_argument("--user", default="daniele", help="Utente MCP/Gitea token")
|
||||
parser.add_argument("--max-files", type=int, default=0, help="Limite file per repo (0=config)")
|
||||
parser.add_argument("--force", action="store_true", help="Re-indicizza anche file invariati")
|
||||
args = parser.parse_args()
|
||||
|
||||
_load_dotenv()
|
||||
if os.path.isfile("/.dockerenv"):
|
||||
os.environ.setdefault("MCP_DB", "/data/loogle_mcp.db")
|
||||
os.environ.setdefault("MCP_VECTOR_FALLBACK", "/data/vector_fallback.db")
|
||||
|
||||
from app.db import init_db
|
||||
from app.knowledge import gitea_indexer
|
||||
|
||||
init_db()
|
||||
max_files = args.max_files or None
|
||||
|
||||
if args.repo:
|
||||
result = gitea_indexer.index_repo(
|
||||
args.repo,
|
||||
username=args.user,
|
||||
force=args.force,
|
||||
max_files=max_files,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
result = gitea_indexer.index_all(max_files_per_repo=max_files)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
stats = gitea_indexer.index_stats()
|
||||
print("stats", json.dumps(stats, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Crea un progetto demo per ogni utente famiglia."""
|
||||
import sys
|
||||
sys.path.insert(0, "/srv")
|
||||
from app.context import store as context_store
|
||||
|
||||
USERS = {
|
||||
"daniele": "Homelab LOOGLE",
|
||||
"lucia": "Progetti personali",
|
||||
"davide": "Studio e task",
|
||||
"luca": "Progetti Luca",
|
||||
}
|
||||
|
||||
for username, title in USERS.items():
|
||||
root = context_store._user_root(username)
|
||||
existing = context_store.list_projects(username)
|
||||
if existing:
|
||||
print(f"{username}: skip ({len(existing)} progetti)")
|
||||
continue
|
||||
meta = context_store.create_project(username, title, tags=["demo"])
|
||||
context_store.save_context(
|
||||
username,
|
||||
meta["id"],
|
||||
f"Progetto demo iniziale per {username}. Usa save_context per ampliare la memoria agente.",
|
||||
)
|
||||
print(f"{username}: creato {meta['id']}")
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Crea repo progetti per davide/luca e progetti Loogle collegati."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
os.environ[k.strip()] = v.strip().strip("'").strip('"')
|
||||
|
||||
|
||||
def ensure_repo(user: str, repo_name: str, description: str) -> str:
|
||||
from app.knowledge import gitea
|
||||
|
||||
gitea._tokens_cache = None
|
||||
full = f"{user}/{repo_name}"
|
||||
existing = {r["full_name"] for r in gitea.list_repos(username=user)["repos"]}
|
||||
if full in existing:
|
||||
print(f"repo_exists: {full}")
|
||||
return full
|
||||
created = gitea.create_repo(
|
||||
repo_name,
|
||||
username=user,
|
||||
private=True,
|
||||
description=description,
|
||||
auto_init=True,
|
||||
)
|
||||
print(f"repo_created: {created['full_name']}")
|
||||
return created["full_name"]
|
||||
|
||||
|
||||
def ensure_mcp_project(user: str, project_title: str, gitea_repo: str) -> None:
|
||||
from app.context import store as context_store
|
||||
|
||||
projects = {p["id"]: p for p in context_store.list_projects(user, include_archived=True)}
|
||||
for meta in projects.values():
|
||||
if meta.get("gitea_repo") == gitea_repo:
|
||||
print(f"project_linked: {user}/{meta['id']} -> {gitea_repo}")
|
||||
return
|
||||
slug = project_title.lower().replace(" ", "-")
|
||||
if slug in projects:
|
||||
meta = context_store.link_project_repo(user, slug, gitea_repo=gitea_repo, seed_from_gitea=True)
|
||||
print(f"project_updated: {user}/{meta['id']} -> {gitea_repo}")
|
||||
return
|
||||
meta = context_store.create_project(
|
||||
user,
|
||||
project_title,
|
||||
tags=["gitea", "p4"],
|
||||
gitea_repo=gitea_repo,
|
||||
seed_from_gitea=True,
|
||||
)
|
||||
print(f"project_created: {user}/{meta['id']} -> {gitea_repo}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_dotenv()
|
||||
os.environ.setdefault("MCP_DB", os.environ.get("MCP_DB", "/data/loogle_mcp.db"))
|
||||
from app.db import init_db
|
||||
|
||||
init_db()
|
||||
|
||||
ensure_repo(
|
||||
"davide",
|
||||
"progetti",
|
||||
"Workspace personale — codice e note generate con Loogle MCP",
|
||||
)
|
||||
ensure_repo(
|
||||
"luca",
|
||||
"progetti",
|
||||
"Workspace personale — codice e note generate con Loogle MCP",
|
||||
)
|
||||
|
||||
ensure_mcp_project("davide", "Progetti Davide", "davide/progetti")
|
||||
ensure_mcp_project("luca", "Progetti Luca", "luca/progetti")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test integrazione Gitea MCP — mock server + opzionale live API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
MOCK_TOKEN = "test-gitea-token"
|
||||
MOCK_PORT = 18799
|
||||
|
||||
os.environ["GITEA_URL"] = "https://git.loogle.it"
|
||||
os.environ["GITEA_API_URL"] = f"http://127.0.0.1:{MOCK_PORT}"
|
||||
os.environ["GITEA_API_TOKEN_DANIELE"] = MOCK_TOKEN
|
||||
os.environ["MCP_DB"] = "/tmp/loogle_mcp_test.db"
|
||||
|
||||
|
||||
class GiteaMockHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
def _auth_ok(self) -> bool:
|
||||
auth = self.headers.get("Authorization", "")
|
||||
return auth == f"token {MOCK_TOKEN}"
|
||||
|
||||
def _json(self, code: int, payload):
|
||||
body = json.dumps(payload).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
if not self._auth_ok():
|
||||
self._json(401, {"message": "invalid token"})
|
||||
return
|
||||
path = urlparse(self.path).path
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
|
||||
if path == "/api/v1/user/repos":
|
||||
self._json(200, [
|
||||
{
|
||||
"full_name": "daniele/rete",
|
||||
"description": "Infra HA",
|
||||
"private": True,
|
||||
"html_url": "https://git.loogle.it/daniele/rete",
|
||||
"default_branch": "main",
|
||||
"updated_at": "2026-08-22T10:00:00Z",
|
||||
}
|
||||
])
|
||||
return
|
||||
|
||||
if path == "/api/v1/repos/daniele/rete/contents/ha/RUNBOOK-failover.md":
|
||||
content = "# Failover tier-b\n\nloogle-mcp nel pivot tier-b.\n"
|
||||
import base64
|
||||
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"type": "file",
|
||||
"path": "ha/RUNBOOK-failover.md",
|
||||
"content": base64.b64encode(content.encode()).decode(),
|
||||
"encoding": "base64",
|
||||
"size": len(content),
|
||||
"sha": "abc123",
|
||||
"html_url": "https://git.loogle.it/daniele/rete/src/branch/main/ha/RUNBOOK-failover.md",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/v1/search/code":
|
||||
q = (qs.get("q") or [""])[0]
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"repository": {"full_name": "daniele/rete"},
|
||||
"path": "ha/RUNBOOK-failover.md",
|
||||
"sha": "abc123",
|
||||
"content": f"...{q}...",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/v1/repos/daniele/rete/issues":
|
||||
self._json(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test issue",
|
||||
"state": "open",
|
||||
"user": {"login": "daniele"},
|
||||
"html_url": "https://git.loogle.it/daniele/rete/issues/1",
|
||||
"created_at": "2026-08-22T10:00:00Z",
|
||||
"updated_at": "2026-08-22T10:00:00Z",
|
||||
"labels": [],
|
||||
}
|
||||
],
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/v1/repos/daniele/rete/issues/1":
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test issue",
|
||||
"state": "open",
|
||||
"body": "Corpo issue di test",
|
||||
"user": {"login": "daniele"},
|
||||
"html_url": "https://git.loogle.it/daniele/rete/issues/1",
|
||||
"created_at": "2026-08-22T10:00:00Z",
|
||||
"updated_at": "2026-08-22T10:00:00Z",
|
||||
"labels": [],
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
self._json(404, {"message": f"not found: {path}"})
|
||||
|
||||
def do_POST(self):
|
||||
if not self._auth_ok():
|
||||
self._json(401, {"message": "invalid token"})
|
||||
return
|
||||
path = urlparse(self.path).path
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length).decode() or "{}")
|
||||
if path == "/api/v1/repos/daniele/rete/issues":
|
||||
self._json(
|
||||
201,
|
||||
{
|
||||
"number": 42,
|
||||
"title": body.get("title"),
|
||||
"state": "open",
|
||||
"html_url": "https://git.loogle.it/daniele/rete/issues/42",
|
||||
},
|
||||
)
|
||||
return
|
||||
self._json(404, {"message": f"not found: {path}"})
|
||||
|
||||
|
||||
def run_mock_tests() -> None:
|
||||
from app.db import init_db
|
||||
from app.knowledge import gitea as gitea_mod
|
||||
from app.mcp import tools
|
||||
|
||||
init_db()
|
||||
gitea_mod._tokens_cache = None
|
||||
|
||||
server = HTTPServer(("127.0.0.1", MOCK_PORT), GiteaMockHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
|
||||
claims = {"sub": "daniele", "scope": "gitea:read gitea:write"}
|
||||
|
||||
repos = tools.call_tool("list_repos", {}, claims)
|
||||
assert "daniele/rete" in repos["content"][0]["text"]
|
||||
|
||||
file_data = tools.call_tool(
|
||||
"get_file",
|
||||
{"repo": "daniele/rete", "path": "ha/RUNBOOK-failover.md"},
|
||||
claims,
|
||||
)
|
||||
assert "tier-b" in file_data["content"][0]["text"]
|
||||
|
||||
search = tools.call_tool(
|
||||
"search_code",
|
||||
{"query": "tier-b", "repo": "daniele/rete"},
|
||||
claims,
|
||||
)
|
||||
assert "RUNBOOK-failover" in search["content"][0]["text"]
|
||||
|
||||
issues = tools.call_tool("list_issues", {"repo": "daniele/rete"}, claims)
|
||||
assert "Test issue" in issues["content"][0]["text"]
|
||||
|
||||
issue = tools.call_tool(
|
||||
"get_issue",
|
||||
{"repo": "daniele/rete", "number": 1},
|
||||
claims,
|
||||
)
|
||||
assert "Corpo issue" in issue["content"][0]["text"]
|
||||
|
||||
created = tools.call_tool(
|
||||
"create_issue",
|
||||
{"repo": "daniele/rete", "title": "Da MCP", "body": "Test"},
|
||||
claims,
|
||||
)
|
||||
assert "42" in created["content"][0]["text"]
|
||||
|
||||
server.shutdown()
|
||||
print("OK mock: list_repos, get_file, search_code, list_issues, get_issue, create_issue")
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
env_path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
with open(env_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip().strip("'").strip('"')
|
||||
if key.startswith("GITEA_"):
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def run_live_tests() -> None:
|
||||
_load_dotenv()
|
||||
from app.knowledge import gitea as gitea_mod
|
||||
from app.mcp import tools
|
||||
|
||||
token = os.environ.get("GITEA_API_TOKEN_DANIELE", "").strip()
|
||||
if not token or token == MOCK_TOKEN:
|
||||
print("SKIP live: GITEA_API_TOKEN_DANIELE non impostato (token reale in .env)")
|
||||
return
|
||||
|
||||
api_url = gitea_mod.api_base_url()
|
||||
if str(MOCK_PORT) in api_url:
|
||||
print("SKIP live: GITEA_API_URL punta al mock")
|
||||
return
|
||||
|
||||
gitea_mod._tokens_cache = None
|
||||
claims = {"sub": "daniele", "scope": "gitea:read gitea:write"}
|
||||
|
||||
repos = gitea_mod.list_repos(username="daniele")
|
||||
if not repos.get("repos"):
|
||||
raise RuntimeError("live list_repos: nessun repository")
|
||||
|
||||
first = repos["repos"][0]["full_name"]
|
||||
tools.call_tool("list_repos", {}, claims)
|
||||
print(f"OK live list_repos: {len(repos['repos'])} repo, primo={first}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
run_mock_tests()
|
||||
run_live_tests()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test P4: scrittura Gitea (create repo, create/update file)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
os.environ[k.strip()] = v.strip().strip("'").strip('"')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_dotenv()
|
||||
os.environ.setdefault("MCP_DB", "/data/loogle_mcp.db")
|
||||
|
||||
from app.db import init_db
|
||||
from app.knowledge import gitea
|
||||
from app.mcp import tools
|
||||
|
||||
init_db()
|
||||
gitea._tokens_cache = None
|
||||
|
||||
if not os.environ.get("GITEA_API_TOKEN_DANIELE"):
|
||||
print("ERR: token daniele assente")
|
||||
return 1
|
||||
|
||||
claims = {"sub": "daniele", "scope": "gitea:read gitea:write knowledge:read admin"}
|
||||
|
||||
test_repo = "daniele/mcp-p4-test"
|
||||
test_path = "docs/p4-verify.md"
|
||||
test_content = "# P4 verify\n\nFile creato da test_gitea_p4.py\n"
|
||||
|
||||
# cleanup file if exists from prior run
|
||||
try:
|
||||
gitea.get_file(test_repo, test_path, username="daniele")
|
||||
gitea.create_or_update_file(
|
||||
test_repo,
|
||||
test_path,
|
||||
test_content + "\n(updated)\n",
|
||||
"test p4 update",
|
||||
username="daniele",
|
||||
)
|
||||
action = "update"
|
||||
except FileNotFoundError:
|
||||
# ensure repo exists - use temp repo name under daniele
|
||||
repos = {r["full_name"] for r in gitea.list_repos(username="daniele")["repos"]}
|
||||
if test_repo not in repos:
|
||||
created = tools.call_tool(
|
||||
"create_gitea_repo",
|
||||
{
|
||||
"name": "mcp-p4-test",
|
||||
"private": True,
|
||||
"description": "Repo temporaneo test P4",
|
||||
},
|
||||
claims,
|
||||
)
|
||||
print("create_repo", created["content"][0]["text"][:200])
|
||||
written = tools.call_tool(
|
||||
"create_or_update_file",
|
||||
{
|
||||
"repo": test_repo,
|
||||
"path": test_path,
|
||||
"content": test_content,
|
||||
"message": "test p4 create",
|
||||
"reindex": False,
|
||||
},
|
||||
claims,
|
||||
)
|
||||
payload = json.loads(written["content"][0]["text"])
|
||||
action = payload.get("action")
|
||||
print("write", action, payload.get("path"))
|
||||
|
||||
read = gitea.get_file(test_repo, test_path, username="daniele")
|
||||
assert "P4 verify" in read.get("content", ""), read
|
||||
print("read_ok", read["path"])
|
||||
|
||||
for user, repo in (("davide", "davide/progetti"), ("luca", "luca/progetti")):
|
||||
token_key = f"GITEA_API_TOKEN_{user.upper()}"
|
||||
if not os.environ.get(token_key):
|
||||
print(f"SKIP {user}: no token")
|
||||
continue
|
||||
gitea._tokens_cache = None
|
||||
listed = gitea.list_repos(username=user)
|
||||
names = {r["full_name"] for r in listed["repos"]}
|
||||
if repo not in names:
|
||||
raise RuntimeError(f"repo mancante per {user}: {repo}")
|
||||
print(f"{user}_repo_ok", repo)
|
||||
|
||||
print("OK P4: create_or_update_file + repos davide/luca/progetti")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test P3: RAG semantico su file Gitea indicizzati."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
env_path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
with open(env_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value.strip().strip("'").strip('"')
|
||||
|
||||
|
||||
def _configure_paths() -> str:
|
||||
in_container = os.path.isfile("/.dockerenv") or (
|
||||
os.path.isdir("/data") and os.access("/data", os.W_OK)
|
||||
)
|
||||
if in_container:
|
||||
os.environ.setdefault("MCP_DB", "/data/loogle_mcp.db")
|
||||
os.environ.setdefault("MCP_VECTOR_FALLBACK", "/data/vector_fallback.db")
|
||||
return "container"
|
||||
os.environ["MCP_DB"] = "/tmp/loogle_mcp_p3_test.db"
|
||||
os.environ["MCP_VECTOR_FALLBACK"] = "/tmp/loogle_mcp_p3_vectors.db"
|
||||
return "host"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_dotenv()
|
||||
mode = _configure_paths()
|
||||
print(f"mode={mode} db={os.environ['MCP_DB']} vector={os.environ['MCP_VECTOR_FALLBACK']}")
|
||||
|
||||
from app.db import get_conn, init_db
|
||||
from app.knowledge import gitea_indexer
|
||||
from app.mcp import tools
|
||||
|
||||
init_db()
|
||||
if not get_conn().execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE name='indexed_gitea_files'"
|
||||
).fetchone():
|
||||
raise RuntimeError("tabella indexed_gitea_files assente")
|
||||
|
||||
if not os.environ.get("GITEA_API_TOKEN_DANIELE", "").strip():
|
||||
print("SKIP: GITEA_API_TOKEN_DANIELE assente")
|
||||
return 1
|
||||
|
||||
repo = "daniele/rete"
|
||||
existing = gitea_indexer.list_indexed_files(limit=5, repo=repo)
|
||||
stats = gitea_indexer.index_stats()
|
||||
if len(existing) < 1 or stats.get("qdrant_points", 0) < 1:
|
||||
index_result = gitea_indexer.index_repo(
|
||||
repo,
|
||||
username="daniele",
|
||||
force=True,
|
||||
max_files=1,
|
||||
)
|
||||
print("index", json.dumps(index_result, ensure_ascii=False))
|
||||
else:
|
||||
print("index_skip", "already", len(existing), "files")
|
||||
|
||||
files = gitea_indexer.list_indexed_files(limit=5, repo=repo)
|
||||
if not files:
|
||||
raise RuntimeError("indexed_gitea_files vuota")
|
||||
print("indexed_sample", [f["path"] for f in files[:3]])
|
||||
|
||||
claims = {"sub": "daniele", "scope": "knowledge:read gitea:read admin", "admin": "admin"}
|
||||
for query in ("failover tier-b", "runbook"):
|
||||
hits = gitea_indexer.search_gitea_knowledge("daniele", query, limit=5, is_admin=True)
|
||||
if not hits:
|
||||
raise RuntimeError(f"search_gitea_knowledge senza risultati per: {query}")
|
||||
top = hits[0]
|
||||
print(
|
||||
f"search_ok[{query}]",
|
||||
top.get("path") or top.get("title"),
|
||||
round(float(top.get("score", 0)), 3),
|
||||
)
|
||||
|
||||
tool_out = tools.call_tool(
|
||||
"search_gitea_knowledge",
|
||||
{"query": "failover", "limit": 5},
|
||||
claims,
|
||||
)
|
||||
if not json.loads(tool_out["content"][0]["text"]).get("results"):
|
||||
raise RuntimeError("tool search_gitea_knowledge vuoto")
|
||||
|
||||
unified = tools.call_tool(
|
||||
"search_knowledge",
|
||||
{"query": "FAILOVER CENSIMENTO documenti servizi", "limit": 12},
|
||||
claims,
|
||||
)
|
||||
unified_payload = json.loads(unified["content"][0]["text"])
|
||||
results = unified_payload.get("results") or []
|
||||
sources = set()
|
||||
for r in results:
|
||||
if r.get("repo"):
|
||||
sources.add("gitea")
|
||||
elif r.get("source"):
|
||||
sources.add(r["source"])
|
||||
else:
|
||||
sources.add("paperless")
|
||||
print("search_knowledge_sources", sorted(sources))
|
||||
if "gitea" not in sources:
|
||||
raise RuntimeError("search_knowledge non include risultati Gitea")
|
||||
|
||||
list_tool = tools.call_tool("list_gitea_indexed_files", {"repo": repo, "limit": 5}, claims)
|
||||
if json.loads(list_tool["content"][0]["text"]).get("count", 0) <= 0:
|
||||
raise RuntimeError("list_gitea_indexed_files vuoto")
|
||||
|
||||
print("OK P3: search + unified + list (reindex opzionale via tool reindex_gitea_repo)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Verifica P5: tool live Loogle Casa + Home Assistant."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/srv" if os.path.isdir("/srv/app") else os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from app.integrations import casa, homeassistant
|
||||
from app.mcp import tools
|
||||
|
||||
user = os.environ.get("MCP_TEST_USER", "daniele")
|
||||
claims = {
|
||||
"sub": user,
|
||||
"scope": "home:read context:read knowledge:read gitea:read admin",
|
||||
}
|
||||
errors = []
|
||||
|
||||
print("=== P5 integrations ===")
|
||||
print("casa configured:", casa.is_configured(user))
|
||||
print("ha configured:", homeassistant.is_configured())
|
||||
|
||||
if casa.is_configured(user):
|
||||
for tool, args in (
|
||||
("get_home_dashboard", {}),
|
||||
("get_home_weather", {}),
|
||||
("get_network_overview", {}),
|
||||
("get_network_failover_status", {}),
|
||||
):
|
||||
try:
|
||||
out = tools.call_tool(tool, args, claims)
|
||||
text = out["content"][0]["text"]
|
||||
data = json.loads(text)
|
||||
print(f"OK {tool}: keys={list(data.keys())[:8]}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL {tool}: {exc}")
|
||||
errors.append(tool)
|
||||
else:
|
||||
errors.append("casa-not-configured")
|
||||
|
||||
if homeassistant.is_configured():
|
||||
try:
|
||||
cfg = homeassistant.get_config()
|
||||
print(f"OK ha config: version={cfg.get('version')}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL ha config: {exc}")
|
||||
errors.append("ha-config")
|
||||
try:
|
||||
out = tools.call_tool("list_ha_entities", {"domain": "switch", "limit": 5}, claims)
|
||||
data = json.loads(out["content"][0]["text"])
|
||||
print(f"OK list_ha_entities: count={len(data.get('entities', []))}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL list_ha_entities: {exc}")
|
||||
errors.append("list_ha_entities")
|
||||
else:
|
||||
errors.append("ha-not-configured")
|
||||
|
||||
if errors:
|
||||
print("P5 FAILED:", errors)
|
||||
return 1
|
||||
print("P5 OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Verifica P6: tool live Irrigazione + Turni."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/srv" if os.path.isdir("/srv/app") else os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from app.integrations import irrigazione, turni
|
||||
from app.mcp import tools
|
||||
|
||||
user = os.environ.get("MCP_TEST_USER", "daniele")
|
||||
claims = {
|
||||
"sub": user,
|
||||
"scope": "irrigation:read turni:read home:read admin",
|
||||
}
|
||||
errors = []
|
||||
|
||||
print("=== P6 integrations ===")
|
||||
print("irrigazione configured:", irrigazione.is_configured(user))
|
||||
print("turni configured:", turni.is_configured(user))
|
||||
|
||||
if irrigazione.is_configured(user):
|
||||
for tool, args in (
|
||||
("get_irrigation_status", {}),
|
||||
("get_irrigation_zones", {}),
|
||||
("get_irrigation_history", {"limit": 5}),
|
||||
):
|
||||
try:
|
||||
out = tools.call_tool(tool, args, claims)
|
||||
data = json.loads(out["content"][0]["text"])
|
||||
print(f"OK {tool}: type={type(data).__name__}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL {tool}: {exc}")
|
||||
errors.append(tool)
|
||||
else:
|
||||
errors.append("irrigazione-not-configured")
|
||||
|
||||
try:
|
||||
out = tools.call_tool("get_turni_status", {}, claims)
|
||||
data = json.loads(out["content"][0]["text"])
|
||||
print(f"OK get_turni_status: build={data.get('buildVersion', '')[:30]}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL get_turni_status: {exc}")
|
||||
errors.append("get_turni_status")
|
||||
|
||||
if turni.is_configured(user):
|
||||
for tool, args in (
|
||||
("list_turni_doctors", {}),
|
||||
("get_my_shifts", {"limit": 10}),
|
||||
):
|
||||
try:
|
||||
out = tools.call_tool(tool, args, claims)
|
||||
data = json.loads(out["content"][0]["text"])
|
||||
print(f"OK {tool}: keys={list(data.keys())[:6]}")
|
||||
except Exception as exc:
|
||||
print(f"FAIL {tool}: {exc}")
|
||||
errors.append(tool)
|
||||
else:
|
||||
errors.append("turni-not-configured")
|
||||
|
||||
if errors:
|
||||
print("P6 FAILED:", errors)
|
||||
return 1
|
||||
print("P6 OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Verifica P7: RAG Irrigazione + Turni."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/srv" if os.path.isdir("/srv/app") else os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from app.db import init_db
|
||||
from app.knowledge import apps_indexer, indexer
|
||||
from app.mcp import tools
|
||||
|
||||
init_db()
|
||||
user = os.environ.get("MCP_TEST_USER", "daniele")
|
||||
claims = {
|
||||
"sub": user,
|
||||
"scope": "knowledge:read irrigation:read turni:read admin",
|
||||
}
|
||||
errors = []
|
||||
|
||||
print("=== P7 apps RAG ===")
|
||||
existing = apps_indexer.list_indexed_records(limit=5)
|
||||
if len(existing) >= 2:
|
||||
print(f"skip re-index: {len(existing)} records already present")
|
||||
result = {"irrigazione": {"indexed": len(existing), "skipped_reindex": True}, "turni": {}}
|
||||
else:
|
||||
result = {
|
||||
"irrigazione": apps_indexer.index_irrigazione(history_limit=0, events_limit=0),
|
||||
"turni": apps_indexer.index_turni(assignments_limit=3),
|
||||
}
|
||||
print("index_all:", json.dumps(result, ensure_ascii=False)[:500])
|
||||
|
||||
irr = result.get("irrigazione", {})
|
||||
turn = result.get("turni", {})
|
||||
if irr.get("skipped") and turn.get("skipped"):
|
||||
print("P7 FAILED: both sources skipped")
|
||||
return 1
|
||||
if irr.get("indexed", 0) + turn.get("indexed", 0) < 2 and not irr.get("skipped_reindex"):
|
||||
errors.append("insufficient-indexed-records")
|
||||
|
||||
records = apps_indexer.list_indexed_records(limit=10)
|
||||
print(f"indexed records: {len(records)}")
|
||||
if not records:
|
||||
errors.append("no-records")
|
||||
|
||||
try:
|
||||
hits = apps_indexer.search_apps_knowledge("irrigazione zona prato", limit=3)
|
||||
print(f"search_apps_knowledge irrigazione: {len(hits)} hits")
|
||||
if not hits:
|
||||
errors.append("search-irrigazione-empty")
|
||||
except Exception as exc:
|
||||
print(f"FAIL search_apps: {exc}")
|
||||
errors.append("search_apps")
|
||||
|
||||
try:
|
||||
out = tools.call_tool("search_apps_knowledge", {"query": "turno guardia", "limit": 3}, claims)
|
||||
data = json.loads(out["content"][0]["text"])
|
||||
print(f"OK tool search_apps_knowledge: {len(data.get('results', []))} hits")
|
||||
except Exception as exc:
|
||||
print(f"FAIL tool search_apps_knowledge: {exc}")
|
||||
errors.append("tool-search")
|
||||
|
||||
try:
|
||||
combined = indexer.search_knowledge(user, "irrigazione valvola", limit=5, is_admin=True)
|
||||
apps_hits = [h for h in combined if h.get("source") in ("irrigazione", "turni")]
|
||||
print(f"search_knowledge includes apps: {len(apps_hits)} app hits / {len(combined)} total")
|
||||
except Exception as exc:
|
||||
print(f"FAIL search_knowledge: {exc}")
|
||||
errors.append("search_knowledge")
|
||||
|
||||
if errors:
|
||||
print("P7 FAILED:", errors)
|
||||
return 1
|
||||
print("P7 OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
from app.db import init_db
|
||||
from app.knowledge.apps_indexer import (
|
||||
_summarize_irrigation_status,
|
||||
_index_text,
|
||||
list_indexed_records,
|
||||
search_apps_knowledge,
|
||||
)
|
||||
from app.integrations import irrigazione
|
||||
|
||||
init_db()
|
||||
print("1 db ok")
|
||||
existing = list_indexed_records(source="irrigazione", limit=1)
|
||||
if existing and existing[0].get("record_id") == "status-snapshot":
|
||||
print("2 skip index (status-snapshot exists)")
|
||||
else:
|
||||
s = irrigazione.get_status("daniele")
|
||||
print("3 status ok", len(s))
|
||||
text = _summarize_irrigation_status(s)
|
||||
print("4 summary", len(text))
|
||||
r = _index_text("irrigazione", "status-snapshot", "Stato", text)
|
||||
print("5 indexed", r)
|
||||
hits = search_apps_knowledge("irrigazione zona", limit=2)
|
||||
print("6 hits", len(hits))
|
||||
print("STEP OK")
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test P2: collegamento progetti MCP ↔ repository Gitea."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
os.environ["MCP_DB"] = "/tmp/loogle_mcp_p2_test.db"
|
||||
TEST_ROOT = tempfile.mkdtemp(prefix="mcp-p2-")
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
env_path = os.path.join(ROOT, ".env")
|
||||
if not os.path.isfile(env_path):
|
||||
return
|
||||
with open(env_path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
os.environ.setdefault(key.strip(), value.strip().strip("'").strip('"'))
|
||||
|
||||
|
||||
def run_unit_tests() -> None:
|
||||
os.environ["MCP_CONTEXT_ROOT"] = TEST_ROOT
|
||||
from app.db import init_db
|
||||
from app.context import store as context_store
|
||||
from app.mcp import tools
|
||||
|
||||
init_db()
|
||||
claims = {
|
||||
"sub": "daniele",
|
||||
"scope": "context:read context:write gitea:read gitea:write",
|
||||
}
|
||||
|
||||
meta = context_store.create_project(
|
||||
"daniele",
|
||||
"Progetto unit test",
|
||||
tags=["test"],
|
||||
)
|
||||
meta_path = os.path.join(TEST_ROOT, "daniele", "projects", meta["id"], "meta.json")
|
||||
meta["gitea_repo"] = "daniele/rete"
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
json.dump(meta, f, ensure_ascii=False, indent=2)
|
||||
|
||||
ctx = tools.call_tool(
|
||||
"get_project_context",
|
||||
{"project_id": meta["id"], "include_gitea": False},
|
||||
claims,
|
||||
)
|
||||
payload = json.loads(ctx["content"][0]["text"])
|
||||
assert payload["meta"]["gitea_repo"] == "daniele/rete"
|
||||
|
||||
unlinked = tools.call_tool(
|
||||
"link_project_repo",
|
||||
{"project_id": meta["id"], "gitea_repo": ""},
|
||||
claims,
|
||||
)
|
||||
payload = json.loads(unlinked["content"][0]["text"])
|
||||
assert "gitea_repo" not in payload
|
||||
|
||||
print("OK unit: meta gitea_repo + scollegamento locale")
|
||||
|
||||
|
||||
def run_live_tests() -> None:
|
||||
_load_dotenv()
|
||||
token = os.environ.get("GITEA_API_TOKEN_DANIELE", "").strip()
|
||||
if not token:
|
||||
print("SKIP live: GITEA_API_TOKEN_DANIELE assente")
|
||||
return
|
||||
|
||||
live_root = os.path.join(TEST_ROOT, "live")
|
||||
os.makedirs(live_root, exist_ok=True)
|
||||
os.environ["MCP_CONTEXT_ROOT"] = live_root
|
||||
|
||||
from app.context import store as context_store
|
||||
from app.mcp import tools
|
||||
from app.knowledge import gitea
|
||||
|
||||
gitea._tokens_cache = None
|
||||
claims = {
|
||||
"sub": "daniele",
|
||||
"scope": "context:read context:write gitea:read gitea:write",
|
||||
}
|
||||
|
||||
project_id = "rete-ha-p2-test"
|
||||
proj_dir = os.path.join(live_root, "daniele", "projects", project_id)
|
||||
if os.path.isdir(proj_dir):
|
||||
shutil.rmtree(proj_dir)
|
||||
|
||||
created = tools.call_tool(
|
||||
"create_project",
|
||||
{
|
||||
"title": "Rete HA P2 test",
|
||||
"tags": ["infra", "test"],
|
||||
"gitea_repo": "daniele/rete",
|
||||
"seed_from_gitea": True,
|
||||
},
|
||||
claims,
|
||||
)
|
||||
payload = json.loads(created["content"][0]["text"])
|
||||
project_id = payload["id"]
|
||||
assert payload.get("gitea_repo") == "daniele/rete"
|
||||
assert payload.get("gitea_seeded_at")
|
||||
|
||||
ctx_path = os.path.join(live_root, "daniele", "projects", project_id, "context.md")
|
||||
context_md = open(ctx_path, encoding="utf-8").read()
|
||||
assert "seed:gitea daniele/rete" in context_md
|
||||
assert len(context_md) > 200
|
||||
|
||||
enriched = tools.call_tool(
|
||||
"get_project_context",
|
||||
{"project_id": project_id, "include_gitea": True, "session_limit": 0},
|
||||
claims,
|
||||
)
|
||||
data = json.loads(enriched["content"][0]["text"])
|
||||
assert data["meta"]["gitea_repo"] == "daniele/rete"
|
||||
assert data["gitea"]["available"] is True
|
||||
assert data["gitea"]["readme"] is not None
|
||||
assert isinstance(data["gitea"].get("docs_files"), list)
|
||||
|
||||
link_existing = tools.call_tool(
|
||||
"link_project_repo",
|
||||
{"project_id": project_id, "gitea_repo": "daniele/rete", "seed_from_gitea": True},
|
||||
claims,
|
||||
)
|
||||
meta2 = json.loads(link_existing["content"][0]["text"])
|
||||
assert "gitea_seeded_at" in meta2
|
||||
|
||||
shutil.rmtree(proj_dir)
|
||||
print(f"OK live: create+seed+enrichment su daniele/rete (project {project_id})")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
run_unit_tests()
|
||||
run_live_tests()
|
||||
return 0
|
||||
finally:
|
||||
shutil.rmtree(TEST_ROOT, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
os.environ.setdefault('MCP_DB','/data/loogle_mcp.db')
|
||||
os.environ.setdefault('MCP_VECTOR_FALLBACK','/data/vector_fallback.db')
|
||||
from app.db import init_db, get_conn
|
||||
from app.knowledge import gitea_indexer
|
||||
from app.mcp import tools
|
||||
init_db()
|
||||
assert get_conn().execute("SELECT 1 FROM sqlite_master WHERE name='indexed_gitea_files'").fetchone()
|
||||
repo='daniele/rete'
|
||||
for path in ['ha/FAILOVER-VOLUMES.md','ha/RUNBOOK-failover.md']:
|
||||
r=gitea_indexer.index_file(repo, path, username='daniele', private=True, force=True)
|
||||
print('indexed', path, r.get('chunks'), r.get('skipped'), flush=True)
|
||||
files=gitea_indexer.list_indexed_files(limit=10, repo=repo)
|
||||
print('db_files', [f['path'] for f in files], flush=True)
|
||||
for q in ('failover tier-b','runbook'):
|
||||
hits=gitea_indexer.search_gitea_knowledge('daniele', q, limit=3, is_admin=True)
|
||||
print('search', q, hits[0]['path'] if hits else None, round(float(hits[0]['score']),3) if hits else None, flush=True)
|
||||
if not hits: sys.exit(1)
|
||||
claims={'sub':'daniele','scope':'knowledge:read gitea:read admin'}
|
||||
for tool,args in [
|
||||
('search_gitea_knowledge',{'query':'failover tier-b','limit':5}),
|
||||
('list_gitea_indexed_files',{'repo':repo,'limit':10}),
|
||||
('reindex_gitea_repo',{'repo':repo,'max_files':1}),
|
||||
('search_knowledge',{'query':'failover keepalived','limit':8}),
|
||||
]:
|
||||
out=tools.call_tool(tool,args,claims)
|
||||
p=json.loads(out['content'][0]['text'])
|
||||
if tool=='search_knowledge':
|
||||
src=sorted({r.get('source') for r in p['results']})
|
||||
print(tool, src, flush=True)
|
||||
assert 'gitea' in src
|
||||
elif tool=='list_gitea_indexed_files':
|
||||
print(tool, p['count'], flush=True)
|
||||
else:
|
||||
print(tool, 'ok', flush=True)
|
||||
print('OK P3 COMPLETE', flush=True)
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
os.environ.setdefault('MCP_DB','/data/loogle_mcp.db')
|
||||
os.environ.setdefault('MCP_VECTOR_FALLBACK','/data/vector_fallback.db')
|
||||
from app.db import init_db, get_conn
|
||||
from app.knowledge import gitea_indexer
|
||||
from app.mcp import tools
|
||||
init_db()
|
||||
print('table_ok', bool(get_conn().execute("SELECT 1 FROM sqlite_master WHERE name='indexed_gitea_files'").fetchone()), flush=True)
|
||||
files=gitea_indexer.list_indexed_files(limit=10, repo='daniele/rete')
|
||||
print('indexed', [f['path'] for f in files], flush=True)
|
||||
if not files:
|
||||
print('WARN: nessun file indicizzato — indicizzo stacks.conf (piccolo)', flush=True)
|
||||
r=gitea_indexer.index_file('daniele/rete','ha/stacks.conf', username='daniele', private=True, force=True)
|
||||
print('new_index', r, flush=True)
|
||||
files=gitea_indexer.list_indexed_files(limit=10, repo='daniele/rete')
|
||||
for q in ('failover','censimento','tier-b'):
|
||||
hits=gitea_indexer.search_gitea_knowledge('daniele', q, limit=3, is_admin=True)
|
||||
print('search', q, hits[0]['path'] if hits else 'NONE', round(float(hits[0]['score']),3) if hits else None, flush=True)
|
||||
if not hits and q=='failover':
|
||||
sys.exit(1)
|
||||
claims={'sub':'daniele','scope':'knowledge:read gitea:read admin'}
|
||||
sg=tools.call_tool('search_gitea_knowledge',{'query':'failover tier-b','limit':5},claims)
|
||||
assert json.loads(sg['content'][0]['text'])['results']
|
||||
print('tool search_gitea_knowledge ok', flush=True)
|
||||
li=tools.call_tool('list_gitea_indexed_files',{'repo':'daniele/rete','limit':10},claims)
|
||||
print('tool list', json.loads(li['content'][0]['text'])['count'], flush=True)
|
||||
ri=tools.call_tool('reindex_gitea_repo',{'repo':'daniele/rete','max_files':1},claims)
|
||||
print('tool reindex', json.loads(ri['content'][0]['text']).get('files_indexed'), flush=True)
|
||||
sk=tools.call_tool('search_knowledge',{'query':'failover keepalived','limit':8},claims)
|
||||
src=sorted({r.get('source') for r in json.loads(sk['content'][0]['text'])['results']})
|
||||
print('tool search_knowledge sources', src, flush=True)
|
||||
assert 'gitea' in src
|
||||
print('OK P3 SEARCH VERIFIED', flush=True)
|
||||
Reference in new issue
Block a user