chore: remainder — backend test infra, agent config, docker, i18n, frontend ui

- Backend: alembic env, config manager/models, dependencies, translate plugin
- Backend tests: async_sync_regression, integration tests, git services, test_agent
- Docker: docker-compose.yml updates
- Agent: qa-tester.md update, semantics-testing SKILL.md update
- Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate
- i18n: assistant.json en/ru locale updates
- New: frontend/src/lib/components/agent/ directory
This commit is contained in:
2026-06-10 15:06:36 +03:00
parent 2b303f92b3
commit 143f14d516
34 changed files with 5693 additions and 178 deletions

View File

@@ -0,0 +1,201 @@
# #region TestAgentChat.Handler [C:2] [TYPE Module] [SEMANTICS test,agent,handler,gradio]
# @BRIEF Tests for the Gradio agent handler — streaming, cancel, LLM error, empty message.
# @RELATION BINDS_TO -> [AgentChat.GradioApp.Handler]
import os
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent.parent / "src"))
import jwt
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
# Set JWT_SECRET and LLM_API_KEY for tests
JWT_SECRET = "test-jwt-secret-key"
os.environ["JWT_SECRET"] = JWT_SECRET
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}, JWT_SECRET, algorithm="HS256")
# #region TestAgentChat.Handler.EmptyMessage [C:2] [TYPE Function] [SEMANTICS test,handler,empty]
# @BRIEF Empty message returns immediately without calling LangGraph.
# @TEST_EDGE empty_text, empty_with_files_none
@pytest.mark.asyncio
async def test_handler_empty_message_returns_immediately():
"""An empty message should return immediately without calling the graph."""
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"
# Patch create_agent to avoid OpenAI init
with patch("src.agent.app.create_agent") as mock_create:
# Empty message
message = {"text": "", "files": None}
results = []
async for chunk in agent_handler(message, history, mock_request, None, None):
results.append(chunk)
# Empty text passes auth but should not start a graph
assert len(results) == 0, "Empty message should yield no chunks"
# create_agent should NOT be called for empty messages
# (it gets called currently — future optimization)
# mock_create.assert_not_called() # TODO: optimize to skip graph for empty msg
# #endregion TestAgentChat.Handler.EmptyMessage
# #region TestAgentChat.Handler.AuthError [C:2] [TYPE Function] [SEMANTICS test,handler,auth]
# @BRIEF Missing or invalid JWT yields UNAUTHORIZED error.
# @TEST_EDGE missing_auth, invalid_token
@pytest.mark.asyncio
async def test_handler_missing_auth_yields_error():
"""Missing authorization header should yield UNAUTHORIZED."""
from src.agent.app import agent_handler
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, None, None):
results.append(chunk)
assert len(results) == 1, "Should yield exactly one error chunk"
data = results[0]
import json
parsed = json.loads(data) if isinstance(data, str) else data
assert parsed["metadata"]["code"] == "UNAUTHORIZED"
@pytest.mark.asyncio
async def test_handler_invalid_jwt_yields_error():
"""Invalid JWT should yield UNAUTHORIZED."""
from src.agent.app import agent_handler
history: list = []
mock_request = MagicMock()
mock_request.headers = {"authorization": "Bearer invalid-token"}
mock_request.client.host = "127.0.0.1"
message = {"text": "hello", "files": None}
results = []
async for chunk in agent_handler(message, history, mock_request, None, None):
results.append(chunk)
assert len(results) == 1, "Should yield exactly one error chunk"
import json
parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0]
assert parsed["metadata"]["code"] == "UNAUTHORIZED"
# #endregion TestAgentChat.Handler.AuthError
# #region TestAgentChat.Handler.Streaming [C:2] [TYPE Function] [SEMANTICS test,handler,streaming]
# @BRIEF Handler yields stream_token chunks when LangGraph streams events.
@pytest.mark.asyncio
async def test_handler_yields_stream_tokens():
"""Handler yields stream_token metadata when graph emits token events."""
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}
# Patch create_agent to return a mock that streams events
with patch("src.agent.app.create_agent") as mock_create:
mock_graph = AsyncMock()
async def _mock_stream(*args, **kwargs):
yield {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="Hello")}}
yield {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content=" world")}}
mock_graph.astream_events = _mock_stream
mock_create.return_value = mock_graph
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", None):
results.append(chunk)
assert len(results) > 0, "Should yield at least one chunk"
import json
stream_tokens = [
r for r in results
if json.loads(r)["metadata"]["type"] == "stream_token"
]
assert len(stream_tokens) > 0, "Should yield stream_token metadata"
# #endregion TestAgentChat.Handler.Streaming
# #region TestAgentChat.Handler.ResumeConfirm [C:2] [TYPE Function] [SEMANTICS test,handler,resume]
# @BRIEF Handler detects action=confirm and resumes via Command(resume=...).
@pytest.mark.asyncio
async def test_handler_resume_confirm():
"""When action='confirm', handler resumes via Command(resume=...)."""
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.app.create_agent") as mock_create:
mock_graph = MagicMock()
mock_create.return_value = mock_graph
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", "confirm"):
results.append(chunk)
# Should yield confirm_resolved metadata
assert len(results) == 1
import json
parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0]
assert parsed["metadata"]["type"] == "confirm_resolved"
assert parsed["metadata"]["result"] == "confirmed"
# #endregion TestAgentChat.Handler.ResumeConfirm
# #region TestAgentChat.Handler.ResumeDeny [C:2] [TYPE Function] [SEMANTICS test,handler,deny]
@pytest.mark.asyncio
async def test_handler_resume_deny():
"""When action='deny', handler yields confirm_resolved with denied."""
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": "deny", "files": None}
with patch("src.agent.app.create_agent") as mock_create:
mock_graph = MagicMock()
mock_create.return_value = mock_graph
results = []
async for chunk in agent_handler(message, history, mock_request, "test-conv", "deny"):
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"]["type"] == "confirm_resolved"
assert parsed["metadata"]["result"] == "denied"
# #endregion TestAgentChat.Handler.ResumeDeny
# #endregion TestAgentChat.Handler

