fix(integration): F41+F37 (Astra S2) — Agenten-Stundenlimit und SMTP-Env-Namen
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
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 (for password reset emails) -----------------------------------------
|
||||||
SMTP_HOST=smtp.example.com
|
SMTP_HOST=smtp.example.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USER=noreply@example.com
|
SMTP_USERNAME=noreply@example.com
|
||||||
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
|
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
|
||||||
SMTP_FROM=noreply@example.com
|
SMTP_FROM_EMAIL=noreply@example.com
|
||||||
SMTP_TLS=true
|
SMTP_USE_TLS=true
|
||||||
|
|
||||||
# --- bcrypt tuning ----------------------------------------------------------
|
# --- bcrypt tuning ----------------------------------------------------------
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|||||||
+3
-3
@@ -90,10 +90,10 @@ S3_SECURE=true
|
|||||||
# === SMTP / EMAIL ===
|
# === SMTP / EMAIL ===
|
||||||
SMTP_HOST=localhost
|
SMTP_HOST=localhost
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USER=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=no-reply@localhost
|
SMTP_FROM_EMAIL=no-reply@localhost
|
||||||
SMTP_TLS=true
|
SMTP_USE_TLS=true
|
||||||
|
|
||||||
# === RATE LIMITING ===
|
# === RATE LIMITING ===
|
||||||
RATE_LIMIT_LOGIN_MAX=5
|
RATE_LIMIT_LOGIN_MAX=5
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -65,7 +65,7 @@ async def run_agent(
|
|||||||
# ── Safety Check 1: Rate Limit ──
|
# ── Safety Check 1: Rate Limit ──
|
||||||
if agent.max_executions_per_hour:
|
if agent.max_executions_per_hour:
|
||||||
async with factory() as db:
|
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(
|
count_result = await db.execute(
|
||||||
select(func.count())
|
select(func.count())
|
||||||
.select_from(AgentRun)
|
.select_from(AgentRun)
|
||||||
|
|||||||
+6
-6
@@ -86,10 +86,10 @@ services:
|
|||||||
STORAGE_PATH: ${STORAGE_PATH:-/data/storage}
|
STORAGE_PATH: ${STORAGE_PATH:-/data/storage}
|
||||||
SMTP_HOST: ${SMTP_HOST:-}
|
SMTP_HOST: ${SMTP_HOST:-}
|
||||||
SMTP_PORT: ${SMTP_PORT:-587}
|
SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
SMTP_USER: ${SMTP_USER:-}
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
SMTP_FROM: ${SMTP_FROM:-no-reply@localhost}
|
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-no-reply@localhost}
|
||||||
SMTP_TLS: ${SMTP_TLS:-true}
|
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||||
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
|
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@media-on.de}
|
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@media-on.de}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD must be set — no default credentials (Astra F30)}
|
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}
|
STORAGE_PATH: ${STORAGE_PATH:-/data/storage}
|
||||||
SMTP_HOST: ${SMTP_HOST:-}
|
SMTP_HOST: ${SMTP_HOST:-}
|
||||||
SMTP_PORT: ${SMTP_PORT:-587}
|
SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
SMTP_USER: ${SMTP_USER:-}
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
SMTP_FROM: ${SMTP_FROM:-no-reply@localhost}
|
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-no-reply@localhost}
|
||||||
SMTP_TLS: ${SMTP_TLS:-true}
|
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||||
MAIL_ENCRYPTION_KEY: ${MAIL_ENCRYPTION_KEY:?MAIL_ENCRYPTION_KEY is required}
|
MAIL_ENCRYPTION_KEY: ${MAIL_ENCRYPTION_KEY:?MAIL_ENCRYPTION_KEY is required}
|
||||||
volumes:
|
volumes:
|
||||||
- storage:/data/storage
|
- storage:/data/storage
|
||||||
|
|||||||
@@ -767,66 +767,13 @@ class TestApproval:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_approve_approval_request(self, tenant_id, mock_db):
|
async def test_approve_approval_request(self, tenant_id, mock_db):
|
||||||
"""resolve_approval_request approves a pending request."""
|
"""Superseded by TestF11ApprovalBinding (real DB, deterministic)."""
|
||||||
request_id = uuid.uuid4()
|
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding with a real database")
|
||||||
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
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_reject_approval_request(self, tenant_id, mock_db):
|
async def test_reject_approval_request(self, tenant_id, mock_db):
|
||||||
"""resolve_approval_request rejects a pending request."""
|
"""Superseded by TestF11ApprovalBinding (real DB, deterministic)."""
|
||||||
request_id = uuid.uuid4()
|
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding with a real database")
|
||||||
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"
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_expire_approval_request(self, tenant_id, mock_db):
|
async def test_expire_approval_request(self, tenant_id, mock_db):
|
||||||
@@ -852,30 +799,8 @@ class TestApproval:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_resolve_non_pending_returns_none(self, tenant_id, mock_db):
|
async def test_resolve_non_pending_returns_none(self, tenant_id, mock_db):
|
||||||
"""Resolving a non-pending request returns None."""
|
"""Superseded: F11 changed the contract to ApprovalDecisionError(409)."""
|
||||||
request_id = uuid.uuid4()
|
pytest.skip("F11 contract covered by tests/test_s1_security_guards.py::TestF11ApprovalBinding (not_pending 409) with a real database")
|
||||||
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
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_approvals_with_filters(self, tenant_id, mock_db):
|
async def test_list_approvals_with_filters(self, tenant_id, mock_db):
|
||||||
|
|||||||
Reference in New Issue
Block a user