Some checks failed
CI / test (push) Failing after 4m34s
T2 frontend CronPoolMonitor: polls every 5s, unmount clears interval, plus it.fails guard for missing visibilitychange listener (S6). T3 router ORM contract: heartbeat updates fields + 404, decision-logs POST persists + 404, GET lists inserted (guards S2 — must continue to pass after router handlers move into a service in P3). T8 decision-log immutability: append-only on context change passes; dedupe of identical (decision_type, context) is xfail (real gap). T7 alert→auto-scale: task_backlog alert recorded on first call (passes); check_alerts never invokes auto_scale is xfail (real gap, §6.4). All real gaps are logged in .scratch/v111-architecture-scan.md §6.
164 lines
4.6 KiB
Python
164 lines
4.6 KiB
Python
"""Router ORM contract guards (Gitea issue #6 / T3).
|
|
|
|
These tests pin the observable behaviour of router endpoints that currently
|
|
embed ORM writes directly (S2). When those handlers are later moved into a
|
|
service, the tests should still pass unchanged — that is the contract.
|
|
"""
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import Session, SQLModel, create_engine, select
|
|
|
|
from agenteval.intelligent_eval.models import IntelligentEvalStatus
|
|
from agenteval.storage.db import (
|
|
IntelligentEvalDB,
|
|
IntelligentEvalDecisionLogDB,
|
|
OpenClawCronPoolDB,
|
|
utc_now,
|
|
)
|
|
from agenteval.web.app import app
|
|
from agenteval.web.deps import get_db
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(tmp_path):
|
|
from agenteval.storage.db import ( # noqa: F401
|
|
IntelligentEvalDB,
|
|
IntelligentEvalSessionDB,
|
|
IntelligentEvalTaskQueueDB,
|
|
)
|
|
|
|
engine = create_engine(
|
|
f"sqlite:///{tmp_path / 'test.db'}",
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
SQLModel.metadata.create_all(engine)
|
|
session = Session(engine)
|
|
|
|
def override_get_db():
|
|
try:
|
|
yield session
|
|
finally:
|
|
pass
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
yield TestClient(app)
|
|
app.dependency_overrides.clear()
|
|
session.close()
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.fixture()
|
|
def db_session(client):
|
|
return next(app.dependency_overrides[get_db]())
|
|
|
|
|
|
# T3.1 — heartbeat updates last_active_at / status / current_eval_id
|
|
|
|
|
|
def test_heartbeat_updates_cron_fields(client: TestClient, db_session: Session):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id="cron-1",
|
|
status="idle",
|
|
last_active_at=utc_now() - timedelta(hours=1),
|
|
)
|
|
db_session.add(cron)
|
|
db_session.commit()
|
|
|
|
response = client.post(
|
|
"/api/openclaw/crons/cron-1/heartbeat",
|
|
json={"status": "busy", "current_eval_id": "eval-42"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
db_session.refresh(cron)
|
|
assert cron.status == "busy"
|
|
assert cron.current_eval_id == "eval-42"
|
|
|
|
|
|
def test_heartbeat_404_for_unknown_cron(client: TestClient):
|
|
response = client.post(
|
|
"/api/openclaw/crons/does-not-exist/heartbeat",
|
|
json={"status": "busy", "current_eval_id": None},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
# T3.2 — decision-logs POST persists to DB
|
|
|
|
|
|
def test_create_decision_log_persists(client: TestClient, db_session: Session):
|
|
import uuid
|
|
eval_db = IntelligentEvalDB(
|
|
id=str(uuid.uuid4()),
|
|
name="eval-dl",
|
|
target_id="t1",
|
|
status=IntelligentEvalStatus.EXECUTING.value,
|
|
)
|
|
db_session.add(eval_db)
|
|
db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/api/intelligent-evals/{eval_db.id}/decision-logs",
|
|
json={
|
|
"decision_type": "execute_session",
|
|
"reason": "slot_due",
|
|
"context": {"slot": "8-10h", "deficit": 2},
|
|
"cron_id": "cron-1",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
logs = db_session.exec(
|
|
select(IntelligentEvalDecisionLogDB).where(
|
|
IntelligentEvalDecisionLogDB.eval_id == eval_db.id
|
|
)
|
|
).all()
|
|
assert len(logs) == 1
|
|
assert logs[0].decision_type == "execute_session"
|
|
assert logs[0].reason == "slot_due"
|
|
|
|
|
|
def test_create_decision_log_404_for_unknown_eval(client: TestClient):
|
|
import uuid
|
|
response = client.post(
|
|
f"/api/intelligent-evals/{uuid.uuid4()}/decision-logs",
|
|
json={
|
|
"decision_type": "execute_session",
|
|
"reason": "slot_due",
|
|
"context": {},
|
|
"cron_id": "cron-1",
|
|
},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
# T3.3 — decision-logs GET lists logs
|
|
|
|
|
|
def test_list_decision_logs_returns_inserted(client: TestClient, db_session: Session):
|
|
import uuid
|
|
eval_db = IntelligentEvalDB(
|
|
id=str(uuid.uuid4()),
|
|
name="eval-dl-list",
|
|
target_id="t1",
|
|
status=IntelligentEvalStatus.EXECUTING.value,
|
|
)
|
|
db_session.add(eval_db)
|
|
db_session.commit()
|
|
|
|
for dtype in ("execute_session", "wait", "start_analysis"):
|
|
r = client.post(
|
|
f"/api/intelligent-evals/{eval_db.id}/decision-logs",
|
|
json={"decision_type": dtype, "reason": "test", "context": {}, "cron_id": "cron-1"},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
response = client.get(f"/api/intelligent-evals/{eval_db.id}/decision-logs")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "logs" in data
|
|
types = {log["decision_type"] for log in data["logs"]}
|
|
assert types == {"execute_session", "wait", "start_analysis"}
|