feat: track crawl run employee changes and verify dismissals
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.models import CrawlRun, Employee
|
||||
from app.models import CrawlError, CrawlRun, CrawlRunEmployeeChange, Employee
|
||||
from app.services.admin_data import (
|
||||
employee_detail_payload,
|
||||
employee_display_payload,
|
||||
format_admin_datetime,
|
||||
list_employees_page,
|
||||
run_detail_payload,
|
||||
run_payload,
|
||||
stats_payload,
|
||||
)
|
||||
@@ -207,3 +208,43 @@ def test_run_payload_calculates_progress():
|
||||
assert payload["processed_count"] == 5
|
||||
assert payload["progress_percent"] == 50.0
|
||||
assert payload["status_display"] == "Выполняется"
|
||||
|
||||
|
||||
def test_run_detail_payload_groups_changes_and_handles_old_runs(db_session):
|
||||
old_run = CrawlRun(source_url="https://miem.hse.ru/persons", status="completed")
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="completed", new_count=1)
|
||||
employee = Employee(
|
||||
profile_key="staff:new",
|
||||
canonical_url="https://www.hse.ru/staff/new",
|
||||
full_name="New Person",
|
||||
status="active",
|
||||
first_seen_at=datetime.now(timezone.utc),
|
||||
last_seen_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add_all([old_run, run, employee])
|
||||
db_session.commit()
|
||||
db_session.add(
|
||||
CrawlRunEmployeeChange(
|
||||
crawl_run_id=run.id,
|
||||
employee_id=employee.id,
|
||||
profile_key=employee.profile_key,
|
||||
profile_url=employee.canonical_url,
|
||||
full_name=employee.full_name,
|
||||
change_type="new",
|
||||
profile_available=True,
|
||||
message="added",
|
||||
)
|
||||
)
|
||||
db_session.add(
|
||||
CrawlError(crawl_run_id=run.id, profile_url=employee.canonical_url, error_type="ValueError", message="bad")
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
payload = run_detail_payload(db_session, run)
|
||||
old_payload = run_detail_payload(db_session, old_run)
|
||||
|
||||
assert payload["changes_detail_available"] is True
|
||||
assert payload["changes"]["new"][0]["full_name"] == "New Person"
|
||||
assert payload["errors"][0]["error_type"] == "ValueError"
|
||||
assert old_payload["changes_detail_available"] is False
|
||||
assert old_payload["changes"]["new"] == []
|
||||
|
||||
@@ -32,3 +32,19 @@ def test_admin_employees_route_redirects_to_directory():
|
||||
source = Path("app/admin.py").read_text(encoding="utf-8")
|
||||
|
||||
assert 'RedirectResponse("/admin/directory", status_code=303)' in source
|
||||
|
||||
|
||||
def test_runs_template_links_to_run_detail():
|
||||
template = Path("app/templates/runs.html").read_text(encoding="utf-8")
|
||||
|
||||
assert 'href="/admin/runs/{{ run.id }}"' in template
|
||||
|
||||
|
||||
def test_run_detail_template_extends_base_and_shows_change_groups():
|
||||
template = Path("app/templates/run_detail.html").read_text(encoding="utf-8")
|
||||
|
||||
assert '{% extends "base.html" %}' in template
|
||||
assert "Новые сотрудники" in template
|
||||
assert "Потеряшки" in template
|
||||
assert "Уволенные" in template
|
||||
assert "Детализация сотрудников для этого запуска недоступна" in template
|
||||
|
||||
@@ -13,7 +13,7 @@ import app.security as security
|
||||
from app.config import Settings, get_settings
|
||||
from app.db import Base, get_db
|
||||
from app.main import app
|
||||
from app.models import CrawlRun, Employee
|
||||
from app.models import CrawlRun, CrawlRunEmployeeChange, Employee
|
||||
from app.security import SESSION_COOKIE, sign_session
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_health_returns_versions():
|
||||
response = client.get("/api/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["backend_version"] == "0.3.0"
|
||||
assert response.json()["backend_version"] == "0.4.0"
|
||||
|
||||
|
||||
def test_mcp_requires_token_and_lists_tools():
|
||||
@@ -58,6 +58,7 @@ def test_mcp_requires_token_and_lists_tools():
|
||||
assert unauthorized.status_code == 401
|
||||
assert authorized.status_code == 200
|
||||
assert authorized.json()["result"]["tools"][0]["name"] == "search_employees"
|
||||
assert any(tool["name"] == "get_crawl_run_details" for tool in authorized.json()["result"]["tools"])
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
@@ -117,6 +118,76 @@ def test_mcp_search_employees_returns_matching_employee():
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_mcp_get_crawl_run_details_returns_changes():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="completed", new_count=1)
|
||||
employee = Employee(
|
||||
profile_key="staff:new",
|
||||
profile_type="staff",
|
||||
profile_id="new",
|
||||
canonical_url="https://www.hse.ru/staff/new",
|
||||
full_name="New Person",
|
||||
status="active",
|
||||
first_seen_at=datetime.now(timezone.utc),
|
||||
last_seen_at=datetime.now(timezone.utc),
|
||||
)
|
||||
session.add_all([run, employee])
|
||||
session.commit()
|
||||
session.add(
|
||||
CrawlRunEmployeeChange(
|
||||
crawl_run_id=run.id,
|
||||
employee_id=employee.id,
|
||||
profile_key=employee.profile_key,
|
||||
profile_url=employee.canonical_url,
|
||||
full_name=employee.full_name,
|
||||
change_type="new",
|
||||
profile_available=True,
|
||||
message="added",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
run_id = run.id
|
||||
session.close()
|
||||
|
||||
def override_db():
|
||||
db = Session()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_db
|
||||
app.dependency_overrides[get_settings] = lambda: Settings(
|
||||
mcp_auth_mode="token", mcp_token="secret", session_secret="session-secret"
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/mcp",
|
||||
headers={"Authorization": "Bearer secret"},
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {"name": "get_crawl_run_details", "arguments": {"run_id": run_id}},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
text = response.json()["result"]["content"][0]["text"]
|
||||
assert "New Person" in text
|
||||
assert "changes_detail_available" in text
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_mcp_oauth_rejects_static_token():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
@@ -281,8 +352,23 @@ def test_api_employees_and_stats_require_admin_session():
|
||||
current_data={"contacts": {"emails": ["alpha@hse.ru"]}, "sections": []},
|
||||
)
|
||||
)
|
||||
db.add(CrawlRun(source_url="https://miem.hse.ru/persons", status="completed", new_count=1))
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="completed", new_count=1)
|
||||
db.add(run)
|
||||
db.commit()
|
||||
db.add(
|
||||
CrawlRunEmployeeChange(
|
||||
crawl_run_id=run.id,
|
||||
employee_id=1,
|
||||
profile_key="staff:alpha",
|
||||
profile_url="https://www.hse.ru/staff/alpha",
|
||||
full_name="Alpha Person",
|
||||
change_type="new",
|
||||
profile_available=True,
|
||||
message="added",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
run_id = run.id
|
||||
db.close()
|
||||
|
||||
settings = Settings(admin_username="admin", admin_password="password", session_secret="session-secret")
|
||||
@@ -301,11 +387,14 @@ def test_api_employees_and_stats_require_admin_session():
|
||||
|
||||
employees = client.get("/api/employees", params={"q": "Alpha", "has_email": True})
|
||||
stats = client.get("/api/stats")
|
||||
run_details = client.get(f"/api/crawl-runs/{run_id}")
|
||||
|
||||
assert employees.status_code == 200
|
||||
assert employees.json()["total"] == 1
|
||||
assert stats.status_code == 200
|
||||
assert stats.json()["new_in_last_run"] == 1
|
||||
assert run_details.status_code == 200
|
||||
assert run_details.json()["changes"]["new"][0]["full_name"] == "Alpha Person"
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.models import CrawlRun, Employee
|
||||
from app.models import CrawlRun, CrawlRunEmployeeChange, Employee
|
||||
from app.services.crawler import _mark_dismissed, _upsert_employee
|
||||
|
||||
|
||||
def test_mark_dismissed_only_marks_missing_active_employees(db_session):
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code):
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, statuses):
|
||||
self.statuses = statuses
|
||||
|
||||
def get(self, url, **_kwargs):
|
||||
return FakeResponse(self.statuses[url])
|
||||
|
||||
|
||||
def test_mark_dismissed_records_missing_source_when_profile_is_available(db_session):
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="running")
|
||||
db_session.add(run)
|
||||
db_session.add(
|
||||
Employee(
|
||||
profile_key="staff:kept",
|
||||
@@ -16,8 +31,8 @@ def test_mark_dismissed_only_marks_missing_active_employees(db_session):
|
||||
)
|
||||
db_session.add(
|
||||
Employee(
|
||||
profile_key="staff:gone",
|
||||
canonical_url="https://www.hse.ru/staff/gone",
|
||||
profile_key="staff:missing",
|
||||
canonical_url="https://www.hse.ru/staff/missing",
|
||||
status="active",
|
||||
first_seen_at=datetime.now(timezone.utc),
|
||||
last_seen_at=datetime.now(timezone.utc),
|
||||
@@ -25,16 +40,53 @@ def test_mark_dismissed_only_marks_missing_active_employees(db_session):
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
dismissed = _mark_dismissed(db_session, {"staff:kept"})
|
||||
dismissed = _mark_dismissed(
|
||||
db_session,
|
||||
run,
|
||||
{"staff:kept"},
|
||||
FakeSession({"https://www.hse.ru/staff/missing": 200}),
|
||||
30,
|
||||
)
|
||||
|
||||
assert dismissed == 0
|
||||
assert db_session.query(Employee).filter_by(profile_key="staff:kept").one().status == "active"
|
||||
missing = db_session.query(Employee).filter_by(profile_key="staff:missing").one()
|
||||
assert missing.status == "active"
|
||||
assert missing.dismissed_at is None
|
||||
change = db_session.query(CrawlRunEmployeeChange).one()
|
||||
assert change.change_type == "missing_from_source"
|
||||
assert change.profile_available is True
|
||||
|
||||
|
||||
def test_mark_dismissed_marks_missing_employee_when_profile_is_unavailable(db_session):
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="running")
|
||||
employee = Employee(
|
||||
profile_key="staff:gone",
|
||||
canonical_url="https://www.hse.ru/staff/gone",
|
||||
status="active",
|
||||
first_seen_at=datetime.now(timezone.utc),
|
||||
last_seen_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add_all([run, employee])
|
||||
db_session.commit()
|
||||
|
||||
dismissed = _mark_dismissed(
|
||||
db_session,
|
||||
run,
|
||||
set(),
|
||||
FakeSession({"https://www.hse.ru/staff/gone": 404}),
|
||||
30,
|
||||
)
|
||||
|
||||
assert dismissed == 1
|
||||
assert db_session.query(Employee).filter_by(profile_key="staff:kept").one().status == "active"
|
||||
gone = db_session.query(Employee).filter_by(profile_key="staff:gone").one()
|
||||
assert gone.status == "dismissed"
|
||||
assert gone.dismissed_at is not None
|
||||
assert employee.status == "dismissed"
|
||||
assert employee.dismissed_at is not None
|
||||
change = db_session.query(CrawlRunEmployeeChange).one()
|
||||
assert change.change_type == "dismissed"
|
||||
assert change.profile_available is False
|
||||
|
||||
|
||||
def test_upsert_employee_increments_new_count_for_new_employee(db_session):
|
||||
def test_upsert_employee_increments_new_count_and_records_change_for_new_employee(db_session):
|
||||
run = CrawlRun(source_url="https://miem.hse.ru/persons", status="running")
|
||||
db_session.add(run)
|
||||
db_session.commit()
|
||||
@@ -56,3 +108,6 @@ def test_upsert_employee_increments_new_count_for_new_employee(db_session):
|
||||
db_session.commit()
|
||||
|
||||
assert run.new_count == 1
|
||||
change = db_session.query(CrawlRunEmployeeChange).one()
|
||||
assert change.change_type == "new"
|
||||
assert change.full_name == "New Person"
|
||||
|
||||
Reference in New Issue
Block a user