fix(maria): lacat intre reindexari, scriere atomica a indexului, XML invalid raportat
Descoperit la prima sincronizare reala din Drive: `maria-sync.timer` a pornit peste rularea manuala si doua procese faceau embeddings in paralel pe acelasi Ollama, ambele urmand sa scrie acelasi rag_index.json. Embedding-ul a incetinit de la ~7s la ~20s din concurenta, iar ultimul care termina ar fi suprascris munca celuilalt. - config.exclusive(): lacat `flock` intre procese, luat la intrarea in sync.py si indexer.py. Nu asteapta — a doua rulare iese curat cu "o reindexare e deja in curs", fiindca ar reface exact acelasi lucru. Verificat pe procese reale. - indexer scrie indexul atomic (tmp + os.replace): consumer-ul reciteste fisierul la 30s si putea prinde un JSON pe jumatate scris. - build() intoarce `warnings` pentru XML-urile care nu se pot parsa, iar rularea din linia de comanda le scrie in stderr. Pana acum, un XML invalid se indexa tacut ca text simplu, cu o singura linie pierduta in log. Context de performanta, masurat pe LXC 171 fara alta incarcare: un embedding `nomic-embed-text` ia ~7,3s, deci o reindexare completa a celor 173 de chunk-uri dureaza ~21 de minute — mai mult decat intervalul timer-ului. Nu e o problema practica (amprenta reindexeaza doar la schimbare, iar lacatul opreste suprapunerea), dar explica de ce prima rulare pare blocata. docs/rclone-google-drive-headless.md: procedura de conectare a unui container headless la Drive prin `rclone authorize`, cu transcriptul rularii reale de pe Windows, capcanele (sync e distructiv pe destinatie, connection string in loc de cale pe nume, unde stau secretele) si de ce nu contul de serviciu. Indexata in CLAUDE.md. 6 teste noi (lacat, eliberare la exceptie, scriere atomica, avertismente). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
@@ -61,6 +61,7 @@ input/ # Oracle DMP files for import
|
|||||||
- **Monitorizare UPS pe serverul Oracle de producție 10.0.20.36 (înlocuiește ViewPower)**: `docs/ups-server36-monitorizare.md`
|
- **Monitorizare UPS pe serverul Oracle de producție 10.0.20.36 (înlocuiește ViewPower)**: `docs/ups-server36-monitorizare.md`
|
||||||
- **Acces SSH la clienți cu chei publice (angajați noi)**: `docs/acces-ssh-chei-angajati.md`
|
- **Acces SSH la clienți cu chei publice (angajați noi)**: `docs/acces-ssh-chei-angajati.md`
|
||||||
- **Acces administrativ la server client prin Tailscale + SSH (fără forward pe router)**: `docs/acces-client-tailscale-ssh.md`
|
- **Acces administrativ la server client prin Tailscale + SSH (fără forward pe router)**: `docs/acces-client-tailscale-ssh.md`
|
||||||
|
- **Conectarea unui container headless la Google Drive prin rclone (fără cont de serviciu)**: `docs/rclone-google-drive-headless.md`
|
||||||
- **Cazuri clienți (depanare DB)**: `proxmox/lxc108-oracle/clienti/README.md`
|
- **Cazuri clienți (depanare DB)**: `proxmox/lxc108-oracle/clienti/README.md`
|
||||||
- **ROMPETROL ENERGY — recreare PDB Oracle XE 21c după ORA-12954**: `proxmox/lxc108-oracle/clienti/oracle-xe-21c/README.md`
|
- **ROMPETROL ENERGY — recreare PDB Oracle XE 21c după ORA-12954**: `proxmox/lxc108-oracle/clienti/oracle-xe-21c/README.md`
|
||||||
|
|
||||||
|
|||||||
117
docs/rclone-google-drive-headless.md
Normal file
117
docs/rclone-google-drive-headless.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# rclone + Google Drive pe un container headless (fără cont de serviciu)
|
||||||
|
|
||||||
|
Procedura de conectare a unui container Linux fără browser la Google Drive.
|
||||||
|
Folosită prima dată pentru depozitul de documente al Mariei (LXC 171,
|
||||||
|
`maria-whatsapp-bridge`), dar e generală — se aplică oricărui container din cluster.
|
||||||
|
|
||||||
|
**Ideea:** autorizarea OAuth are nevoie de un browser, containerul nu are. Deci
|
||||||
|
autorizezi pe o mașină care are browser (stația Windows), iar rclone îți dă un token
|
||||||
|
pe care îl muți pe container. Nu e nevoie de cont de serviciu și nu se atinge deloc
|
||||||
|
consola Google Cloud.
|
||||||
|
|
||||||
|
## De ce nu contul de serviciu
|
||||||
|
|
||||||
|
Prima variantă documentată era un service account în Google Cloud Console, cu dosarul
|
||||||
|
din Drive partajat către adresa lui. Funcționează, dar cere: proiect în Google Cloud,
|
||||||
|
activare Drive API, creare cont, descărcare JSON, partajare manuală a dosarului. Șapte
|
||||||
|
pași în două interfețe. `rclone authorize` face același lucru în doi pași, dintr-o
|
||||||
|
fereastră de browser deja autentificată.
|
||||||
|
|
||||||
|
Contul de serviciu rămâne varianta mai bună **doar** dacă ai nevoie de acces care nu
|
||||||
|
depinde de niciun cont de om (ex. un serviciu care trebuie să meargă și după ce omul
|
||||||
|
respectiv pleacă din firmă).
|
||||||
|
|
||||||
|
## Pasul 1 — autorizezi pe Windows
|
||||||
|
|
||||||
|
Descarcă arhiva portabilă de la <https://rclone.org/downloads/> (`rclone-vX-windows-amd64.zip`),
|
||||||
|
dezarhiveaz-o oriunde — nu cere instalare. În acel dosar:
|
||||||
|
|
||||||
|
```
|
||||||
|
rclone.exe authorize "drive" --drive-scope=drive.readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
`drive.readonly` fiindcă sincronizarea doar citește. Dacă ai nevoie și de scriere,
|
||||||
|
folosește `drive`, dar atunci un `rclone sync` greșit poate șterge în Drive.
|
||||||
|
|
||||||
|
Se deschide browserul. Dacă nu se deschide, rclone scrie linkul în consolă. Cum arată
|
||||||
|
o rulare reușită:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\Users\mmari\Downloads\rclone-v1.75.0-windows-amd64>rclone.exe authorize "drive" --drive-scope=drive.readonly
|
||||||
|
NOTICE: Config file "C:\\Users\\mmari\\AppData\\Roaming\\rclone\\rclone.conf" not found - using defaults
|
||||||
|
NOTICE: Make sure your Redirect URL is set to "http://127.0.0.1:53682/" in your custom config.
|
||||||
|
NOTICE: If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth?state=...
|
||||||
|
NOTICE: Log in and authorize rclone for access
|
||||||
|
NOTICE: Waiting for code...
|
||||||
|
NOTICE: Got code
|
||||||
|
Paste the following into your remote machine --->
|
||||||
|
{"access_token":"ya29...","token_type":"Bearer","refresh_token":"1//03...","expiry":"...","expires_in":3599}
|
||||||
|
<---End paste
|
||||||
|
```
|
||||||
|
|
||||||
|
Cele două mesaje despre „Config file not found" și „Redirect URL" **nu sunt erori** —
|
||||||
|
`authorize` nu scrie niciun config pe Windows, doar obține tokenul. Dosarul rclone de
|
||||||
|
pe Windows poate fi șters după.
|
||||||
|
|
||||||
|
Copiază tot JSON-ul dintre `--->` și `<---`, inclusiv acoladele.
|
||||||
|
|
||||||
|
## Pasul 2 — creezi remote-ul pe container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone config create gdrive drive \
|
||||||
|
scope=drive.readonly \
|
||||||
|
token='<TOKEN_JSON>' \
|
||||||
|
--non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
Ghilimelele simple sunt obligatorii — JSON-ul conține ghilimele duble.
|
||||||
|
|
||||||
|
Pentru Maria există un script care face pasul ăsta plus verificarea, scrierea în env și
|
||||||
|
prima sincronizare: `maria-whatsapp-bridge/ops/setup-drive.sh '<TOKEN_JSON>'`.
|
||||||
|
|
||||||
|
## Pasul 3 — fixezi dosarul după ID, nu după nume
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone lsf 'gdrive,root_folder_id=<FOLDER_ID>:'
|
||||||
|
```
|
||||||
|
|
||||||
|
`<FOLDER_ID>` e din URL-ul dosarului în Drive
|
||||||
|
(`https://drive.google.com/drive/folders/<FOLDER_ID>`).
|
||||||
|
|
||||||
|
Sintaxa `remote,parametru=valoare:` e „connection string"-ul rclone. **Folosește-o în loc
|
||||||
|
de căi pe nume** (`gdrive:romfast/document_store`): calea pe nume se rupe dacă dosarul e
|
||||||
|
mutat sau redenumit în Drive, iar `rclone sync` nu dă eroare — pur și simplu nu găsește
|
||||||
|
nimic și, dacă e sincronizare bidirecțională, poate goli destinația.
|
||||||
|
|
||||||
|
## Unde stau secretele
|
||||||
|
|
||||||
|
Tokenul ajunge în `~/.config/rclone/rclone.conf` pe container. Fișierul conține
|
||||||
|
`refresh_token`, adică acces continuu la Drive-ul contului — **nu ajunge niciodată în
|
||||||
|
git** și nu se copiază între containere.
|
||||||
|
|
||||||
|
`access_token` expiră într-o oră; `refresh_token` îl reînnoiește singur, la nesfârșit,
|
||||||
|
cât timp aplicația rămâne autorizată în contul Google. Se poate revoca oricând din
|
||||||
|
<https://myaccount.google.com/permissions>.
|
||||||
|
|
||||||
|
Dacă tokenul e revocat sau expiră, sincronizarea începe să eșueze — se reiau pașii 1-2.
|
||||||
|
|
||||||
|
## Capcane
|
||||||
|
|
||||||
|
- **`rclone sync` e distructiv pe destinație.** Șterge local ce nu mai există în sursă.
|
||||||
|
Depozitul devine o oglindă a dosarului din Drive, nu o colecție care crește: un fișier
|
||||||
|
pus manual acolo dispare la prima sincronizare. Dacă vrei doar adăugare, e `rclone copy`.
|
||||||
|
- **Filtrează ce aduci.** `--include '*.md' --include '*.xml'` etc. Un dosar de Drive
|
||||||
|
strânge în timp tot felul de lucruri (`.json`, `.ps1`, `.docx`) care n-au ce căuta în
|
||||||
|
depozit.
|
||||||
|
- **`rclone authorize` nu merge pe container**, nici prin Tailscale: pornește un server
|
||||||
|
local pe `127.0.0.1:53682` la care redirectează Google, deci browserul trebuie să fie
|
||||||
|
pe aceeași mașină.
|
||||||
|
- **Versiunea de pe container poate fi mai veche** decât cea de pe Windows (aici: 1.60.1
|
||||||
|
vs 1.75.0). Nu contează pentru token — formatul e stabil de mult.
|
||||||
|
|
||||||
|
## Vezi și
|
||||||
|
|
||||||
|
- `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md`, secțiunea
|
||||||
|
„Sincronizare cu Google Drive" — configurarea concretă pentru Maria.
|
||||||
|
- `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/setup-drive.sh` — scriptul
|
||||||
|
care face pașii 2-3 plus prima sincronizare.
|
||||||
@@ -7,6 +7,8 @@ model de citit pentru cine intretine ambele punti pe acest container.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ _DEFAULT_DIR = pathlib.Path.home() / ".maria-bridge"
|
|||||||
STATE_DIR: pathlib.Path = pathlib.Path(os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR)
|
STATE_DIR: pathlib.Path = pathlib.Path(os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR)
|
||||||
DOCS_DIR: pathlib.Path = STATE_DIR / "documents"
|
DOCS_DIR: pathlib.Path = STATE_DIR / "documents"
|
||||||
INDEX_FILE: pathlib.Path = STATE_DIR / "rag_index.json"
|
INDEX_FILE: pathlib.Path = STATE_DIR / "rag_index.json"
|
||||||
|
LOCK_FILE: pathlib.Path = STATE_DIR / ".rag.lock"
|
||||||
LOG_DIR: pathlib.Path = STATE_DIR / "logs"
|
LOG_DIR: pathlib.Path = STATE_DIR / "logs"
|
||||||
ENV_FILE: pathlib.Path = STATE_DIR / "env"
|
ENV_FILE: pathlib.Path = STATE_DIR / "env"
|
||||||
AUTH_DIR: pathlib.Path = STATE_DIR / "whatsapp-auth"
|
AUTH_DIR: pathlib.Path = STATE_DIR / "whatsapp-auth"
|
||||||
@@ -70,12 +73,13 @@ def parse_env(text: str) -> dict[str, str]:
|
|||||||
|
|
||||||
def reload(base_dir: str | os.PathLike | None = None) -> dict[str, str]:
|
def reload(base_dir: str | os.PathLike | None = None) -> dict[str, str]:
|
||||||
"""Recalculeaza caile si reciteste env-ul. Returneaza dictionarul incarcat."""
|
"""Recalculeaza caile si reciteste env-ul. Returneaza dictionarul incarcat."""
|
||||||
global STATE_DIR, DOCS_DIR, INDEX_FILE, LOG_DIR, ENV_FILE, AUTH_DIR, _env
|
global STATE_DIR, DOCS_DIR, INDEX_FILE, LOCK_FILE, LOG_DIR, ENV_FILE, AUTH_DIR, _env
|
||||||
if base_dir is None:
|
if base_dir is None:
|
||||||
base_dir = os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR
|
base_dir = os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR
|
||||||
STATE_DIR = pathlib.Path(base_dir)
|
STATE_DIR = pathlib.Path(base_dir)
|
||||||
DOCS_DIR = STATE_DIR / "documents"
|
DOCS_DIR = STATE_DIR / "documents"
|
||||||
INDEX_FILE = STATE_DIR / "rag_index.json"
|
INDEX_FILE = STATE_DIR / "rag_index.json"
|
||||||
|
LOCK_FILE = STATE_DIR / ".rag.lock"
|
||||||
LOG_DIR = STATE_DIR / "logs"
|
LOG_DIR = STATE_DIR / "logs"
|
||||||
ENV_FILE = STATE_DIR / "env"
|
ENV_FILE = STATE_DIR / "env"
|
||||||
AUTH_DIR = STATE_DIR / "whatsapp-auth"
|
AUTH_DIR = STATE_DIR / "whatsapp-auth"
|
||||||
@@ -102,3 +106,30 @@ def get_int(key: str, default: int) -> int:
|
|||||||
|
|
||||||
|
|
||||||
reload()
|
reload()
|
||||||
|
|
||||||
|
|
||||||
|
class Busy(RuntimeError):
|
||||||
|
"""Alta reindexare e deja in curs."""
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def exclusive():
|
||||||
|
"""Lacat intre PROCESE pentru reindexare (timer vs dashboard vs rulare manuala).
|
||||||
|
|
||||||
|
Reindexarea dureaza minute (embeddings pe CPU). Fara lacat, `maria-sync.timer`
|
||||||
|
poate porni peste o sincronizare manuala si ambele scriu acelasi rag_index.json.
|
||||||
|
Nu asteptam: a doua rulare se anuleaza curat, fiindca oricum ar reface acelasi
|
||||||
|
lucru imediat dupa.
|
||||||
|
"""
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
fh = open(LOCK_FILE, "w", encoding="utf-8")
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except OSError:
|
||||||
|
raise Busy("o reindexare e deja in curs")
|
||||||
|
fh.write(str(os.getpid()))
|
||||||
|
fh.flush()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
fh.close()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Doua strategii de taiere in chunk-uri:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -132,19 +133,36 @@ def embed(text: str) -> list[float]:
|
|||||||
|
|
||||||
def build() -> dict:
|
def build() -> dict:
|
||||||
entries = []
|
entries = []
|
||||||
|
warnings: list[str] = []
|
||||||
docs = store.documents_for_index()
|
docs = store.documents_for_index()
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
text = store.read_document(doc["name"])
|
text = store.read_document(doc["name"])
|
||||||
|
if doc["name"].endswith(".xml") and chunk_xml(text) is None:
|
||||||
|
warnings.append(f"{doc['name']}: XML invalid, indexat ca text simplu")
|
||||||
for i, chunk in enumerate(chunk_document(doc["name"], text)):
|
for i, chunk in enumerate(chunk_document(doc["name"], text)):
|
||||||
vec = embed(chunk)
|
vec = embed(chunk)
|
||||||
entries.append({"source": doc["name"], "chunk": i, "text": chunk, "embedding": vec})
|
entries.append({"source": doc["name"], "chunk": i, "text": chunk, "embedding": vec})
|
||||||
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
config.INDEX_FILE.write_text(json.dumps(entries, ensure_ascii=False), encoding="utf-8")
|
# Scriere atomica: consumer-ul reciteste fisierul la 30s si ar putea prinde
|
||||||
return {"documents": len(docs), "chunks": len(entries)}
|
# un JSON pe jumatate scris daca am scrie direct peste el.
|
||||||
|
tmp = config.INDEX_FILE.with_suffix(".json.tmp")
|
||||||
|
tmp.write_text(json.dumps(entries, ensure_ascii=False), encoding="utf-8")
|
||||||
|
os.replace(tmp, config.INDEX_FILE)
|
||||||
|
out = {"documents": len(docs), "chunks": len(entries)}
|
||||||
|
if warnings:
|
||||||
|
out["warnings"] = warnings
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
result = build()
|
try:
|
||||||
|
with config.exclusive():
|
||||||
|
result = build()
|
||||||
|
except config.Busy as exc:
|
||||||
|
print(f"[indexer] {exc}, ies fara sa fac nimic", file=sys.stderr)
|
||||||
|
raise SystemExit(0)
|
||||||
|
for w in result.get("warnings", []):
|
||||||
|
print(f"[indexer] ATENTIE {w}", file=sys.stderr)
|
||||||
print(
|
print(
|
||||||
f"[indexer] {result['documents']} documente, {result['chunks']} chunk-uri -> {config.INDEX_FILE}",
|
f"[indexer] {result['documents']} documente, {result['chunks']} chunk-uri -> {config.INDEX_FILE}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
|
|||||||
@@ -89,5 +89,9 @@ def sync_and_reindex(force: bool = False) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
out = sync_and_reindex(force="--force" in sys.argv[1:])
|
try:
|
||||||
|
with config.exclusive():
|
||||||
|
out = sync_and_reindex(force="--force" in sys.argv[1:])
|
||||||
|
except config.Busy as exc:
|
||||||
|
out = {"skipped": str(exc)}
|
||||||
print(json.dumps(out, ensure_ascii=False), file=sys.stderr)
|
print(json.dumps(out, ensure_ascii=False), file=sys.stderr)
|
||||||
|
|||||||
@@ -112,3 +112,70 @@ def test_md_foloseste_taierea_pe_paragrafe():
|
|||||||
text = "primul paragraf\n\n" + "al doilea paragraf " * 20
|
text = "primul paragraf\n\n" + "al doilea paragraf " * 20
|
||||||
assert len(indexer.chunk_document("x.md", text)) >= 1
|
assert len(indexer.chunk_document("x.md", text)) >= 1
|
||||||
assert indexer.chunk_document("x.md", text) == indexer.chunk_text(text)
|
assert indexer.chunk_document("x.md", text) == indexer.chunk_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------- concurenta intre reindexari
|
||||||
|
def test_lacatul_refuza_a_doua_reindexare():
|
||||||
|
"""Timer-ul nu trebuie sa porneasca peste o sincronizare manuala."""
|
||||||
|
import config
|
||||||
|
|
||||||
|
with config.exclusive():
|
||||||
|
with pytest.raises(config.Busy):
|
||||||
|
with config.exclusive():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_lacatul_se_elibereaza_dupa_iesire():
|
||||||
|
import config
|
||||||
|
|
||||||
|
with config.exclusive():
|
||||||
|
pass
|
||||||
|
with config.exclusive(): # trebuie sa mearga din nou
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_lacatul_se_elibereaza_si_la_exceptie():
|
||||||
|
import config
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
with config.exclusive():
|
||||||
|
raise ValueError("ceva")
|
||||||
|
with config.exclusive():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_indexul_se_scrie_atomic(write, monkeypatch):
|
||||||
|
"""Consumer-ul reciteste indexul la 30s; nu are voie sa prinda JSON pe jumatate."""
|
||||||
|
import config
|
||||||
|
import indexer
|
||||||
|
|
||||||
|
write("x.md", "un paragraf oarecare")
|
||||||
|
monkeypatch.setattr(indexer, "embed", lambda text: [0.1, 0.2])
|
||||||
|
vazute = []
|
||||||
|
real_replace = indexer.os.replace
|
||||||
|
|
||||||
|
def spion(src, dst):
|
||||||
|
vazute.append((str(src), str(dst)))
|
||||||
|
return real_replace(src, dst)
|
||||||
|
|
||||||
|
monkeypatch.setattr(indexer.os, "replace", spion)
|
||||||
|
indexer.build()
|
||||||
|
assert vazute and vazute[0][1] == str(config.INDEX_FILE)
|
||||||
|
assert not config.INDEX_FILE.with_suffix(".json.tmp").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_xml_invalid_e_raportat_ca_avertisment(write, monkeypatch):
|
||||||
|
import indexer
|
||||||
|
|
||||||
|
write("stricat.xml", "<root><a>x</a></root><in-plus/>")
|
||||||
|
monkeypatch.setattr(indexer, "embed", lambda text: [0.0])
|
||||||
|
out = indexer.build()
|
||||||
|
assert any("stricat.xml" in w for w in out.get("warnings", []))
|
||||||
|
|
||||||
|
|
||||||
|
def test_xml_valid_nu_produce_avertismente(write, monkeypatch):
|
||||||
|
import indexer
|
||||||
|
|
||||||
|
write("bun.xml", "<root><a>x</a></root>")
|
||||||
|
monkeypatch.setattr(indexer, "embed", lambda text: [0.0])
|
||||||
|
assert "warnings" not in indexer.build()
|
||||||
|
|||||||
Reference in New Issue
Block a user