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
181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
"""Direct integration tests for ``ChatService.add_progress_step``.
|
|
|
|
This method previously had only indirect coverage via the migration /
|
|
cascade tests; this file adds direct coverage.
|
|
|
|
The method is the persistence half of the step-emit/persist symmetry
|
|
that the chat-mode research path in research_service.py maintains. Its
|
|
contract:
|
|
|
|
- ``content`` is required (``ValueError`` on None).
|
|
- Sequence number is allocated atomically by ``UPDATE...RETURNING`` on
|
|
``ResearchHistory.step_count``.
|
|
- If the research row doesn't exist, ``ValueError("Research ... not
|
|
found")`` is raised and no ``chat_progress_steps`` row is written.
|
|
- Multiple steps for the same research get strictly increasing sequence
|
|
numbers (1, 2, 3, ...).
|
|
- The denormalised ``session_id`` column on ``chat_progress_steps``
|
|
mirrors the chat session the research was launched from.
|
|
- ``phase`` is optional and stored verbatim.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from src.local_deep_research.chat.service import ChatService
|
|
from src.local_deep_research.database.models import (
|
|
ChatProgressStep,
|
|
ChatSession,
|
|
ChatSessionStatus,
|
|
ResearchHistory,
|
|
)
|
|
|
|
|
|
def _seed(db):
|
|
"""Insert a session + research row, return (session_id, research_id)."""
|
|
sid = str(uuid.uuid4())
|
|
rid = str(uuid.uuid4())
|
|
db.add(
|
|
ChatSession(
|
|
id=sid,
|
|
title="step-test",
|
|
status=ChatSessionStatus.ACTIVE.value,
|
|
message_count=0,
|
|
)
|
|
)
|
|
db.add(
|
|
ResearchHistory(
|
|
id=rid,
|
|
query="probe",
|
|
mode="quick",
|
|
status="in_progress",
|
|
created_at="2026-05-14T00:00:00",
|
|
chat_session_id=sid,
|
|
)
|
|
)
|
|
db.commit()
|
|
return sid, rid
|
|
|
|
|
|
def _patch_user_db(monkeypatch, SessionLocal):
|
|
"""Patch get_user_db_session inside chat.service to yield our test DB."""
|
|
from contextlib import contextmanager
|
|
|
|
@contextmanager
|
|
def _ctx(_username, password=None):
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
monkeypatch.setattr(
|
|
"src.local_deep_research.chat.service.get_user_db_session", _ctx
|
|
)
|
|
|
|
|
|
def test_rejects_none_content(setup_database_for_all_tests, monkeypatch):
|
|
"""Content cannot be None — the row would violate NOT NULL."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
_patch_user_db(monkeypatch, SessionLocal)
|
|
|
|
with SessionLocal() as db:
|
|
sid, rid = _seed(db)
|
|
|
|
service = ChatService(username="alice")
|
|
with pytest.raises(ValueError, match="content is required"):
|
|
service.add_progress_step(session_id=sid, research_id=rid, content=None)
|
|
|
|
|
|
def test_rejects_unknown_research(setup_database_for_all_tests, monkeypatch):
|
|
"""The atomic UPDATE matches zero rows when the research doesn't
|
|
exist, so add_progress_step raises and writes nothing."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
_patch_user_db(monkeypatch, SessionLocal)
|
|
|
|
service = ChatService(username="alice")
|
|
with pytest.raises(ValueError, match="Research .* not found"):
|
|
service.add_progress_step(
|
|
session_id=str(uuid.uuid4()),
|
|
research_id=str(uuid.uuid4()),
|
|
content="step content",
|
|
)
|
|
|
|
|
|
def test_persists_row_and_returns_id(setup_database_for_all_tests, monkeypatch):
|
|
"""A successful call persists a row in chat_progress_steps and
|
|
returns the new step's UUID."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
_patch_user_db(monkeypatch, SessionLocal)
|
|
|
|
with SessionLocal() as db:
|
|
sid, rid = _seed(db)
|
|
|
|
service = ChatService(username="alice")
|
|
step_id = service.add_progress_step(
|
|
session_id=sid,
|
|
research_id=rid,
|
|
content="Searching for topic X",
|
|
phase="search",
|
|
)
|
|
assert isinstance(step_id, str) and len(step_id) >= 32
|
|
|
|
with SessionLocal() as db:
|
|
row = db.query(ChatProgressStep).filter_by(id=step_id).first()
|
|
assert row is not None
|
|
assert row.session_id == sid
|
|
assert row.research_id == rid
|
|
assert row.content == "Searching for topic X"
|
|
assert row.phase == "search"
|
|
assert row.sequence_number == 1
|
|
|
|
|
|
def test_sequence_increments_monotonically(
|
|
setup_database_for_all_tests, monkeypatch
|
|
):
|
|
"""Per-research sequence is allocated atomically via
|
|
``UPDATE step_count RETURNING``. Three consecutive calls must yield
|
|
1, 2, 3."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
_patch_user_db(monkeypatch, SessionLocal)
|
|
|
|
with SessionLocal() as db:
|
|
sid, rid = _seed(db)
|
|
|
|
service = ChatService(username="alice")
|
|
for _ in range(3):
|
|
service.add_progress_step(
|
|
session_id=sid, research_id=rid, content="step"
|
|
)
|
|
|
|
with SessionLocal() as db:
|
|
rows = (
|
|
db.query(ChatProgressStep)
|
|
.filter_by(research_id=rid)
|
|
.order_by(ChatProgressStep.sequence_number)
|
|
.all()
|
|
)
|
|
assert [r.sequence_number for r in rows] == [1, 2, 3]
|
|
# And research_history.step_count must mirror the count.
|
|
rh = db.query(ResearchHistory).filter_by(id=rid).first()
|
|
assert rh.step_count == 3
|
|
|
|
|
|
def test_phase_is_optional(setup_database_for_all_tests, monkeypatch):
|
|
"""``phase`` defaults to None; the column stores NULL."""
|
|
SessionLocal = setup_database_for_all_tests
|
|
_patch_user_db(monkeypatch, SessionLocal)
|
|
|
|
with SessionLocal() as db:
|
|
sid, rid = _seed(db)
|
|
|
|
service = ChatService(username="alice")
|
|
step_id = service.add_progress_step(
|
|
session_id=sid, research_id=rid, content="step without phase"
|
|
)
|
|
|
|
with SessionLocal() as db:
|
|
row = db.query(ChatProgressStep).filter_by(id=step_id).first()
|
|
assert row.phase is None
|