"""Regression tests for task-result delivery into chat sessions (issue #326).""" import asyncio import sys import types as _types import pytest sqlalchemy = pytest.importorskip("sqlalchemy") if not isinstance(sqlalchemy, _types.ModuleType): pytest.skip("sqlalchemy is stubbed in this environment", allow_module_level=True) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from tests.helpers.import_state import clear_fake_database_modules clear_fake_database_modules() import core.database as cdb from core.database import Base, Session as DbSession from core.models import ChatMessage as MemChatMessage from src.task_scheduler import TaskScheduler # This test needs the real core.database (real SQLAlchemy Base/ChatMessage). # test_null_owner_gates.py no longer leaks its stubs (per-test fixture cleanup # since PR #1513), but several other files still install core.database stubs # at module level without teardown (test_model_routes, test_companion_readonly, # test_endpoint_probing, test_vault_password_not_in_argv). When any of those # are collected before us, core.database is a stub and Base is a MagicMock. # Skip in that case — the test passes correctly in isolation or when collected # before the stubbing files. if type(Base).__name__ == "MagicMock": pytest.skip("core.database is stubbed — run this file in isolation", allow_module_level=True) def _make_db(): engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) return sessionmaker(bind=engine)() def _make_task(): return _types.SimpleNamespace( id="task-1", name="Chat Sessions Tidy", prompt="tidy", output_target="session", endpoint_url=None, model=None, session_id=None, owner=None, crew_member_id=None, ) def test_session_delivery_survives_empty_database(monkeypatch): """On a fresh/wiped database there is no session to inherit endpoint/model from, so _resolve_defaults returns None. The delivery must still persist a session instead of crashing on the NOT NULL constraint (issue #326).""" monkeypatch.setitem(sys.modules, "core.database", cdb) parent = sys.modules.get("core") if parent is not None: monkeypatch.setattr(parent, "database", cdb, raising=False) db = _make_db() scheduler = TaskScheduler.__new__(TaskScheduler) scheduler._session_manager = None asyncio.run(scheduler._deliver_task_result(_make_task(), "done", db)) sessions = db.query(DbSession).all() assert len(sessions) == 1 assert sessions[0].endpoint_url == "" assert sessions[0].model == "" def test_session_delivery_uses_in_memory_messages_with_manager(monkeypatch): """Manager delivery must not construct the SQLAlchemy ChatMessage model.""" monkeypatch.setitem(sys.modules, "core.database", cdb) parent = sys.modules.get("core") if parent is not None: monkeypatch.setattr(parent, "database", cdb, raising=False) class RecordingManager: def __init__(self): self.messages = [] def add_message(self, session_id, message): assert isinstance(message, MemChatMessage) self.messages.append((session_id, message)) db = _make_db() manager = RecordingManager() scheduler = TaskScheduler.__new__(TaskScheduler) scheduler._session_manager = manager task = _make_task() task.session_id = "existing-session" task.endpoint_url = "http://endpoint" task.model = "test-model" asyncio.run(scheduler._deliver_task_result(task, "done", db)) assert [message.role for _, message in manager.messages] == [ "user", "assistant", ] assert [message.content for _, message in manager.messages] == [ "tidy", "done", ] assert all(session_id == "existing-session" for session_id, _ in manager.messages) assert all( message.metadata == {"model": "test-model"} for _, message in manager.messages )