Hub: installations.processing_enabled / sync_interval_seconds / last_sync_* (migration 0026). The first heartbeat of an installation without coverage queues exactly one INITIAL_SYNC; afterwards one SYNC_INCREMENTAL per interval (idempotent, backs off on failure). One fiscal job at a time per Agent (FIFO, stale RUNNING recovery). Job completion now updates the coverage snapshot, blockers and JOB_STARTED/COMPLETED/FAILED events. Panel: Sincronizar agora / Reprocessar tudo / Pausar / Retomar + status; pause is enforced Hub-side. Agent: local AGENT_JOBS_ENABLED is ignored (central control); SYNC_INCREMENTAL re-classifies only new/changed products via a per-product fingerprint+decision cache and rebuilds coverage from it; results carry operational summary and blockers; Firebird credentials come from a DPAPI firebird.bin. Setup: read-only SGV Firebird discovery (auto-select only when unambiguous, wizard chooser otherwise), starts the service, waits for the first heartbeat, upgrade-in-place mode. All jobs stay read-only (FIREBIRD_WRITES=0). Version 1.1.3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
4.8 KiB
Python
84 lines
4.8 KiB
Python
"""Real zero-touch runtime E2E against the local dev Hub + a real Firebird test database.
|
|
|
|
No manual step between heartbeat and result: a brand-new installation identity sends its
|
|
first heartbeat, the Hub queues exactly one INITIAL_SYNC, the Agent claims and executes it
|
|
(READ-ONLY), coverage/blockers land on the Hub; then the central scheduler queues a
|
|
SYNC_INCREMENTAL by itself (interval shortened to 1 s for the test) which must reprocess
|
|
only the delta (nothing changed -> 0), and finally a central pause stops job execution while
|
|
heartbeats continue.
|
|
|
|
Usage (dev Hub on :8000, .env with FB credentials): python scripts/e2e_zero_touch_local.py
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path[:0] = [str(ROOT), str(ROOT / "agent" / "src"), str(ROOT / "agent" / "scripts")]
|
|
|
|
from dotenv import dotenv_values # noqa: E402
|
|
|
|
env_file = dotenv_values(ROOT / ".env")
|
|
HUB = "http://localhost:8000"
|
|
|
|
|
|
def psql(sql):
|
|
out = subprocess.run(["docker", "exec", "projeto-postgres-1", "psql", "-U", "gti", "-d", "gti_fiscal", "-At", "-F", "|", "-c", sql], capture_output=True, text=True)
|
|
return out.stdout.strip()
|
|
|
|
|
|
def main():
|
|
import run_agent_service as service
|
|
|
|
tmp = Path(tempfile.mkdtemp(prefix="gti_zt_"))
|
|
# A brand-new Agent (own tenant + token), like a freshly enrolled installation — never the shared dev agent.
|
|
reg = subprocess.run(["docker", "exec", "projeto-fiscal-hub-1", "python", "central/scripts/register_agent.py", "--company", f"e2e-zero-touch-{int(time.time())}", "--name", "zt"], capture_output=True, text=True).stdout
|
|
token = next(line.split("=", 1)[1] for line in reg.splitlines() if line.strip().startswith("AGENT_TOKEN="))
|
|
installation = str(uuid.uuid4())
|
|
os.environ.update({
|
|
"GTI_HUB_URL": HUB, "AGENT_TOKEN": token, "AGENT_ID": "dev", "INSTALLATION_ID": installation,
|
|
"FIREBIRD_DATABASE_PATH": env_file["FIREBIRD_DATABASE_PATH"], "FB_USER": env_file.get("FB_USER", "SYSDBA"), "FB_PASSWORD": env_file["FB_PASSWORD"],
|
|
"FIREBIRD_CLIENT_LIBRARY": env_file.get("FIREBIRD_CLIENT_LIBRARY", ""), "FIREBIRD_WRITES": "0", "FISCAL_MODE": "DRY_RUN",
|
|
"AGENT_STATE_DB": str(tmp / "state.sqlite"), "AGENT_LOG_PATH": str(tmp / "agent.log"), "POLL_INTERVAL_SECONDS": "0.2",
|
|
"AGENT_JOBS_ENABLED": "0", # legacy local toggle deliberately set to 0: must be ignored (control is central)
|
|
})
|
|
config = service.Config(os.environ)
|
|
service.setup_logging(config.log_path)
|
|
print("INSTALLATION_ID=", installation, "FIREBIRD_WRITES=", config.firebird_writes)
|
|
|
|
def run(n):
|
|
service.run(config, os.environ, max_iterations=n)
|
|
|
|
# 1) first heartbeat -> INITIAL_SYNC by the Hub -> claimed + executed in the same loop
|
|
run(4)
|
|
jobs = psql(f"select j.id,j.kind,j.status,j.result_summary->>'products_total',j.result_summary->>'products_changed' from jobs j join installations i on i.agent_id=j.agent_id where i.installation_id='{installation}' order by j.id")
|
|
print("AFTER_INITIAL:\n" + jobs)
|
|
snap = psql(f"select s.total_products,s.fiscally_complete,s.safe_blocked,s.failed_products,s.blockers from agent_snapshots s join installations i on i.id=s.installation_id where i.installation_id='{installation}'")
|
|
print("SNAPSHOT=", snap)
|
|
blockers = psql(f"select count(*) from blocked_products b join installations i on i.id=b.installation_id where i.installation_id='{installation}'")
|
|
print("BLOCKERS_TOTAL=", blockers)
|
|
|
|
# 2) central scheduler: shorten the interval, next heartbeat queues one SYNC_INCREMENTAL by itself
|
|
psql(f"update installations set sync_interval_seconds=1 where installation_id='{installation}'")
|
|
time.sleep(2)
|
|
run(4)
|
|
print("AFTER_INCREMENTAL:\n" + psql(f"select j.id,j.kind,j.status,j.payload->>'reason',j.result_summary->>'products_total',j.result_summary->>'products_changed' from jobs j join installations i on i.agent_id=j.agent_id where i.installation_id='{installation}' order by j.id"))
|
|
|
|
# 3) central pause: heartbeat continues, nothing is claimed
|
|
psql(f"update installations set processing_enabled=false where installation_id='{installation}'")
|
|
before = psql("select count(*) from jobs where status='RUNNING'")
|
|
time.sleep(2)
|
|
run(3)
|
|
print("PAUSED_RUNNING_JOBS=", before, "LAST_STATUS=", psql(f"select last_status from installations where installation_id='{installation}'"))
|
|
print("WRITES:", psql(f"select coalesce(sum((result_summary->>'writes_attempted')::int),0), coalesce(sum((result_summary->>'writes_succeeded')::int),0) from jobs j join installations i on i.agent_id=j.agent_id where i.installation_id='{installation}'"))
|
|
print("LOCAL_ipi_write_log_rows=", __import__("sqlite3").connect(tmp / "state.sqlite").execute("select count(*), sum(write_status='DRY_RUN_ONLY') from ipi_write_log").fetchone())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|