"""Integration regression test for the archive-while-running guard. `ChatService.archive_session` must refuse to archive a chat session that has a `ResearchHistory` row in `status='in_progress'` for it. Without this guard, archiving leaves the running research alive but tied to a session the user thinks is read-only — an orphaned research that keeps burning LLM cycles and would race with the archived-session read-only invariant when it tries to write its final result back. The chosen behaviour mirrors the existing send-to-archived 409 rule (see ``insert_message_in_db`` and the send-to-archived guard in ``chat/routes.py``): rather than auto-terminating the research (which delete_session does — but delete is destructive by design), archive returns 409 and asks the caller to stop the research first. This preserves "archive = read-only" without silent side effects on running computation. """ import uuid from unittest.mock import patch import pytest from src.local_deep_research.chat.service import ( ArchiveBlockedError, ChatService, ) from src.local_deep_research.constants import ResearchStatus from src.local_deep_research.database.models import ( ChatSession, ChatSessionStatus, ResearchHistory, ) def _seed_session_and_research(db, status: str): """Insert an active chat session + a research row in *status*.""" session_id = str(uuid.uuid4()) research_id = str(uuid.uuid4()) db.add( ChatSession( id=session_id, title="archive-guard test", status=ChatSessionStatus.ACTIVE.value, message_count=0, ) ) db.add( ResearchHistory( id=research_id, query="probe", mode="quick", status=status, created_at="2026-05-14T00:00:00", chat_session_id=session_id, ) ) db.commit() return session_id, research_id def _run_archive(SessionLocal, service, session_id): """Drive ``archive_session`` against the test SessionLocal.""" with patch( "src.local_deep_research.chat.service.get_user_db_session" ) as ctx: ctx.return_value.__enter__.return_value = SessionLocal() ctx.return_value.__exit__.return_value = False return service.archive_session(session_id) def test_archive_blocked_when_research_in_progress( setup_database_for_all_tests, ): """An active session with an in_progress research must NOT archive.""" SessionLocal = setup_database_for_all_tests username = f"alice_{uuid.uuid4().hex[:8]}" service = ChatService(username=username) with SessionLocal() as db: session_id, _ = _seed_session_and_research( db, status=ResearchStatus.IN_PROGRESS.value ) with pytest.raises(ArchiveBlockedError, match="in_progress"): _run_archive(SessionLocal, service, session_id) # And the session must still be ACTIVE — the failed archive must # not have partially-committed a status change. with SessionLocal() as db: row = db.query(ChatSession).filter_by(id=session_id).first() assert row is not None assert row.status == ChatSessionStatus.ACTIVE.value def test_archive_succeeds_when_research_completed( setup_database_for_all_tests, ): """Completed research must not block archive — the guard is in_progress-only.""" SessionLocal = setup_database_for_all_tests username = f"alice_{uuid.uuid4().hex[:8]}" service = ChatService(username=username) with SessionLocal() as db: session_id, _ = _seed_session_and_research( db, status=ResearchStatus.COMPLETED.value ) assert _run_archive(SessionLocal, service, session_id) is True with SessionLocal() as db: row = db.query(ChatSession).filter_by(id=session_id).first() assert row.status == ChatSessionStatus.ARCHIVED.value def test_archive_succeeds_when_no_research_attached( setup_database_for_all_tests, ): """A session with zero research rows archives cleanly (sanity).""" SessionLocal = setup_database_for_all_tests username = f"alice_{uuid.uuid4().hex[:8]}" service = ChatService(username=username) sid = str(uuid.uuid4()) with SessionLocal() as db: db.add( ChatSession( id=sid, title="clean archive", status=ChatSessionStatus.ACTIVE.value, message_count=0, ) ) db.commit() assert _run_archive(SessionLocal, service, sid) is True with SessionLocal() as db: assert ( db.query(ChatSession).filter_by(id=sid).first().status == ChatSessionStatus.ARCHIVED.value ) def test_archive_missing_session_returns_false( setup_database_for_all_tests, ): """Unknown id → False return, no exception.""" SessionLocal = setup_database_for_all_tests username = f"alice_{uuid.uuid4().hex[:8]}" service = ChatService(username=username) assert _run_archive(SessionLocal, service, str(uuid.uuid4())) is False