fix(integration): F41+F37 (Astra S2) — Agenten-Stundenlimit und SMTP-Env-Namen
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:
Agent Zero
2026-09-18 11:15:45 +02:00
parent 8c5682f669
commit 5d6fe6b1f6
5 changed files with 20 additions and 95 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -81
View File
@@ -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):