Files
ss-tools/backend/tests/test_agent/test_langgraph_setup.py
busya 12678c637b fix(agent-chat): streaming state leak, document parser magic bytes, HITL flow
### Bugfixes — Agent Chat 'Думаю' State Leak
- fix(agent-chat): loadHistory() now resets streamingState/idle + cancels stale
  submission — prevents 'Думаю' state leak across conversation switches
- fix(agent-chat): onDisconnected/onDisconnectedPermanent cascade to
  streamingState — prevents permanent hang on connection loss during stream
- fix(agent-chat):  guard on isLoadingHistory — prevents false commit
  of 'agent unavailable' fallback when switching conversations
- fix(agent-chat): remove race in _sendNow empty-response check vs Svelte
   microtask (duplicate logic removed,  handles correctly)
- fix(stream-processor): confirm_resolved now appends msg.text to partialText
  instead of dropping it

### Bugfixes — Backend PDF Upload
- fix(document-parser): _detect_format_by_magic() — reads file header magic
  bytes as fallback when Gradio loses filename
- fix(document-parser): improved name extraction — tries orig_name, path stem
- fix(document-parser): @RELATION AgentChatTypes -> AgentChat.Types

### HITL Flow & Agent Chat Improvements
- feat(agent): HITL resume confirm/deny with userId/userJwt/envId propagation
- feat(agent): confirm_required metadata fallback via aget_state() after
  'Event loop is closed' error during interrupt
- feat(agent): interrupt_before re-enabled via AGENT_CONFIRM_TOOLS env var
- feat(frontend): debug panel with connection/stream state monitoring
- feat(frontend): AgentChatModel constructor options + onBeforeSend callback
- feat(frontend): crypto.randomUUID() for local conversation ID on first send

### Backend Agent Refactoring
- refactor(agent): langgraph_setup — monkey-patch for PydanticSerializationError
- refactor(agent): tools.py — dual identity headers, expanded tool set
- refactor(agent): run.py — _find_free_port, Gradio server port fallback
- refactor(agent): app.py — file size validation, message truncation, HITL path

### Frontend
- feat(dashboard-hub): DashboardHubModel with filters, pagination, git actions
- feat(ui): DateRangeFilter component
- feat(i18n): new dashboard keys; cache tooltips fix
- fix(i18n): full run tooltips — cache is NOT ignored

### Semantic Protocol
- chore(agents): update all agents with canonical format
- chore(skills): sync semantics-core, semantics-contracts, molecular-cot-logging

### Housekeeping
- chore: remove stale semantic reports (10 files, Jan 2026)
- chore: update 033-gradio-agent-chat specs, contracts, UX, tasks, tests
- chore: add .agents/ directory (mirrors .opencode/ agent layouts)
- chore: update run.sh with DEV_MODE, port management
2026-06-29 17:15:25 +03:00

185 lines
8.5 KiB
Python

