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
155 lines
5.0 KiB
Python
155 lines
5.0 KiB
Python
"""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
|