Files
GTI_GERENCIADOR_FISCAL/fiscal/central/tests/test_jobs.py
T
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

215 lines
8.7 KiB
Python

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.jobs import claim_next_job, complete_job, create_job, fail_job
from central.app.main import create_app
from central.app.models import Agent, Base, BlockedProduct, Installation, Job, Tenant
def _make_agent(session):
tenant = Tenant(name="development")
session.add(tenant); session.flush()
agent = Agent(tenant_id=tenant.id, name="agent-a", token_hash="hash-a")
other = Agent(tenant_id=tenant.id, name="agent-b", token_hash="hash-b")
session.add_all([agent, other]); session.commit()
return agent, other
def test_claim_is_scoped_to_owning_agent_and_marks_running():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, other = _make_agent(session)
job = create_job(session, agent, "REPROCESS_ALL")
assert claim_next_job(session, other) is None # other agent never sees it
claimed = claim_next_job(session, agent)
assert claimed.id == job.id
assert claimed.status == "RUNNING"
assert claimed.attempts == 1
def test_claim_is_not_reentrant_once_running():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
first = claim_next_job(session, agent)
second = claim_next_job(session, agent) # repeated polling must not duplicate consumption
assert first is not None
assert second is None
def test_completed_job_can_never_run_again():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
job = claim_next_job(session, agent)
assert complete_job(session, job, {"writes": 0}) is True
assert session.get(Job, job.id).status == "COMPLETED"
assert claim_next_job(session, agent) is None
assert complete_job(session, job, {"writes": 1}) is False # already terminal
def test_failed_job_is_terminal_with_sanitized_error():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
job = claim_next_job(session, agent)
assert fail_job(session, job, "boom " * 1000) is True
stored = session.get(Job, job.id)
assert stored.status == "FAILED"
assert len(stored.error) <= 2000
def _client():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
return TestClient(app), engine
def test_job_api_end_to_end_flow():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
heartbeat = client.post("/v1/agents/heartbeat", json={"status": "IDLE", "schema_fingerprint": "abc", "agent_version": "0.1.0"}, headers=headers)
assert heartbeat.status_code == 200
assert heartbeat.json()["enabled"] is True
# Zero-touch: the first heartbeat makes the Hub queue exactly one INITIAL_SYNC by itself.
initial = client.get("/v1/jobs/next", headers=headers).json()
assert initial["kind"] == "INITIAL_SYNC"
assert client.post(f"/v1/jobs/{initial['job_id']}/complete", json={"result_summary": {}}, headers=headers).json()["status"] == "COMPLETED"
created = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["000026"]}}, headers=headers)
assert created.status_code == 200
job_id = created.json()["job_id"]
assert created.json()["status"] == "PENDING"
claimed = client.get("/v1/jobs/next", headers=headers)
assert claimed.json()["job_id"] == job_id
assert claimed.json()["status"] == "RUNNING"
empty = client.get("/v1/jobs/next", headers=headers)
assert empty.json() is None # no more pending work; not the same job again
completed = client.post(f"/v1/jobs/{job_id}/complete", json={"result_summary": {"writes_succeeded": 1}}, headers=headers)
assert completed.json()["status"] == "COMPLETED"
assert completed.json()["result_summary"]["writes_succeeded"] == 1
fetched = client.get(f"/v1/jobs/{job_id}", headers=headers)
assert fetched.json()["status"] == "COMPLETED"
def test_agent_status_derives_online_offline():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
offline = client.get("/v1/agents/me", headers=headers)
assert offline.json()["status"] == "OFFLINE" # never heartbeated yet
client.post("/v1/agents/heartbeat", json={"status": "IDLE"}, headers=headers)
online = client.get("/v1/agents/me", headers=headers)
body = online.json()
assert body["status"] == "ONLINE"
assert body["enabled"] is True
assert "company_id" in body
def test_agent_identity_endpoint_returns_existing_installation_identity_only():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
installation_id = "00000000-0000-4000-8000-000000000222"
heartbeat = client.post(
"/v1/agents/heartbeat",
json={
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"agent_version": "1.1.1",
"status": "IDLE",
},
headers=headers,
)
assert heartbeat.status_code == 200
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
assert response.json() == {
"agent_id": heartbeat.json()["agent_id"],
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"version": "1.1.1",
"status": "ONLINE",
}
def test_agent_identity_endpoint_returns_null_installation_without_creating_one():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
body = response.json()
assert body == {
"agent_id": body["agent_id"],
"installation_id": None,
"hostname": None,
"version": None,
"status": "OFFLINE",
}
assert isinstance(body["agent_id"], int) and body["agent_id"] > 0
with Session(engine) as session:
assert session.query(Installation).count() == 0
def test_agent_identity_endpoint_requires_agent_authentication():
client, _ = _client()
response = client.get("/v1/agents/me/identity")
assert response.status_code == 401
def test_job_creation_rejects_unknown_kind():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.post("/v1/jobs", json={"kind": "DELETE_EVERYTHING"}, headers=headers)
assert response.status_code == 400
def test_reprocess_products_requires_codigos():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {}}, headers=headers)
assert response.status_code == 400
def test_blocker_registry_api_deduplicates_and_resolves():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
assert client.post("/v1/agents/heartbeat", json={"status": "IDLE", "installation_id": "00000000-0000-4000-8000-000000000001"}, headers=headers).status_code == 200
summary = {"blocked_products": [{"company": "C", "product_code": "P1", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "v1", "evidence_summary": {"source": "ERP"}}]}
created = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["P1"]}}, headers=headers)
job_id = created.json()["job_id"]
client.get("/v1/jobs/next", headers=headers)
client.post(f"/v1/jobs/{job_id}/complete", json={"result_summary": summary}, headers=headers)
listed = client.get("/v1/blockers?field=CFOP", headers=headers)
assert listed.status_code == 200
assert len(listed.json()["blockers"]) == 1
with Session(engine) as session:
assert session.query(BlockedProduct).one().installation_id is not None
blocker_id = listed.json()["blockers"][0]["id"]
resolved = client.post(f"/v1/blockers/{blocker_id}/resolve", json={"resolution_source": "TEST"}, headers=headers)
assert resolved.json()["resolved"] is True
assert client.get("/v1/blockers", headers=headers).json()["blockers"] == []