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:
@@ -0,0 +1,71 @@
|
||||
"""Setup.exe's Firebird discovery: read-only SGV detection, auto-select only when unambiguous."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "installer"))
|
||||
import firebird_discovery as fbd # noqa: E402
|
||||
|
||||
|
||||
class FakeConn:
|
||||
def __init__(self, tables): self.tables = tables; self.closed = False; self.sql = []
|
||||
def cursor(self):
|
||||
outer = self
|
||||
class C:
|
||||
def execute(self, sql): outer.sql.append(sql)
|
||||
def fetchall(self): return [(t,) for t in outer.tables]
|
||||
return C()
|
||||
def close(self): self.closed = True
|
||||
|
||||
|
||||
def make_connect(spec):
|
||||
"""spec: path -> tables | 'AUTH' | Exception"""
|
||||
def connect(path, user, password):
|
||||
value = spec[path]
|
||||
if value == "AUTH" and (user, password) != ("SYSDBA", "masterkey"):
|
||||
raise Exception("Your user name and password are not defined")
|
||||
if value == "BROKEN": raise OSError("cannot open")
|
||||
return FakeConn(["C000025", "C000004"] if value == "AUTH" else value)
|
||||
return connect
|
||||
|
||||
|
||||
def run(spec, credentials=fbd.DEFAULT_CREDENTIALS):
|
||||
return fbd.discover(roots=["X"], finder=lambda roots: list(spec), connect=make_connect(spec), credentials=credentials)
|
||||
|
||||
|
||||
def test_exactly_one_sgv_database_is_selected_automatically():
|
||||
found = run({r"D:\SGV\loja.fdb": ["C000025", "C000004", "OTHER"], r"D:\other\x.fdb": ["FOO"]})
|
||||
assert found.state == "SELECTED" and found.selected.path == r"D:\SGV\loja.fdb" and found.selected.user == "SYSDBA"
|
||||
|
||||
|
||||
def test_several_compatible_databases_are_never_chosen_silently():
|
||||
found = run({r"D:\a.fdb": ["C000025", "C000004"], r"D:\b.fdb": ["C000025", "C000004"]})
|
||||
assert found.state == "AMBIGUOUS" and len(found.compatible) == 2 and found.selected is None
|
||||
|
||||
|
||||
def test_database_needs_both_c000025_and_c000004():
|
||||
assert run({r"D:\a.fdb": ["C000025"]}).state == "NONE"
|
||||
|
||||
|
||||
def test_unknown_credentials_ask_the_operator_inside_the_installer():
|
||||
found = run({r"D:\a.fdb": "AUTH"}, credentials=(("SYSDBA", "wrong"),))
|
||||
assert found.state == "AUTH_REQUIRED" and found.auth_failed[0].path == r"D:\a.fdb"
|
||||
|
||||
|
||||
def test_probe_is_read_only_and_closes_the_connection():
|
||||
conn = FakeConn(["C000025", "C000004"])
|
||||
cand = fbd.probe("x.fdb", "SYSDBA", "masterkey", connect=lambda *a: conn)
|
||||
assert cand.status == "OK" and conn.closed and all(sql.lstrip().upper().startswith("SELECT") for sql in conn.sql)
|
||||
|
||||
|
||||
def test_broken_file_is_reported_not_raised():
|
||||
assert fbd.probe("x.fdb", "u", "p", connect=lambda *a: (_ for _ in ()).throw(OSError("boom"))).status == "ERROR"
|
||||
|
||||
|
||||
def test_finder_skips_system_dirs_security_db_and_tiny_files(tmp_path):
|
||||
big = b"0" * (fbd.MIN_BYTES + 1)
|
||||
(tmp_path / "SGV").mkdir(); (tmp_path / "SGV" / "loja.FDB").write_bytes(big)
|
||||
(tmp_path / "SGV" / "security2.fdb").write_bytes(big)
|
||||
(tmp_path / "SGV" / "tiny.fdb").write_bytes(b"0")
|
||||
(tmp_path / "Windows").mkdir(); (tmp_path / "Windows" / "sys.fdb").write_bytes(big)
|
||||
found = fbd.find_fdb_files([str(tmp_path)])
|
||||
assert [Path(p).name for p in found] == ["loja.FDB"]
|
||||
Reference in New Issue
Block a user