fix(agent_jobs): mover cola de SQLite a ficheros JSON (cross-9p safe)
Bug: Echo (gx-cli en WSL) recibia "disk I/O error" al INSERT en la tabla `agent_jobs` de graph_explorer.db. Causa: graph_explorer.exe mantiene esa BD abierta con journal_mode=WAL desde Windows, y SQLite WAL exige mmap del .shm compartido entre procesos. Cuando un escritor accede via /mnt/c (9p) y el otro nativo NTFS, ese mmap falla. El proyecto ya habia resuelto este patron antes: el contador de mutaciones (.mutations.marker) usa fichero plano en vez de SQL por exactamente la misma razon. agent_jobs era la unica cola que se quedo en SQLite — momento de aplicar el mismo fix. Cambios: * gx-cli cmd_enricher_run: en lugar de INSERT, escribe `<app_dir>/agent_jobs_queue/<req_id>.json` con el payload del job. Atomic write (tmp + rename, atomico tanto en NTFS como en 9p). * main.cpp polling: en lugar de SELECT/DELETE sobre agent_jobs, escanea ese directorio cada frame, lee cada JSON via json_extract (sqlite3 in-memory, sin tocar archivos en disco), llama jobs_submit, y borra el fichero. Throttle a 8 jobs por frame igual que antes. * main.cpp: anyade <filesystem> y <fstream>. * tests/test_gx_cli.py: 5 tests nuevos en TestCliEnricherRun: - escribe fichero JSON con req_id como nombre - NO crea tabla agent_jobs en graph_explorer.db (regresion) - errores claros si enricher o nodo no existen - no quedan .tmp tras encolado exitoso WSL 79 / Windows 68 + 11 skipped.
This commit is contained in:
@@ -316,6 +316,86 @@ class TestCliRelations:
|
||||
assert rows == []
|
||||
|
||||
|
||||
class TestCliEnricherRun:
|
||||
"""`enricher run` encola un job. Debe escribir un fichero JSON en
|
||||
`<app_dir>/agent_jobs_queue/` (NO insertar en SQL). Esto blinda
|
||||
contra la regresion del bug "disk I/O error" que aparecia cuando
|
||||
gx-cli (en WSL) escribia agent_jobs en graph_explorer.db abierta
|
||||
en WAL desde Windows."""
|
||||
|
||||
def _make_enricher(self, env_dirs, eid: str = "split_sentences"):
|
||||
"""Crea un manifest dummy en el dir de enrichers para que la
|
||||
validacion de gx-cli (`if manifest.yaml exists`) pase."""
|
||||
edir = env_dirs["dir"] / "enrichers" / eid
|
||||
edir.mkdir(parents=True, exist_ok=True)
|
||||
(edir / "manifest.yaml").write_text(
|
||||
f"id: {eid}\nname: x\napplies_to: [text]\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
def test_enricher_run_writes_json_file(self, env_dirs):
|
||||
self._make_enricher(env_dirs, "split_sentences")
|
||||
node = run_gx(env_dirs, "node", "create", "--name", "doc",
|
||||
"--type", "text")
|
||||
out = run_gx(env_dirs, "enricher", "run", "split_sentences",
|
||||
"--node", node["id"])
|
||||
# 1 fichero JSON dropped en agent_jobs_queue/
|
||||
queue = env_dirs["dir"] / "agent_jobs_queue"
|
||||
files = list(queue.glob("*.json"))
|
||||
assert len(files) == 1
|
||||
payload = json.loads(files[0].read_text(encoding="utf-8"))
|
||||
assert payload["enricher_id"] == "split_sentences"
|
||||
assert payload["node_id"] == node["id"]
|
||||
assert payload["id"] == out["request_id"]
|
||||
assert "params_json" in payload
|
||||
# Naming: el fichero se llama <req_id>.json, no .tmp
|
||||
assert files[0].name == f"{out['request_id']}.json"
|
||||
|
||||
def test_enricher_run_does_not_use_sqlite(self, env_dirs):
|
||||
"""Regresion del bug WAL cross-9p: el flujo NO debe abrir ni
|
||||
crear la tabla agent_jobs en graph_explorer.db."""
|
||||
self._make_enricher(env_dirs, "split_sentences")
|
||||
node = run_gx(env_dirs, "node", "create", "--name", "doc",
|
||||
"--type", "text")
|
||||
run_gx(env_dirs, "enricher", "run", "split_sentences",
|
||||
"--node", node["id"])
|
||||
# graph_explorer.db NO debe haber recibido tabla agent_jobs.
|
||||
cn = sqlite3.connect(env_dirs["app"])
|
||||
try:
|
||||
row = cn.execute(
|
||||
"SELECT name FROM sqlite_master "
|
||||
"WHERE type='table' AND name='agent_jobs'"
|
||||
).fetchone()
|
||||
finally:
|
||||
cn.close()
|
||||
assert row is None, \
|
||||
"agent_jobs table re-creada en graph_explorer.db; el queue " \
|
||||
"debe vivir en ficheros, NO en SQLite (cross-9p WAL falla)"
|
||||
|
||||
def test_enricher_run_unknown_enricher_errors(self, env_dirs):
|
||||
out = run_gx(env_dirs, "enricher", "run", "no_existe",
|
||||
expect_ok=False)
|
||||
assert out.get("ok") is False
|
||||
assert "not found" in (out.get("error") or "").lower()
|
||||
|
||||
def test_enricher_run_unknown_node_errors(self, env_dirs):
|
||||
self._make_enricher(env_dirs, "split_sentences")
|
||||
out = run_gx(env_dirs, "enricher", "run", "split_sentences",
|
||||
"--node", "node_inexistente", expect_ok=False)
|
||||
assert out.get("ok") is False
|
||||
assert "node not found" in (out.get("error") or "").lower()
|
||||
|
||||
def test_enricher_run_atomic_write(self, env_dirs):
|
||||
"""No deben quedar ficheros .tmp tras un encolado exitoso."""
|
||||
self._make_enricher(env_dirs, "split_sentences")
|
||||
node = run_gx(env_dirs, "node", "create", "--name", "doc",
|
||||
"--type", "text")
|
||||
run_gx(env_dirs, "enricher", "run", "split_sentences",
|
||||
"--node", node["id"])
|
||||
queue = env_dirs["dir"] / "agent_jobs_queue"
|
||||
tmp_files = list(queue.glob("*.tmp"))
|
||||
assert tmp_files == []
|
||||
|
||||
|
||||
class TestCliQuery:
|
||||
def test_query_select(self, env_dirs):
|
||||
run_gx(env_dirs, "node", "create", "--name", "q1", "--type", "text")
|
||||
|
||||
Reference in New Issue
Block a user