# #region Test.AgentChat.LangGraph.Setup [C:3] [TYPE Module] [SEMANTICS test,agent,langgraph,setup]
# @BRIEF Tests for agent/langgraph_setup.py — configure_from_api, create_agent.
# @RELATION BINDS_TO -> [AgentChat.LangGraph.Setup]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@pytest.fixture
def anyio_backend():
return "asyncio"
# #region test_configure_from_api [C:2] [TYPE Function]
# @BRIEF Test configure_from_api updates global config.
class TestConfigureFromApi:
def test_sets_llm_config(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None # Reset
ls.configure_from_api({"configured": True, "api_key": "sk-test", "default_model": "gpt-4o"})
assert ls._llm_config is not None
assert ls._llm_config["configured"] is True
# Reset for other tests
ls.configure_from_api(None)
ls._llm_config = None
def test_overwrites_previous_config(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None # Reset
ls.configure_from_api({"configured": True, "api_key": "sk-1"})
ls.configure_from_api({"configured": False})
assert ls._llm_config["configured"] is False
ls._llm_config = None
# #endregion test_configure_from_api
# #region test_create_agent [C:2] [TYPE Function]
# @BRIEF Test create_agent with various LLM config states.
class TestCreateAgent:
@pytest.mark.anyio
async def test_creates_agent_with_api_config(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None # Reset
ls.configure_from_api({
"configured": True,
"api_key": "sk-api-config",
"base_url": "https://custom.api.com/v1",
"default_model": "gpt-4o-mini",
})
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)):
mock_create.return_value = MagicMock()
result = await ls.create_agent([MagicMock()])
assert result is mock_create.return_value
call_kwargs = mock_llm.call_args[1]
assert call_kwargs["api_key"] == "sk-api-config"
assert call_kwargs["base_url"] == "https://custom.api.com/v1"
assert call_kwargs["model"] == "gpt-4o-mini"
ls._llm_config = None
@pytest.mark.anyio
async def test_creates_agent_with_env_fallback(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None # Reset
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"LLM_API_KEY": "sk-env-key",
"LLM_BASE_URL": "https://env.api.com",
"LLM_MODEL": "gpt-4",
}.get(key, default)
mock_create.return_value = MagicMock()
result = await ls.create_agent([])
assert result is mock_create.return_value
call_kwargs = mock_llm.call_args[1]
assert call_kwargs["api_key"] == "sk-env-key"
assert call_kwargs["model"] == "gpt-4"
ls._llm_config = None
@pytest.mark.anyio
async def test_creates_agent_with_partial_api_config(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
ls.configure_from_api({
"configured": True,
"api_key": "sk-key-only",
})
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=ls._llm_config)):
mock_create.return_value = MagicMock()
result = await ls.create_agent([])
assert result is mock_create.return_value
call_kwargs = mock_llm.call_args[1]
assert call_kwargs["api_key"] == "sk-key-only"
assert call_kwargs["base_url"] == "https://api.openai.com/v1"
assert call_kwargs["model"] == "gpt-4o-mini"
ls._llm_config = None
@pytest.mark.anyio
async def test_uses_inmemory_saver(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv", return_value=None):
mock_create.return_value = MagicMock()
await ls.create_agent([])
call_kwargs = mock_create.call_args[1]
from langgraph.checkpoint.memory import InMemorySaver
assert isinstance(call_kwargs["checkpointer"], InMemorySaver)
ls._llm_config = None
@pytest.mark.anyio
async def test_uses_empty_interrupt_list_by_default(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv", return_value=None):
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == []
ls._llm_config = None
@pytest.mark.anyio
async def test_confirm_tools_env_interrupts_before_tools_node(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_CONFIRM_TOOLS": "true",
}.get(key, default)
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == ["tools"]
ls._llm_config = None
@pytest.mark.anyio
async def test_uses_env_configured_interrupt_nodes(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_INTERRUPT_BEFORE": "tools",
}.get(key, default)
mock_create.return_value = MagicMock()
await ls.create_agent([])
assert mock_create.call_args[1]["interrupt_before"] == ["tools"]
ls._llm_config = None
@pytest.mark.anyio
async def test_interrupt_override_bypasses_env_guardrail(self):
import src.agent.langgraph_setup as ls
ls._llm_config = None
with patch("src.agent.langgraph_setup.ChatOpenAI"), \
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
patch("src.agent.langgraph_setup._fetch_llm_config", new=AsyncMock(return_value=None)), \
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
mock_getenv.side_effect = lambda key, default=None: {
"AGENT_CONFIRM_TOOLS": "true",
}.get(key, default)
mock_create.return_value = MagicMock()
await ls.create_agent([], interrupt_before=[])
assert mock_create.call_args[1]["interrupt_before"] == []
ls._llm_config = None
# #endregion test_create_agent
# #endregion Test.AgentChat.LangGraph.Setup