';
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
index 9ec2c8c..c9a24fc 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
@@ -91,57 +91,93 @@ puntii Discord — vezi `../discord-bridge/README.md` pentru URL si autentificar
## Depozitul de documente
-Fisiere `.txt`/`.md` in `~/.maria-bridge/documents/`. Se pot administra:
+Fisiere `.txt`/`.md`/`.xml` in `~/.maria-bridge/documents/` (lista exacta:
+`store.DOC_EXTENSIONS`). Se pot administra:
1. **manual din dashboard-ul comun** (adaugare/stergere text, reindexare automata la salvare);
2. **prin sincronizare Google Drive** — vezi mai jos.
## Sincronizare cu Google Drive
-Sursa: dosarul `D:\GoogleDrive\romfast\document_store` de pe Windows (Google
-Drive Desktop). Containerul e headless, deci sincronizarea foloseste
-**rclone cu un cont de serviciu** — nu OAuth interactiv in browser.
+Sursa: dosarul `document_store` din Drive-ul contului `mmarius28@gmail.com`
+(pe Windows apare ca `D:\GoogleDrive\romfast\document_store`, prin Google Drive
+Desktop). ID-ul dosarului: `1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O`.
+
+Containerul e headless (fara browser pentru OAuth), deci autorizarea se face pe o
+masina cu browser si se muta aici ca token — **fara cont de serviciu si fara consola
+Google Cloud**. rclone e deja instalat pe LXC 171 (`rclone v1.60.1`).
Pasi (o singura data):
-1. **Instaleaza rclone** pe container: `sudo apt-get install -y rclone`.
- ✅ Deja instalat pe LXC 171 (2026-08-31), `rclone v1.60.1`. Pasii 2-7 de mai
- jos raman de facut — cer acces la consola Google Cloud a utilizatorului, nu
- se pot automatiza din container.
-2. **Creeaza un cont de serviciu Google** cu acces la Drive API (Google Cloud
- Console -> IAM -> Service Accounts -> Create -> descarca cheia JSON).
-3. **Partajeaza folderul** `document_store` din Google Drive cu adresa de email
- a contului de serviciu (click dreapta pe folder -> Share), exact cum ai
- partaja cu o persoana. Fara acest pas, contul de serviciu nu vede nimic.
-4. Pune cheia JSON pe container, ex. `~/.maria-bridge/gdrive-service-account.json`
- (0600).
-5. Configureaza remote-ul rclone (`rclone config`, fara sesiune interactiva de
- browser cu tip `service_account_file`):
+1. **Pe Windows**, ia `rclone.exe` de la (arhiva
+ portabila, nu cere instalare) si ruleaza in acel dosar:
```
- rclone config create gdrive drive \
- scope=drive.readonly \
- service_account_file=/home/claude/.maria-bridge/gdrive-service-account.json
+ rclone.exe authorize "drive" --drive-scope=drive.readonly
```
-6. Gaseste ID-ul folderului `document_store` (din URL-ul Drive) si testeaza:
+ Se deschide browserul; autentifica-te cu `mmarius28@gmail.com` si accepta.
+ In consola apare un token JSON intre `--->` si `<---`. Copiaza-l intreg.
+2. **Pe container**, creeaza remote-ul cu tokenul copiat:
```
- rclone lsf gdrive: --drive-root-folder-id=
+ rclone config create gdrive drive scope=drive.readonly token=''
```
- ID-ul curent al folderului `document_store` (cont `mmarius28@gmail.com`,
- gasit prin cautare Drive): `1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O`.
- Sau, mai simplu, foloseste calea prin nume daca folderul e in "My Drive" al
- contului care a facut share (rclone urmareste shared-with-me cu
- `--drive-shared-with-me` daca e nevoie).
-7. Pune tinta gasita in `~/.maria-bridge/env`:
+3. Testeaza ca vede dosarul:
```
- DRIVE_REMOTE=gdrive:romfast/document_store
+ rclone lsf gdrive: --drive-root-folder-id=1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O
```
-8. Testeaza manual: `systemctl --user start maria-sync.service` apoi
- `journalctl --user -u maria-sync -n 50`, sau butonul „sincronizeaza din Drive
- acum" din dashboard.
+4. Pune tinta in `~/.maria-bridge/env`:
+ ```
+ DRIVE_REMOTE=gdrive,root_folder_id=1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O:
+ ```
+ (sintaxa „connection string" a rclone — fixeaza dosarul fara sa depinda de
+ structura de nume din My Drive.)
+5. Testeaza: butonul „sincronizeaza din Drive acum" din dashboard, sau
+ `systemctl --user start maria-sync.service` apoi
+ `journalctl --user -u maria-sync -n 50`.
+
+Tokenul se reimprospateaza singur (refresh token) cat timp aplicatia ramane
+autorizata in contul Google. Daca expira, dashboard-ul arata sincronizarea ca
+esuata — reia pasii 1-2.
+
+### Ce se sincronizeaza
+
+`rclone sync` aduce doar `*.txt`, `*.md` si `*.xml` (vezi `store.DOC_EXTENSIONS`).
+Restul din dosar — chatflow-uri Flowise `.json`, scripturi `.ps1`, `.docx` — sunt
+ignorate deliberat: nu sunt cunostinte de suport.
+
+**`sync` sterge local ce nu mai exista in Drive**, deci depozitul e o oglinda a
+dosarului din Drive, nu o colectie care creste. Documentele adaugate manual din
+dashboard dispar la prima sincronizare daca nu exista si in Drive.
+
+### Acelasi document in doua formate
+
+Cand exista `X.xml` si `X.md`, **in index intra doar `.xml`** (ordinea de
+preferinta: `.xml` > `.md` > `.txt`, in `rag/store.py`). Sursele `.xml` sunt
+structurate pe probleme si de regula mai noi decat exporturile `.md` — la
+`d406_saft_knowledge`, `.xml` era cu trei luni mai nou si cu 50% mai mare.
+Fisierul umbrit ramane pe disc si apare in dashboard marcat „umbrit de …", ca sa
+se vada de ce nu e indexat; daca ar fi indexate ambele, acelasi raspuns ar aparea
+de doua ori in rezultatele RAG.
+
+### Cum se taie XML-ul in chunk-uri
+
+Un chunk per element de nivel 1 — adica **o problema = un chunk**, cu mesajul de
+eroare si rezolvarea impreuna. Taierea pe linii goale (cea folosita la `.md`) le-ar
+separa, iar cautarea ar gasi eroarea si ar returna un chunk fara raspuns.
+Etichetele raman ca prefixe lizibile (`mesaj eroare: …`, `rezolvare: …`), fara
+paranteze unghiulare. Un XML care nu se poate parsa nu opreste indexarea: cade pe
+taierea obisnuita, cu o linie in log. Vezi `rag/indexer.py` si
+`tests/test_store_si_chunking.py`.
Dupa configurare, `maria-sync.timer` trage la fiecare 10 minute; reindexarea
ruleaza DOAR daca s-a schimbat efectiv ceva in depozit (amprenta pe nume +
mtime + marime, vezi `rag/sync.py`), ca sa nu reface embeddings degeaba.
+## Teste
+
+```bash
+cd /workspace/romfastsql/proxmox/lxc171-claude-agent/maria-whatsapp-bridge
+python3 -m pytest # preferinta de format + taierea XML, fara retea si fara Ollama
+```
+
## Context conversational
`rag/consumer.py` nu retine memorie intre mesaje — fiecare intrebare e o
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
index 4a93b7b..ca305f7 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
@@ -24,12 +24,13 @@ MAX_TOKENS=250
POLL_INTERVAL_S=2
# --- Depozit de documente + sincronizare Google Drive -----------------------
-# Tinta rclone pentru dosarul de documente (partajat de pe Windows,
-# D:\GoogleDrive\romfast\document_store), format `:`.
+# Tinta rclone pentru dosarul de documente din Drive (pe Windows apare ca
+# D:\GoogleDrive\romfast\document_store). Se aduc doar *.txt, *.md, *.xml.
# Gol = sincronizare dezactivata; documentele se administreaza doar manual din
-# dashboard. Vezi README.md, "Sincronizare cu Google Drive", pentru configurarea
-# remote-ului `rclone` (cont de serviciu, fara OAuth interactiv pe un container
-# headless).
+# dashboard. Vezi README.md, "Sincronizare cu Google Drive": autorizarea se face
+# cu `rclone authorize` pe o masina cu browser, tokenul se muta aici — fara cont
+# de serviciu. Exemplu, cu dosarul fixat pe ID:
+# DRIVE_REMOTE=gdrive,root_folder_id=1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O:
DRIVE_REMOTE=
# --- control -----------------------------------------------------------------
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/pytest.ini b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/pytest.ini
new file mode 100644
index 0000000..eeb9d41
--- /dev/null
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = tests
+addopts = -q
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
index 017bec8..bf69cef 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
@@ -35,9 +35,9 @@ DEFAULTS: dict[str, str] = {
"POLL_INTERVAL_S": "2",
"TEST_MODE_SELF_CHAT_ONLY": "true",
# Tinta rclone pentru sincronizarea depozitului de documente, ex:
- # "gdrive:romfast/document_store" (dosarul D:\GoogleDrive\romfast\document_store
- # de pe Windows, vazut prin Google Drive API). Gol = sincronizare dezactivata,
- # doar upload manual din dashboard.
+ # "gdrive,root_folder_id=1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O:" (dosarul
+ # document_store din Drive, vazut pe Windows ca D:\GoogleDrive\romfast\document_store).
+ # Gol = sincronizare dezactivata, doar upload manual din dashboard.
"DRIVE_REMOTE": "",
}
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/indexer.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/indexer.py
index 3370e98..392ca5b 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/indexer.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/indexer.py
@@ -1,12 +1,21 @@
#!/usr/bin/env python3
"""(Re)construieste rag_index.json din toate documentele din depozit (store.py),
-cu embeddings Ollama. Rulat manual sau declansat din dashboard (`/api/reindex`)."""
+cu embeddings Ollama. Rulat manual sau declansat din dashboard (`/api/reindex`).
+
+Doua strategii de taiere in chunk-uri:
+- **.xml** — un chunk per element de nivel 1 (o problema = un chunk). Sursele sunt
+ structurate `……`,
+ iar taierea pe linii goale ar rupe mesajul de eroare de rezolvarea lui: cautarea ar
+ gasi eroarea si ar returna un chunk fara raspuns.
+- **restul** — paragrafe unite pana la ~200 de caractere, ca inainte.
+"""
from __future__ import annotations
import json
import re
import sys
+import xml.etree.ElementTree as ET
import requests
@@ -32,6 +41,85 @@ def chunk_text(text: str) -> list[str]:
return chunks
+# Peste atat, un singur element XML se taie mai departe ca text obisnuit.
+MAX_XML_CHUNK = 4000
+
+
+def _render_element(el, depth: int = 0) -> str:
+ """Elementul, ca text cu etichete pe post de etichete: `mesaj_eroare: ...`.
+
+ Numele etichetelor poarta intelesul (care bucata e eroarea, care e rezolvarea),
+ deci raman in text — dar fara paranteze unghiulare, care doar dilueaza embedding-ul.
+ """
+ lines = []
+ own = (el.text or "").strip()
+ label = el.tag.replace("_", " ")
+ children = list(el)
+ if own:
+ lines.append(f"{label}: {own}" if children or depth else own)
+ elif children and depth == 0:
+ lines.append(f"[{label}]")
+ for child in children:
+ rendered = _render_element(child, depth + 1)
+ if rendered:
+ lines.append(rendered)
+ tail = (child.tail or "").strip()
+ if tail:
+ lines.append(tail)
+ return "\n".join(lines)
+
+
+def _split_long(text: str, limit: int = MAX_XML_CHUNK) -> list[str]:
+ """Taie o bucata prea lunga pe granite de cuvant.
+
+ `chunk_text` imparte pe linii goale, deci un element XML scris ca un singur
+ paragraf lung nu s-ar imparti deloc — de aici taierea asta, pe lungime.
+ """
+ out: list[str] = []
+ for part in chunk_text(text) or [text]:
+ while len(part) > limit:
+ cut = part.rfind(" ", 0, limit)
+ if cut <= 0:
+ cut = limit
+ out.append(part[:cut].strip())
+ part = part[cut:].lstrip()
+ if part.strip():
+ out.append(part.strip())
+ return out
+
+
+def chunk_xml(text: str) -> list[str] | None:
+ """Un chunk per element de nivel 1. `None` daca XML-ul nu se poate parsa."""
+ try:
+ root = ET.fromstring(text)
+ except ET.ParseError:
+ return None
+
+ chunks: list[str] = []
+ for el in root:
+ rendered = _render_element(el).strip()
+ if not rendered:
+ continue
+ if len(rendered) > MAX_XML_CHUNK:
+ chunks.extend(_split_long(rendered))
+ else:
+ chunks.append(rendered)
+ if not chunks:
+ # radacina fara copii (sau doar text): trateaz-o ca document obisnuit
+ whole = _render_element(root).strip()
+ return chunk_text(whole) if whole else []
+ return chunks
+
+
+def chunk_document(name: str, text: str) -> list[str]:
+ if name.endswith(".xml"):
+ chunks = chunk_xml(text)
+ if chunks is not None:
+ return chunks
+ print(f"[indexer] {name}: XML invalid, il tai ca text simplu", file=sys.stderr)
+ return chunk_text(text)
+
+
def embed(text: str) -> list[float]:
resp = requests.post(
f"{config.get('OLLAMA_URL')}/api/embeddings",
@@ -44,10 +132,10 @@ def embed(text: str) -> list[float]:
def build() -> dict:
entries = []
- docs = store.list_documents()
+ docs = store.documents_for_index()
for doc in docs:
text = store.read_document(doc["name"])
- for i, chunk in enumerate(chunk_text(text)):
+ for i, chunk in enumerate(chunk_document(doc["name"], text)):
vec = embed(chunk)
entries.append({"source": doc["name"], "chunk": i, "text": chunk, "embedding": vec})
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py
index e46f5b7..5cfb2cb 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py
@@ -1,38 +1,75 @@
"""Depozitul de documente pentru indexarea RAG a lui Maria.
-Fisiere text (.txt/.md) sub STATE_DIR/documents/, un singur nivel (fara
+Fisiere text (.txt/.md/.xml) sub STATE_DIR/documents/, un singur nivel (fara
subdirectoare), ca numele afisat in dashboard sa fie neambiguu si sa poata servi
direct ca parametru de request fara riscuri de traversare de cale.
+
+Cand acelasi document exista in mai multe formate (`d406_saft_knowledge.xml` si
+`d406_saft_knowledge.md`), la indexare intra UNUL SINGUR, cel cu formatul cel mai
+bogat: .xml > .md > .txt. Sursele .xml sunt structurate pe probleme
+(``, ``) si de regula mai noi decat exporturile .md.
+Celalalt ramane pe disc si se vede in dashboard, marcat ca umbrit — altfel acelasi
+raspuns ar aparea de doua ori in rezultatele RAG.
"""
from __future__ import annotations
+import pathlib
import re
import config
_SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,200}$")
+# Extensiile acceptate in depozit. Sursa unica de adevar pentru sync.py si indexer.py;
+# dashboard-ul (discord-bridge/dashboard/api.py) tine o copie, fiindca nu importa
+# module din acest proiect (coliziune de nume pe `config`) — tine-le sincronizate.
+DOC_EXTENSIONS = (".txt", ".md", ".xml")
+
+# Mai mic = preferat cand acelasi nume de baza exista in mai multe formate.
+_FORMAT_RANK = {".xml": 0, ".md": 1, ".txt": 2}
+
def validate_name(name: str) -> str:
if not name or not _SAFE_NAME.match(name) or ".." in name or "/" in name:
raise ValueError(f"nume de document invalid: {name!r}")
- if not name.endswith((".txt", ".md")):
- raise ValueError("doar fisiere .txt sau .md")
+ if not name.endswith(DOC_EXTENSIONS):
+ raise ValueError("doar fisiere " + ", ".join(DOC_EXTENSIONS))
return name
def list_documents() -> list[dict]:
+ """Tot ce e in depozit. `shadowed_by` spune de ce un fisier nu ajunge in index."""
config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
+ files = [
+ f for f in sorted(config.DOCS_DIR.glob("*"))
+ if f.is_file() and f.suffix in DOC_EXTENSIONS
+ ]
+ # castigatorul per nume de baza: formatul cel mai bogat
+ winners: dict[str, str] = {}
+ for f in files:
+ best = winners.get(f.stem)
+ if best is None or _FORMAT_RANK[f.suffix] < _FORMAT_RANK[pathlib.Path(best).suffix]:
+ winners[f.stem] = f.name
+
out = []
- for f in sorted(config.DOCS_DIR.glob("*")):
- if not f.is_file() or f.suffix not in (".txt", ".md"):
- continue
+ for f in files:
st = f.stat()
- out.append({"name": f.name, "size": st.st_size, "mtime": st.st_mtime})
+ winner = winners[f.stem]
+ out.append({
+ "name": f.name,
+ "size": st.st_size,
+ "mtime": st.st_mtime,
+ "shadowed_by": None if winner == f.name else winner,
+ })
return out
+def documents_for_index() -> list[dict]:
+ """Doar documentele care ajung efectiv in index (fara cele umbrite)."""
+ return [d for d in list_documents() if not d["shadowed_by"]]
+
+
def read_document(name: str) -> str:
name = validate_name(name)
return (config.DOCS_DIR / name).read_text(encoding="utf-8")
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py
index 615c533..22c0dd4 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py
@@ -22,6 +22,7 @@ import time
import config
import indexer
+import store
STATE_FILE_NAME = ".sync_state.json"
@@ -32,7 +33,7 @@ def _fingerprint() -> str:
config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
h = hashlib.sha256()
for f in sorted(config.DOCS_DIR.glob("*")):
- if f.is_file() and f.suffix in (".txt", ".md"):
+ if f.is_file() and f.suffix in store.DOC_EXTENSIONS:
st = f.stat()
h.update(f"{f.name}:{st.st_mtime_ns}:{st.st_size}\n".encode())
return h.hexdigest()
@@ -65,8 +66,8 @@ def pull_from_drive() -> dict:
config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
try:
r = subprocess.run(
- ["rclone", "sync", remote, str(config.DOCS_DIR),
- "--include", "*.txt", "--include", "*.md"],
+ ["rclone", "sync", remote, str(config.DOCS_DIR)]
+ + [arg for ext in store.DOC_EXTENSIONS for arg in ("--include", f"*{ext}")],
capture_output=True, text=True, timeout=300,
)
except FileNotFoundError:
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/conftest.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/conftest.py
new file mode 100644
index 0000000..8b23370
--- /dev/null
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/conftest.py
@@ -0,0 +1,32 @@
+"""Fixturi: depozit intr-un tmp, zero retea, zero Ollama."""
+
+from __future__ import annotations
+
+import pathlib
+import sys
+
+import pytest
+
+RAG = pathlib.Path(__file__).resolve().parent.parent / "rag"
+if str(RAG) not in sys.path:
+ sys.path.insert(0, str(RAG))
+
+import config # noqa: E402
+
+
+@pytest.fixture(autouse=True)
+def docs_dir(tmp_path, monkeypatch):
+ monkeypatch.setenv("MARIA_BRIDGE_DIR", str(tmp_path))
+ config.reload(tmp_path)
+ d = config.DOCS_DIR
+ d.mkdir(parents=True, exist_ok=True)
+ yield d
+ config.reload()
+
+
+@pytest.fixture
+def write(docs_dir):
+ def _write(name: str, text: str):
+ (docs_dir / name).write_text(text, encoding="utf-8")
+ return docs_dir / name
+ return _write
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py
new file mode 100644
index 0000000..4158c37
--- /dev/null
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py
@@ -0,0 +1,114 @@
+"""Ce intra in index (preferinta de format) si cum se taie XML-ul in chunk-uri."""
+
+from __future__ import annotations
+
+import pytest
+
+import indexer
+import store
+
+XML = """
+
+
+
+ stocuri negative NIR
+ Documentul nu poate fi sters: stocuri negative pentru BANDA HARTIE.
+ Optiuni firma > VERIFICARESTOCNEGATIV = 0.
+
+
+ Alta eroare.
+ Alta rezolvare.
+
+"""
+
+
+# ------------------------------------------------------- preferinta de format
+def test_xml_umbreste_md_cu_acelasi_nume(write):
+ write("d406.xml", "x")
+ write("d406.md", "versiune veche")
+ umbrite = {d["name"]: d["shadowed_by"] for d in store.list_documents()}
+ assert umbrite["d406.md"] == "d406.xml"
+ assert umbrite["d406.xml"] is None
+ assert [d["name"] for d in store.documents_for_index()] == ["d406.xml"]
+
+
+def test_md_umbreste_txt(write):
+ write("x.md", "md")
+ write("x.txt", "txt")
+ assert [d["name"] for d in store.documents_for_index()] == ["x.md"]
+
+
+def test_fisierele_fara_pereche_raman_toate(write):
+ write("bilant.md", "a")
+ write("roagest.xml", "b")
+ write("note.txt", "c")
+ assert len(store.documents_for_index()) == 3
+
+
+def test_depozitul_e_vizibil_intreg_chiar_daca_nu_tot_se_indexeaza(write):
+ write("d406.xml", "x")
+ write("d406.md", "vechi")
+ assert len(store.list_documents()) == 2 # dashboard le arata pe ambele
+ assert len(store.documents_for_index()) == 1 # indexul ia doar una
+
+
+def test_extensiile_necunoscute_sunt_ignorate(write):
+ write("chatflow.json", "{}")
+ write("script.ps1", "echo")
+ assert store.list_documents() == []
+
+
+@pytest.mark.parametrize("name", ["a.xml", "a.md", "a.txt"])
+def test_numele_acceptate(name):
+ assert store.validate_name(name) == name
+
+
+@pytest.mark.parametrize("name", ["a.json", "a.pdf", "../a.md", "a b.md"])
+def test_numele_respinse(name):
+ with pytest.raises(ValueError):
+ store.validate_name(name)
+
+
+# ----------------------------------------------------------- chunking XML
+def test_o_problema_ramane_un_singur_chunk():
+ """Miezul: mesajul de eroare si rezolvarea NU trebuie separate."""
+ chunks = indexer.chunk_document("x.xml", XML)
+ assert len(chunks) == 2
+ assert "BANDA HARTIE" in chunks[0] and "VERIFICARESTOCNEGATIV" in chunks[0]
+ assert "Alta eroare" in chunks[1] and "Alta rezolvare" in chunks[1]
+
+
+def test_etichetele_raman_ca_etichete_fara_paranteze():
+ chunk = indexer.chunk_document("x.xml", XML)[0]
+ assert "mesaj eroare:" in chunk and "rezolvare:" in chunk
+ assert "" not in chunk
+
+
+def test_entitatile_sunt_decodate():
+ assert ">" not in indexer.chunk_document("x.xml", XML)[0]
+
+
+def test_comentariile_nu_devin_chunk():
+ assert not any("comentariu" in c for c in indexer.chunk_document("x.xml", XML))
+
+
+def test_xml_invalid_cade_pe_taierea_obisnuita():
+ chunks = indexer.chunk_document("stricat.xml", "")
+ assert chunks == [""]
+
+
+def test_xml_fara_copii_e_tratat_ca_text():
+ assert indexer.chunk_document("x.xml", "doar text") == ["doar text"]
+
+
+def test_element_urias_e_taiat_mai_departe():
+ mare = "" + ("propozitie lunga. " * 500) + ""
+ chunks = indexer.chunk_document("x.xml", mare)
+ assert len(chunks) > 1
+ assert all(len(c) < indexer.MAX_XML_CHUNK * 2 for c in chunks)
+
+
+def test_md_foloseste_taierea_pe_paragrafe():
+ text = "primul paragraf\n\n" + "al doilea paragraf " * 20
+ assert len(indexer.chunk_document("x.md", text)) >= 1
+ assert indexer.chunk_document("x.md", text) == indexer.chunk_text(text)