fix(integration): F41+F37 (Astra S2) — Agenten-Stundenlimit und SMTP-Env-Namen
Check Cross-Plugin Imports / check (push) Waiting to run
Check Cross-Plugin Imports / check (push) Waiting to run
F41 — Stundliches Agentenlimit zaehlte ab jetzt() statt letzte Stunde: one_hour_ago = datetime.now(UTC) zog die Stunde nie ab — die Abfrage zaehlte nur Eintraege ab dem aktuellen Zeitpunkt (wirksam null), das konfigurierte Limit schuetzte nicht vor wiederholten Starts. Fix: timedelta(hours=1) + Import. F37 — SMTP-Variablen hiessen in Compose anders als in Settings: Settings erwarten smtp_username/smtp_from_email/smtp_use_tls, Compose setzte SMTP_USER/SMTP_FROM/SMTP_TLS — Benutzername, Absender und TLS kamen nie an (Systemmails: Reset, Einladungen, geplante Alarmierung). Fix: Compose (app+worker) und .env-Beispiele durchgaengig auf die Settings-Namen (SMTP_USERNAME/SMTP_FROM_EMAIL/SMTP_USE_TLS) umgestellt. Prod-Check: SMTP dort aktuell unkonfiguriert (leere Werte verifiziert) — umbenennen risikofrei; sobald SMTP gesetzt wird, greift die Kette. Test-Anpassung (F11-Folge, Vertragsaenderung): 3 veraltete Approval- Unit-Tests in test_phase_f_agents (MagicMock-Ketten gegen VOR-F11- Semantik) durch dokumentierte Skip-Verweise auf die 6 echten F11-Tests in test_s1_security_guards ersetzt — reale DB, deterministisch. Verifikation: phase_f_agents 39 passed/3 skipped, ruff clean, agent_runner Syntax OK, Compose-Namen durchgaengig verifiziert.
This commit is contained in:
+3
-3
@@ -44,10 +44,10 @@ STORAGE_PATH=/data/storage
|
||||
# --- SMTP (for password reset emails) -----------------------------------------
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@example.com
|
||||
SMTP_USERNAME=noreply@example.com
|
||||
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
|
||||
SMTP_FROM=noreply@example.com
|
||||
SMTP_TLS=true
|
||||
SMTP_FROM_EMAIL=noreply@example.com
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# --- bcrypt tuning ----------------------------------------------------------
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
+3
-3
@@ -90,10 +90,10 @@ S3_SECURE=true
|
||||
# === SMTP / EMAIL ===
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=no-reply@localhost
|
||||
SMTP_TLS=true
|
||||
SMTP_FROM_EMAIL=no-reply@localhost
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# === RATE LIMITING ===
|
||||
RATE_LIMIT_LOGIN_MAX=5
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
@@ -65,7 +65,7 @@ async def run_agent(
|
||||
# ── Safety Check 1: Rate Limit ──
|
||||
if agent.max_executions_per_hour:
|
||||
async with factory() as db:
|
||||
one_hour_ago = datetime.now(UTC)
|
||||
one_hour_ago = datetime.now(UTC) - timedelta(hours=1)
|
||||
count_result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(AgentRun)
|
||||
|
||||
+6
-6
@@ -86,10 +86,10 @@ services:
|
||||
STORAGE_PATH: ${STORAGE_PATH:-/data/storage}
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USER: ${SMTP_USER:-}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_FROM: ${SMTP_FROM:-no-reply@localhost}
|
||||
SMTP_TLS: ${SMTP_TLS:-true}
|
||||
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-no-reply@localhost}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@media-on.de}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD must be set — no default credentials (Astra F30)}
|
||||
@@ -131,10 +131,10 @@ services:
|
||||
STORAGE_PATH: ${STORAGE_PATH:-/data/storage}
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USER: ${SMTP_USER:-}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_FROM: ${SMTP_FROM:-no-reply@localhost}
|
||||
SMTP_TLS: ${SMTP_TLS:-true}
|
||||
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-no-reply@localhost}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||
MAIL_ENCRYPTION_KEY: ${MAIL_ENCRYPTION_KEY:?MAIL_ENCRYPTION_KEY is required}
|
||||
volumes:
|
||||
- storage:/data/storage
|
||||
|
||||
@@ -767,66 +767,13 @@ class TestApproval:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_approval_request(self, tenant_id, mock_db):
|
||||
"""resolve_approval_request approves a pending request."""
|
||||
request_id = uuid.uuid4()
|
||||
approver_id = uuid.uuid4()
|
||||
req = ApprovalRequest(
|
||||
id=request_id,
|
||||
tenant_id=tenant_id,
|
||||
entity_type="agent_definition",
|
||||
entity_id=uuid.uuid4(),
|
||||
action="execute",
|
||||
requested_by=uuid.uuid4(),
|
||||
status="pending",
|
||||
)
|
||||
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = req
|
||||
mock_db.execute.return_value = result
|
||||
|
||||
updated = await resolve_approval_request(
|
||||
mock_db,
|
||||
tenant_id,
|
||||
request_id,
|
||||
decision="approved",
|
||||
approver_id=approver_id,
|
||||
comment="OK",
|
||||
)
|
||||
|
||||
assert updated is req
|
||||
assert req.status == "approved"
|
||||
assert req.approver_id == approver_id
|
||||
assert req.comment == "OK"
|
||||
assert req.resolved_at is not None
|
||||
"""Superseded by TestF11ApprovalBinding (real DB, deterministic)."""
|
||||
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding with a real database")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reject_approval_request(self, tenant_id, mock_db):
|
||||
"""resolve_approval_request rejects a pending request."""
|
||||
request_id = uuid.uuid4()
|
||||
req = ApprovalRequest(
|
||||
id=request_id,
|
||||
tenant_id=tenant_id,
|
||||
entity_type="agent_definition",
|
||||
entity_id=uuid.uuid4(),
|
||||
action="execute",
|
||||
requested_by=uuid.uuid4(),
|
||||
status="pending",
|
||||
)
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = req
|
||||
mock_db.execute.return_value = result
|
||||
|
||||
updated = await resolve_approval_request(
|
||||
mock_db,
|
||||
tenant_id,
|
||||
request_id,
|
||||
decision="rejected",
|
||||
approver_id=uuid.uuid4(),
|
||||
comment="No",
|
||||
)
|
||||
|
||||
assert updated is req
|
||||
assert req.status == "rejected"
|
||||
"""Superseded by TestF11ApprovalBinding (real DB, deterministic)."""
|
||||
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding with a real database")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expire_approval_request(self, tenant_id, mock_db):
|
||||
@@ -852,30 +799,8 @@ class TestApproval:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_non_pending_returns_none(self, tenant_id, mock_db):
|
||||
"""Resolving a non-pending request returns None."""
|
||||
request_id = uuid.uuid4()
|
||||
req = ApprovalRequest(
|
||||
id=request_id,
|
||||
tenant_id=tenant_id,
|
||||
entity_type="agent_definition",
|
||||
entity_id=uuid.uuid4(),
|
||||
action="execute",
|
||||
requested_by=uuid.uuid4(),
|
||||
status="approved",
|
||||
)
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = req
|
||||
mock_db.execute.return_value = result
|
||||
|
||||
updated = await resolve_approval_request(
|
||||
mock_db,
|
||||
tenant_id,
|
||||
request_id,
|
||||
decision="rejected",
|
||||
approver_id=uuid.uuid4(),
|
||||
)
|
||||
|
||||
assert updated is None
|
||||
"""Superseded: F11 changed the contract to ApprovalDecisionError(409)."""
|
||||
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding (not_pending 409) with a real database")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_approvals_with_filters(self, tenant_id, mock_db):
|
||||
|
||||
Reference in New Issue
Block a user