feat: zero-touch Agent — central scheduler, central pause, delta sync, discovery-driven installer

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>
This commit is contained in:
gti
2026-09-24 08:47:00 -03:00
co-authored by Claude Sonnet 5
parent 9f9e06e9e9
commit bb663a979c
36 changed files with 1409 additions and 118 deletions
+7 -6
View File
@@ -90,10 +90,12 @@ def test_config_defaults_from_env():
assert config.agent_jobs_enabled is True
def test_config_can_disable_agent_job_polling():
def test_local_agent_jobs_toggle_is_ignored_control_is_central():
# Zero-touch: a stale AGENT_JOBS_ENABLED=0 left in an old agent.env must not silence the Agent;
# only the Hub (heartbeat processing_enabled) can pause it.
config = service.Config({"FIREBIRD_DATABASE_PATH": AUTHORIZED, "AGENT_JOBS_ENABLED": "0"})
assert config.agent_jobs_enabled is False
assert config.agent_jobs_enabled is True
def test_config_reads_gti_hub_url_and_writes_flag():
@@ -294,7 +296,7 @@ class _ExternalStopFlag:
self.stop = False
def test_run_sends_heartbeat_but_never_polls_when_jobs_disabled(monkeypatch, tmp_path):
def test_run_sends_heartbeat_but_never_polls_when_paused_centrally(monkeypatch, tmp_path):
stopper = _ExternalStopFlag()
fake_hub = _FakeHubForLoop()
installation_id = "00000000-0000-4000-8000-000000000222"
@@ -308,11 +310,11 @@ def test_run_sends_heartbeat_but_never_polls_when_jobs_disabled(monkeypatch, tmp
calls["heartbeat"] += 1
calls["heartbeat_installation_id"] = payload["installation_id"]
stopper.stop = True
return {"agent_id": 2}
return {"agent_id": 2, "processing_enabled": False} # paused centrally by the Hub
def forbidden_next_job():
calls["next_job"] += 1
raise AssertionError("jobs are disabled; next_job must not be called")
raise AssertionError("processing is paused centrally; next_job must not be called")
fake_hub.identity = identity
fake_hub.heartbeat_metadata = heartbeat_metadata
@@ -323,7 +325,6 @@ def test_run_sends_heartbeat_but_never_polls_when_jobs_disabled(monkeypatch, tmp
config = _config(
agent_id="2",
agent_jobs_enabled=False,
poll_interval_seconds=0,
state_db=str(tmp_path / "agent.db"),
)