mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-06 04:14:19 +00:00
* feat: postgres tests * feat: mongo cutoff * feat: mongo cutoff * feat: adjust docs and compose files * fix: mini code mongo removals * fix: tests and k8s mongo stuff * feat: test fixes * fix: ruff * fix: vale * Potential fix for pull request finding 'CodeQL / Clear-text logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: mini suggestions * vale lint fix 2 * fix: codeql columns thing * fix: test mongo * fix: tests coverage * feat: better tests 4 * feat: more tests * feat: decent coverage * fix: ruff fixes * fix: remove mongo mock * feat: enhance workflow engine and API routes; add document retrieval and source handling * feat: e2e tests * fix: mcp, mongo and more * fix: mini codeql warning * fix: agent chunk view * fix: mini issues * fix: more pg fixes * feat: postgres prep on start * feat: qa tests * fix: mini improvements * fix: tests --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Siddhant Rai <siddhant.rai.5686@gmail.com>
257 lines
8.8 KiB
Python
257 lines
8.8 KiB
Python
"""Tests for application/api/answer/services/continuation_service.py using pg_conn."""
|
|
|
|
from contextlib import contextmanager
|
|
from unittest.mock import patch
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
@contextmanager
|
|
def _patch_db(conn):
|
|
@contextmanager
|
|
def _yield():
|
|
yield conn
|
|
|
|
with patch(
|
|
"application.api.answer.services.continuation_service.db_readonly",
|
|
_yield,
|
|
), patch(
|
|
"application.api.answer.services.continuation_service.db_session",
|
|
_yield,
|
|
):
|
|
yield
|
|
|
|
|
|
class TestMakeSerializable:
|
|
def test_uuid_becomes_string(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
u = uuid4()
|
|
assert _make_serializable(u) == str(u)
|
|
|
|
def test_dict_keys_stringified(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
got = _make_serializable({42: "a", "b": 2})
|
|
assert got == {"42": "a", "b": 2}
|
|
|
|
def test_list_elements_recursively_serialized(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
u = uuid4()
|
|
got = _make_serializable([u, {"x": u}, 1])
|
|
assert got == [str(u), {"x": str(u)}, 1]
|
|
|
|
def test_bytes_decoded_to_string(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
assert _make_serializable(b"hello") == "hello"
|
|
|
|
def test_bytes_invalid_utf8_replaced(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
# Invalid UTF-8 byte sequence
|
|
got = _make_serializable(b"\xff\xfe")
|
|
assert isinstance(got, str)
|
|
|
|
def test_passes_through_primitives(self):
|
|
from application.api.answer.services.continuation_service import (
|
|
_make_serializable,
|
|
)
|
|
assert _make_serializable("hello") == "hello"
|
|
assert _make_serializable(42) == 42
|
|
assert _make_serializable(None) is None
|
|
assert _make_serializable(True) is True
|
|
|
|
|
|
class TestContinuationServiceSaveLoad:
|
|
def test_save_and_load_state(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
from application.storage.db.repositories.conversations import (
|
|
ConversationsRepository,
|
|
)
|
|
|
|
user = "u-cont"
|
|
conv = ConversationsRepository(pg_conn).create(user, name="c")
|
|
conv_id = str(conv["id"])
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
service.save_state(
|
|
conversation_id=conv_id,
|
|
user=user,
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
pending_tool_calls=[{"id": "call-1", "name": "search"}],
|
|
tools_dict={"search": {"arg": "value"}},
|
|
tool_schemas=[{"name": "search", "params": {}}],
|
|
agent_config={"model_id": "gpt-4"},
|
|
client_tools=[{"name": "client-tool"}],
|
|
)
|
|
loaded = service.load_state(conv_id, user)
|
|
assert loaded is not None
|
|
assert loaded["messages"] == [{"role": "user", "content": "hi"}]
|
|
|
|
def test_load_state_returns_none_when_not_found(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
got = service.load_state(
|
|
"00000000-0000-0000-0000-000000000000", "u",
|
|
)
|
|
assert got is None
|
|
|
|
def test_save_state_no_client_tools(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
from application.storage.db.repositories.conversations import (
|
|
ConversationsRepository,
|
|
)
|
|
|
|
user = "u-no-client"
|
|
conv = ConversationsRepository(pg_conn).create(user, name="c")
|
|
conv_id = str(conv["id"])
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
service.save_state(
|
|
conversation_id=conv_id,
|
|
user=user,
|
|
messages=[],
|
|
pending_tool_calls=[{"id": "c"}],
|
|
tools_dict={},
|
|
tool_schemas=[],
|
|
agent_config={},
|
|
client_tools=None,
|
|
)
|
|
loaded = service.load_state(conv_id, user)
|
|
assert loaded is not None
|
|
|
|
def test_delete_state_returns_true_when_deleted(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
from application.storage.db.repositories.conversations import (
|
|
ConversationsRepository,
|
|
)
|
|
|
|
user = "u-del"
|
|
conv = ConversationsRepository(pg_conn).create(user, name="c")
|
|
conv_id = str(conv["id"])
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
service.save_state(
|
|
conversation_id=conv_id,
|
|
user=user,
|
|
messages=[],
|
|
pending_tool_calls=[{"id": "c"}],
|
|
tools_dict={},
|
|
tool_schemas=[],
|
|
agent_config={},
|
|
)
|
|
got = service.delete_state(conv_id, user)
|
|
assert got is True
|
|
|
|
def test_delete_state_returns_false_when_missing(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
got = service.delete_state(
|
|
"00000000-0000-0000-0000-000000000000", "u",
|
|
)
|
|
assert got is False
|
|
|
|
|
|
class TestContinuationServiceLegacyIdHandling:
|
|
"""An unresolvable Mongo ObjectId must not reach ``CAST(:conv_id AS uuid)``
|
|
in ``PendingToolStateRepository`` — that cast raises and aborts the
|
|
enclosing transaction. This can happen when an OpenAI-compatible client
|
|
echoes back a ``chatcmpl-<legacy_objectid>`` id from before the PG
|
|
cutover and the conversation hasn't been backfilled."""
|
|
|
|
LEGACY_OBJECTID = "507f1f77bcf86cd799439011"
|
|
|
|
def test_load_state_unresolvable_legacy_id_returns_none(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
got = service.load_state(self.LEGACY_OBJECTID, "u")
|
|
assert got is None
|
|
|
|
def test_delete_state_unresolvable_legacy_id_returns_false(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
got = service.delete_state(self.LEGACY_OBJECTID, "u")
|
|
assert got is False
|
|
|
|
def test_save_state_unresolvable_legacy_id_raises(self, pg_conn):
|
|
import pytest
|
|
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
with pytest.raises(ValueError):
|
|
service.save_state(
|
|
conversation_id=self.LEGACY_OBJECTID,
|
|
user="u",
|
|
messages=[],
|
|
pending_tool_calls=[{"id": "c"}],
|
|
tools_dict={},
|
|
tool_schemas=[],
|
|
agent_config={},
|
|
)
|
|
|
|
def test_load_state_resolves_backfilled_legacy_id(self, pg_conn):
|
|
from application.api.answer.services.continuation_service import (
|
|
ContinuationService,
|
|
)
|
|
from application.storage.db.repositories.conversations import (
|
|
ConversationsRepository,
|
|
)
|
|
|
|
user = "legacy-cont"
|
|
ConversationsRepository(pg_conn).create(
|
|
user, name="c", legacy_mongo_id=self.LEGACY_OBJECTID,
|
|
)
|
|
|
|
service = ContinuationService()
|
|
with _patch_db(pg_conn):
|
|
# Save via the legacy id — should resolve to the PG UUID internally.
|
|
service.save_state(
|
|
conversation_id=self.LEGACY_OBJECTID,
|
|
user=user,
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
pending_tool_calls=[{"id": "c"}],
|
|
tools_dict={},
|
|
tool_schemas=[],
|
|
agent_config={},
|
|
)
|
|
# And load via the legacy id — should round-trip.
|
|
loaded = service.load_state(self.LEGACY_OBJECTID, user)
|
|
assert loaded is not None
|
|
assert loaded["messages"] == [{"role": "user", "content": "hi"}]
|