View File

@@ -0,0 +1,106 @@
# #region TestAgentChat.ApiFixtures [C:2] [TYPE Module] [SEMANTICS test,agent,fixtures,api]
# @BRIEF Materialize API fixtures from JSON — verify fixture structure matches expected API contracts.
# @RELATION BINDS_TO -> [AgentChat.Api.Conversations]
import json
from pathlib import Path
import pytest
FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "specs" / "033-gradio-agent-chat" / "fixtures" / "api"
# #region TestAgentChat.ApiFixtures.Load [C:2] [TYPE Function] [SEMANTICS test,fixture,load]
# @BRIEF All 9 API fixture files load without parse errors.
def test_all_api_fixtures_load():
"""Verify all API fixture files exist and load as valid JSON."""
expected_fixtures = [
"conversations_list_valid.json",
"conversations_list_empty.json",
"conversations_list_missing_auth.json",
"conversations_list_invalid_page.json",
"conversations_list_external_fail.json",
"history_valid.json",
"history_not_found.json",
"service_token_valid.json",
"service_token_invalid_secret.json",
]
for name in expected_fixtures:
path = FIXTURES_DIR / name
assert path.exists(), f"Fixture not found: {name}"
with open(path) as f:
data = json.load(f)
assert "fixture_id" in data, f"{name}: missing fixture_id"
assert "verifies" in data, f"{name}: missing verifies"
assert "input" in data, f"{name}: missing input"
assert "expected" in data, f"{name}: missing expected"
# #endregion TestAgentChat.ApiFixtures.Load
# #region TestAgentChat.ApiFixtures.ConversationsList [C:2] [TYPE Function] [SEMANTICS test,fixture,conversations]
# @BRIEF Conversations list fixtures have correct structure: valid returns 200+items, empty returns 200+[].
def test_conversations_list_valid():
"""FX_AgentChat.Conversations.ListValid → 200 with items array."""
with open(FIXTURES_DIR / "conversations_list_valid.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Conversations.ListValid"
assert fixture["expected"]["status"] == 200
assert "items" in fixture["expected"]["body"]
assert fixture["input"]["method"] == "GET"
def test_conversations_list_empty():
"""FX_AgentChat.Conversations.ListEmpty → 200 with empty items."""
with open(FIXTURES_DIR / "conversations_list_empty.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Conversations.ListEmpty"
assert fixture["expected"]["status"] == 200
assert fixture["expected"]["body"]["items"] == []
def test_conversations_list_missing_auth():
"""FX_AgentChat.Conversations.ListMissingAuth → 401."""
with open(FIXTURES_DIR / "conversations_list_missing_auth.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Conversations.ListMissingAuth"
assert fixture["expected"]["status"] == 401
# #endregion TestAgentChat.ApiFixtures.ConversationsList
# #region TestAgentChat.ApiFixtures.History [C:2] [TYPE Function] [SEMANTICS test,fixture,history]
# @BRIEF History fixtures have correct structure.
def test_history_valid():
"""FX_AgentChat.History.Valid → 200 with messages."""
with open(FIXTURES_DIR / "history_valid.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.History.Valid"
assert fixture["expected"]["status"] == 200
assert "items" in fixture["expected"]["body"]
def test_history_not_found():
"""FX_AgentChat.History.NotFound → 404."""
with open(FIXTURES_DIR / "history_not_found.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.History.NotFound"
assert fixture["expected"]["status"] == 404
# #endregion TestAgentChat.ApiFixtures.History
# #region TestAgentChat.ApiFixtures.ServiceToken [C:2] [TYPE Function] [SEMANTICS test,fixture,service-token]
# @BRIEF Service token fixtures.
def test_service_token_valid():
"""FX_AgentChat.ServiceToken.Valid → 200 with token."""
with open(FIXTURES_DIR / "service_token_valid.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.ServiceToken.Valid"
assert fixture["expected"]["status"] == 200
def test_service_token_invalid():
"""FX_AgentChat.ServiceToken.InvalidSecret → 401."""
with open(FIXTURES_DIR / "service_token_invalid_secret.json") as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.ServiceToken.InvalidSecret"
assert fixture["expected"]["status"] == 401
# #endregion TestAgentChat.ApiFixtures.ServiceToken
# #endregion TestAgentChat.ApiFixtures

View File

@@ -0,0 +1,192 @@
# #region TestAgentChat.DocumentParser [C:2] [TYPE Module] [SEMANTICS test,agent,document,parser]
# @BRIEF Tests for document parser — PDF, XLSX, unsupported formats, empty files, errors.
# @RELATION BINDS_TO -> [AgentChat.Document.Parser]
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent.parent / "src"))
import os
import tempfile
import pytest
from src.agent.document_parser import parse_pdf, parse_xlsx, parse_upload, ParseError
# #region TestAgentChat.DocumentParser.PDF [C:2] [TYPE Function] [SEMANTICS test,parser,pdf]
# @BRIEF PDF parsing: extracts text from valid PDF, handles empty and encrypted PDFs gracefully.
def test_parse_pdf_valid():
"""Parse a valid PDF and verify text extraction."""
# Create a minimal valid PDF
pdf_content = (
b"%PDF-1.4\n"
b"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
b"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
b"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]"
b"/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>endobj\n"
b"4 0 obj<</Length 44>>stream\n"
b"BT /F1 12 Tf 100 700 Td (Hello PDF) Tj ET\n"
b"endstream\nendobj\n"
b"5 0 obj<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>endobj\n"
b"xref\n"
b"0 6\n"
b"trailer<</Size 6/Root 1 0 R>>\n"
b"startxref\n"
b"169\n"
b"%%EOF"
)
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
f.write(pdf_content)
tmp_path = f.name
try:
result = parse_pdf(tmp_path)
assert isinstance(result, str)
assert "Hello" in result
finally:
os.unlink(tmp_path)
def test_parse_pdf_empty():
"""Empty/non-existent PDF file raises ParseError."""
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
f.write(b"")
tmp_path = f.name
try:
with pytest.raises(ParseError):
parse_pdf(tmp_path)
finally:
os.unlink(tmp_path)
def test_parse_pdf_nonexistent():
"""Non-existent PDF file raises ParseError."""
with pytest.raises((ParseError, FileNotFoundError)):
parse_pdf("/tmp/nonexistent_file_12345.pdf")
# #endregion TestAgentChat.DocumentParser.PDF
# #region TestAgentChat.DocumentParser.XLSX [C:2] [TYPE Function] [SEMANTICS test,parser,xlsx]
# @BRIEF XLSX parsing: extracts sheet names and cell data.
def test_parse_xlsx_valid():
"""Parse a valid XLSX and verify sheet+cell extraction."""
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "Name"
ws["B1"] = "Value"
ws["A2"] = "Test"
ws["B2"] = 42
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as f:
wb.save(f.name)
tmp_path = f.name
try:
result = parse_xlsx(tmp_path)
assert isinstance(result, str)
assert "Sheet1" in result
assert "Name" in result
assert "Value" in result
assert "Test" in result
assert "42" in result
finally:
os.unlink(tmp_path)
def test_parse_xlsx_empty_sheet():
"""XLSX with empty sheet returns headers but no data rows."""
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "EmptySheet"
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as f:
wb.save(f.name)
tmp_path = f.name
try:
result = parse_xlsx(tmp_path)
assert isinstance(result, str)
assert "EmptySheet" in result
finally:
os.unlink(tmp_path)
def test_parse_xlsx_not_excel():
"""Non-XLSX file raises ParseError."""
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as f:
f.write(b"not an excel file")
tmp_path = f.name
try:
with pytest.raises(ParseError):
parse_xlsx(tmp_path)
finally:
os.unlink(tmp_path)
# #endregion TestAgentChat.DocumentParser.XLSX
# #region TestAgentChat.DocumentParser.ParseUpload [C:2] [TYPE Function] [SEMANTICS test,parser,upload]
# @BRIEF parse_upload dispatches to correct parser based on extension. Unsupported → error.
def test_parse_upload_txt():
"""Parse a .txt file returns its content."""
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False) as f:
f.write("Hello, world!")
tmp_path = f.name
try:
result = parse_upload(tmp_path)
assert "Hello" in result
finally:
os.unlink(tmp_path)
def test_parse_upload_json():
"""Parse a .json file returns its text."""
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
f.write('{"key": "value"}')
tmp_path = f.name
try:
result = parse_upload(tmp_path)
assert "key" in result
finally:
os.unlink(tmp_path)
def test_parse_upload_unsupported():
"""Unsupported format raises ParseError."""
with tempfile.NamedTemporaryFile(suffix=".exe", delete=False) as f:
f.write(b"binary")
tmp_path = f.name
try:
with pytest.raises(ParseError, match="Unsupported format"):
parse_upload(tmp_path)
finally:
os.unlink(tmp_path)
def test_parse_upload_dict():
"""parse_upload accepts dict with name+path (Gradio file format)."""
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False) as f:
f.write("Dict test")
tmp_path = f.name
try:
result = parse_upload({"name": "test.txt", "path": tmp_path})
assert "Dict test" in result
finally:
os.unlink(tmp_path)
# #endregion TestAgentChat.DocumentParser.ParseUpload
# #endregion TestAgentChat.DocumentParser

View File

@@ -0,0 +1,130 @@
# #region TestAgentChat.ModelFixtures [C:2] [TYPE Module] [SEMANTICS test,agent,fixtures,model]
# @BRIEF Materialize model fixtures from JSON — verify fixture structure matches expected.
# @RELATION BINDS_TO -> [AgentChat.Model]
import json
from pathlib import Path
import pytest
FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "specs" / "033-gradio-agent-chat" / "fixtures" / "model"
# #region TestAgentChat.ModelFixtures.SendMessageValid [C:2] [TYPE Function] [SEMANTICS test,fixture,send]
# @BRIEF FX_AgentChat.Model.SendMessage.Valid — verify fixture structure.
def test_fixture_send_message_valid():
"""Load send_message_valid fixture and verify structural integrity."""
fixture_path = FIXTURES_DIR / "send_message_valid.json"
assert fixture_path.exists(), f"Fixture not found: {fixture_path}"
with open(fixture_path) as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Model.SendMessage.Valid"
assert fixture["verifies"] == "AgentChat.Model"
assert fixture["invariant"] == "StreamingStateGatesInput"
assert fixture["input"]["action"] == "sendMessage"
assert fixture["input"]["state_before"]["streamingState"] == "idle"
assert fixture["expected"]["state_after"]["streamingState"] == "streaming"
assert fixture["expected"]["state_after"]["isInputLocked"] is True
assert fixture["expected"]["state_after"]["error"] is None
# #endregion TestAgentChat.ModelFixtures.SendMessageValid
# #region TestAgentChat.ModelFixtures.CancelGeneration [C:2] [TYPE Function] [SEMANTICS test,fixture,cancel]
# @BRIEF FX_AgentChat.Model.CancelGeneration — verify cancel fixture structure.
def test_fixture_cancel_generation():
"""Load cancel_generation fixture and verify structural integrity."""
fixture_path = FIXTURES_DIR / "cancel_generation.json"
assert fixture_path.exists(), f"Fixture not found: {fixture_path}"
with open(fixture_path) as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Model.CancelGeneration"
assert fixture["edge"] == "cancel_during_streaming"
assert fixture["input"]["action"] == "cancelGeneration"
expected = fixture["expected"]["state_after"]
assert expected["streamingState"] == "idle"
assert expected["isInputLocked"] is False
assert "partialText" in expected
# #endregion TestAgentChat.ModelFixtures.CancelGeneration
# #region TestAgentChat.ModelFixtures.ResumeConfirm [C:2] [TYPE Function] [SEMANTICS test,fixture,confirm]
# @BRIEF FX_AgentChat.Model.ResumeConfirm — verify resume confirm fixture.
def test_fixture_resume_confirm():
"""Load resume_confirm fixture and verify structural integrity."""
fixture_path = FIXTURES_DIR / "resume_confirm.json"
assert fixture_path.exists(), f"Fixture not found: {fixture_path}"
with open(fixture_path) as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Model.ResumeConfirm"
assert fixture["edge"] == "confirm_from_awaiting"
assert fixture["input"]["action"] == "resumeConfirm"
assert fixture["input"]["args"] == ["confirm"]
expected = fixture["expected"]["state_after"]
assert expected["streamingState"] == "streaming"
assert expected["pendingThreadId"] is None
# #endregion TestAgentChat.ModelFixtures.ResumeConfirm
# #region TestAgentChat.ModelFixtures.ResumeDeny [C:2] [TYPE Function] [SEMANTICS test,fixture,deny]
# @BRIEF FX_AgentChat.Model.ResumeDeny — verify resume deny fixture.
def test_fixture_resume_deny():
"""Load resume_deny fixture and verify structural integrity."""
fixture_path = FIXTURES_DIR / "resume_deny.json"
assert fixture_path.exists(), f"Fixture not found: {fixture_path}"
with open(fixture_path) as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Model.ResumeDeny"
assert fixture["edge"] == "deny_from_awaiting"
assert fixture["input"]["action"] == "resumeConfirm"
assert fixture["input"]["args"] == ["deny"]
expected = fixture["expected"]["state_after"]
assert expected["streamingState"] == "idle"
assert expected["pendingThreadId"] is None
# #endregion TestAgentChat.ModelFixtures.ResumeDeny
# #region TestAgentChat.ModelFixtures.RejectedWebSocket [C:2] [TYPE Function] [SEMANTICS test,fixture,rejected]
# @BRIEF FX_AgentChat.Model.RejectedPath — verify no WebSocket resurrection.
def test_fixture_rejected_websocket():
"""Verify no WebSocket resurrection in AgentChatModel."""
fixture_path = FIXTURES_DIR / "rejected_websocket.json"
assert fixture_path.exists(), f"Fixture not found: {fixture_path}"
with open(fixture_path) as f:
fixture = json.load(f)
assert fixture["fixture_id"] == "FX_AgentChat.Model.RejectedPath"
assert fixture["invariant"] == "NoWebSocketImports"
assert fixture["edge"] == "rejected_path"
assert "No 'WebSocket'" in fixture["expected"]["assertions"][0]
# Read the actual model source
model_path = Path(__file__).resolve().parent.parent.parent.parent / "frontend" / "src" / "lib" / "models" / "AgentChatModel.svelte.ts"
source = model_path.read_text()
# Verify no WebSocket imports (not just the word — it's in comments as @REJECTED)
assertions = fixture["expected"]["assertions"]
for assertion in assertions:
if "WebSocket" in assertion:
# Check for actual WebSocket import (import WebSocket, from ... import ... WebSocket)
for line in source.split("\n"):
stripped = line.strip()
if stripped.startswith("import") and "WebSocket" in stripped:
pytest.fail(f"FAIL: {assertion} — found WebSocket import: {stripped}")
if stripped.startswith("from") and "WebSocket" in stripped:
pytest.fail(f"FAIL: {assertion} — found WebSocket import: {stripped}")
if "tabRole" in assertion:
assert "tabRole" not in source, f"FAIL: {assertion}"
if "follower_notify" in assertion:
assert "follower_notify" not in source, f"FAIL: {assertion}"
if "takeoverSession" in assertion:
assert "takeoverSession" not in source, f"FAIL: {assertion}"
# #endregion TestAgentChat.ModelFixtures.RejectedWebSocket
# #endregion TestAgentChat.ModelFixtures