7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
167 lines
5.7 KiB
Python
167 lines
5.7 KiB
Python
"""Integration regression test: deleting a session terminates its research.
|
|
|
|
Previously, `ChatService.delete_session` issued a hard ``db.delete``
|
|
on the chat_sessions row. Any in-progress research linked to that session
|
|
had its ``chat_session_id`` set to NULL by the FK (ON DELETE SET NULL),
|
|
but the worker thread itself was left running — burning LLM cycles on a
|
|
conversation the user had already discarded.
|
|
|
|
The fix collapses the termination-flag set and the DELETE into one
|
|
transaction (chat/service.py:delete_session) so the worker is signalled
|
|
to terminate before the session row is removed.
|
|
|
|
This test exercises the full integration path:
|
|
|
|
1. Insert a chat session via ChatService.
|
|
2. Manually insert a ResearchHistory row tied to that session with
|
|
status="in_progress" (simulating a research that was just spawned).
|
|
3. Call ChatService.delete_session(...).
|
|
4. Assert that ``set_termination_flag`` was called with the in-flight
|
|
research id BEFORE the row was removed.
|
|
|
|
The test deliberately patches ``set_termination_flag`` so we can assert
|
|
on call args without needing a real worker thread.
|
|
"""
|
|
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
from src.local_deep_research.chat.service import 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, username: str, status: str):
|
|
"""Create a chat session + a research row linked to it in *status*."""
|
|
session_id = str(uuid.uuid4())
|
|
research_id = str(uuid.uuid4())
|
|
db.add(
|
|
ChatSession(
|
|
id=session_id,
|
|
title="terminate-on-delete 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 test_delete_session_flags_in_progress_research_for_termination(
|
|
setup_database_for_all_tests,
|
|
):
|
|
"""The single happy path: deleting a session with an in-flight
|
|
research must call ``set_termination_flag`` for that research id."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
username = f"alice_{uuid.uuid4().hex[:8]}"
|
|
|
|
with SessionLocal() as db:
|
|
session_id, research_id = _seed_session_and_research(
|
|
db, username, status=ResearchStatus.IN_PROGRESS.value
|
|
)
|
|
|
|
service = ChatService(username=username)
|
|
flagged = []
|
|
|
|
def _capture(rid):
|
|
flagged.append(rid)
|
|
|
|
# The service binds `set_termination_flag` at module top-level
|
|
# (``from ..web.routes.globals import set_termination_flag``), so
|
|
# patch at the binding location, not the source module — patching
|
|
# ``web.routes.globals`` would not intercept the call inside
|
|
# delete_session once the import has captured the reference.
|
|
with (
|
|
patch(
|
|
"src.local_deep_research.chat.service.set_termination_flag",
|
|
side_effect=_capture,
|
|
),
|
|
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
|
|
assert service.delete_session(session_id) is True
|
|
|
|
assert flagged == [research_id], (
|
|
f"Expected exactly one termination-flag for {research_id}, got {flagged}"
|
|
)
|
|
|
|
# And the session row itself must be gone.
|
|
with SessionLocal() as db:
|
|
assert db.query(ChatSession).filter_by(id=session_id).first() is None
|
|
|
|
|
|
def test_delete_session_skips_completed_research(setup_database_for_all_tests):
|
|
"""Completed research should NOT receive a termination flag — it has
|
|
nothing to abort and flagging it would be misleading log noise."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
username = f"alice_{uuid.uuid4().hex[:8]}"
|
|
|
|
with SessionLocal() as db:
|
|
session_id, _research_id = _seed_session_and_research(
|
|
db, username, status=ResearchStatus.COMPLETED.value
|
|
)
|
|
|
|
service = ChatService(username=username)
|
|
flagged = []
|
|
|
|
with (
|
|
patch(
|
|
"src.local_deep_research.chat.service.set_termination_flag",
|
|
side_effect=lambda rid: flagged.append(rid),
|
|
),
|
|
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
|
|
assert service.delete_session(session_id) is True
|
|
|
|
assert flagged == [], (
|
|
f"Completed research must not be flagged for termination, got {flagged}"
|
|
)
|
|
|
|
|
|
def test_delete_session_returns_false_for_missing_session(
|
|
setup_database_for_all_tests,
|
|
):
|
|
"""No-op + False return when the session doesn't exist — no
|
|
termination flag set, no exception."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
username = f"alice_{uuid.uuid4().hex[:8]}"
|
|
service = ChatService(username=username)
|
|
|
|
flagged = []
|
|
with (
|
|
patch(
|
|
"src.local_deep_research.chat.service.set_termination_flag",
|
|
side_effect=lambda rid: flagged.append(rid),
|
|
),
|
|
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
|
|
result = service.delete_session(str(uuid.uuid4()))
|
|
|
|
assert result is False
|
|
assert flagged == []
|