Files
ss-tools/backend/tests/test_agent/test_confirmations.py
busya 48e3ff4503 refactor(env): unify Docker env vars — canonical AUTH_SECRET_KEY, remove JWT_SECRET fallback
Canonical variables:
  - AUTH_SECRET_KEY — JWT signing key for backend + agent (was split across
    AUTH_SECRET_KEY / JWT_SECRET)
  - SERVICE_JWT — agent→backend service token
  - No JWT_SECRET fallback: decoder fails with migration guidance if only
    JWT_SECRET is set

Compose files unified:
  - docker-compose.yml, docker-compose.enterprise-clean.yml,
    docker-compose.e2e.yml — all use AUTH_SECRET_KEY
  - build.sh generated compose now passes AUTH_SECRET_KEY + ENCRYPTION_KEY
    to backend

Env examples unified and completed:
  - .env.example — comprehensive template, all compose vars
  - .env.enterprise-clean.example — production template
  - backend/.env.example — backend-only run
  - docker/.env.agent.example — agent-only run
  - NEW: .env.current.example, .env.master.example,
    .env.e2e.example, frontend/.env.example

Tests aligned:
  - conftest sets AUTH_SECRET_KEY (canonical value matched across test files)
  - test mocks use canonical name
  - 1176 passed, 0 failed
2026-07-06 14:24:17 +03:00

149 lines
5.2 KiB
Python

# #region TestAgentChat.Confirmations [C:3] [TYPE Module] [SEMANTICS test,agent,confirmation,hitl]
# @BRIEF Supplementary HITL confirmation flow tests — concurrent send, unknown action, model edge cases.
# @RELATION BINDS_TO -> [AgentChat.GradioApp.Handler]
# @TEST_EDGE: concurrent_send -> handler yields CONCURRENT_SEND error
# @TEST_EDGE: confirm_without_conversation_id -> handler still processes confirm
# @NOTE Resume confirm/deny handler tests are in test_agent_handler.py (test_handler_resume_confirm,
# test_handler_resume_deny). Model-level state machine tests are in AgentChatModel.test.ts.
import os
from pathlib import Path
import sys
from unittest.mock import AsyncMock, MagicMock, patch
sys.path.append(str(Path(__file__).parent.parent.parent / "src"))
import pytest
import jwt
os.environ.setdefault("AUTH_SECRET_KEY", "test-secret-key-for-jwt-testing")
AUTH_SECRET_KEY = os.environ["AUTH_SECRET_KEY"]
os.environ["OPENAI_API_KEY"] = "sk-test-key"
os.environ["LLM_API_KEY"] = "sk-test-key"
os.environ["LLM_MODEL"] = "gpt-4o"
os.environ["LLM_BASE_URL"] = "https://api.openai.com/v1"
def _make_test_jwt(user_id: str = "test-user") -> str:
return jwt.encode({"sub": user_id}, AUTH_SECRET_KEY, algorithm="HS256")
@pytest.fixture(autouse=True)
def mock_save_conversation():
with patch("src.agent.app.save_conversation", new_callable=AsyncMock):
yield
# #region TestAgentChat.Confirmations.Concurrent [C:2] [TYPE Function] [SEMANTICS test,confirmation,concurrent]
# @BRIEF Concurrent send lock prevents multiple simultaneous sends from same user.
@pytest.mark.asyncio
async def test_concurrent_send_lock():
"""Handler rejects concurrent sends from same user with CONCURRENT_SEND error."""
from src.agent.app import _user_locks, agent_handler
# Handler resolves user_id as "admin" when no JWT and no user_id_str are provided
_user_locks["admin"] = True
history: list = []
mock_request = MagicMock()
mock_request.headers = {}
mock_request.client.host = "127.0.0.1"
message = {"text": "hello", "files": None}
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", None):
results.append(chunk)
assert len(results) == 1
import json
parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0]
assert parsed["metadata"]["code"] == "CONCURRENT_SEND"
# Clean up
_user_locks.pop("admin", None)
# #endregion TestAgentChat.Confirmations.Concurrent
# #region TestAgentChat.Confirmations.UnknownAction [C:2] [TYPE Function] [SEMANTICS test,confirmation,unknown]
# @BRIEF Non-confirm/deny action values are treated as normal messages.
@pytest.mark.asyncio
async def test_handler_unknown_action_treated_as_normal():
"""Handler with unknown action string proceeds as normal message send."""
from src.agent.app import agent_handler
history: list = []
mock_request = MagicMock()
token = _make_test_jwt()
mock_request.headers = {"authorization": f"Bearer {token}"}
mock_request.client.host = "127.0.0.1"
message = {"text": "hello", "files": None}
with patch("src.agent.app.create_agent") as mock_create:
mock_graph = MagicMock()
async def _mock_stream(*_args, **_kwargs):
yield {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="Hello")}}
mock_graph.astream_events = _mock_stream
state = MagicMock(spec_set=["next"])
state.next = ()
mock_graph.aget_state = AsyncMock(return_value=state)
mock_create.return_value = mock_graph
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", "unknown_action"):
results.append(chunk)
# Should produce stream tokens, not confirm_resolved
assert len(results) > 0
import json
meta_types = [json.loads(r)["metadata"]["type"] for r in results]
assert "stream_token" in meta_types
assert "confirm_resolved" not in meta_types
# #endregion TestAgentChat.Confirmations.UnknownAction
# #region TestAgentChat.Confirmations.ExpiredState [C:2] [TYPE Function] [SEMANTICS test,confirmation,expired]
# @BRIEF Stale confirmations — checkpoint no longer available.
@pytest.mark.asyncio
async def test_handler_confirm_no_graph():
"""Handler with action='confirm' but create_agent raises handles it gracefully."""
from src.agent.app import agent_handler
history: list = []
mock_request = MagicMock()
token = _make_test_jwt()
mock_request.headers = {"authorization": f"Bearer {token}"}
mock_request.client.host = "127.0.0.1"
message = {"text": "confirm", "files": None}
with patch("src.agent._confirmation.create_agent") as mock_create:
mock_create.side_effect = Exception("Graph creation failed")
try:
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", "confirm"):
results.append(chunk)
# Exception may propagate or be caught — either is acceptable
except Exception:
pass
# #endregion TestAgentChat.Confirmations.ExpiredState
# #endregion TestAgentChat.Confirmations