deer-flow/backend/tests/test_migration_0033_batch_result_artifact.py
Guo-Yixin 3182ecb1e4
feat(subagents): preserve RAG evidence in durable batch exports (#6413)
* feat(subagents): preserve RAG evidence in durable batch exports

* docs(subagents): clarify evidence omissions and migration head

* docs(persistence): update the migration chain head

* test(persistence): track the new batch evidence migration head

* test(persistence): verify batch evidence migration on PostgreSQL

* docs(subagents): retain evidence migration compatibility guidance

* fix(subagents): propagate cited evidence forwarding omissions

* fix(tests): adapt batch evidence PostgreSQL CI connection options

* fix(tests): preserve TLS policy in batch evidence migration fixture
2026-10-07 18:37:31 +08:00

112 lines
6.0 KiB
Python

"""Nullable batch evidence migration preserves legacy report rows."""
import asyncio
import os
import uuid
import pytest
import sqlalchemy as sa
from alembic import command
from sqlalchemy.ext.asyncio import create_async_engine
from support.postgres import asyncpg_test_url
from deerflow.persistence import bootstrap
from deerflow.persistence.postgres_schema import build_asyncpg_connect_args
from deerflow.persistence.subagent_batches.model import SubagentBatchItemRow
@pytest.mark.asyncio
@pytest.mark.parametrize("backend", ["sqlite", "postgres"])
async def test_upgrade_downgrade_and_reupgrade_preserve_report(tmp_path, backend):
schema = None
if backend == "postgres":
uri = os.environ.get("TEST_POSTGRES_URI")
if not uri:
pytest.skip("requires TEST_POSTGRES_URI (real Postgres batch evidence migration)")
schema = f"batch_evidence_{uuid.uuid4().hex}"
engine = create_async_engine(asyncpg_test_url(uri), connect_args=build_asyncpg_connect_args(schema))
else:
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'batch.db'}")
cfg = bootstrap._get_alembic_config(engine, postgres_schema=schema or "")
try:
if schema:
async with engine.begin() as conn:
await conn.execute(sa.text(f'CREATE SCHEMA "{schema}"'))
await asyncio.to_thread(bootstrap._upgrade, cfg, "0032_activity_and_task_events")
async with engine.begin() as conn:
await conn.execute(
sa.text(
"INSERT INTO subagent_batches (id,user_id,thread_id,submission_key,title,subagent_type,status,total_items,max_live_items,max_running_items,max_attempts,execution_spec,created_at,updated_at) "
"VALUES ('b','u','t','k','title','general-purpose','completed',1,1,1,2,'{}',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
)
)
await conn.execute(
sa.text(
"INSERT INTO subagent_batch_items (id,batch_id,item_key,position,prompt,status,attempt,result,result_truncated,created_at,updated_at) "
"VALUES ('i','b','k',0,'p','succeeded',1,'old report',FALSE,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
)
)
await bootstrap.bootstrap_schema(engine, backend=backend, postgres_schema=schema or "")
await bootstrap.bootstrap_schema(engine, backend=backend, postgres_schema=schema or "")
async with engine.connect() as conn:
row = (await conn.execute(sa.text("SELECT result,status,result_artifact FROM subagent_batch_items"))).one()
assert tuple(row) == ("old report", "succeeded", None)
snapshot = {"knowledge_sources": {"version": 1, "sources": [{"id": "a" * 32 + "-1", "provider": "ragflow", "dataset_id": "kb", "document_id": "doc", "chunk_id": "chunk", "text": "Captured evidence ☃"}], "omitted_count": 0}}
async with engine.begin() as conn:
await conn.execute(sa.update(SubagentBatchItemRow).where(SubagentBatchItemRow.id == "i").values(result_artifact=snapshot))
async with engine.connect() as conn:
assert (await conn.execute(sa.select(SubagentBatchItemRow.result_artifact).where(SubagentBatchItemRow.id == "i"))).scalar_one() == snapshot
await asyncio.to_thread(command.downgrade, cfg, "0032_activity_and_task_events")
async with engine.connect() as conn:
columns = await conn.run_sync(lambda sync: {column["name"] for column in sa.inspect(sync).get_columns("subagent_batch_items")})
assert "result_artifact" not in columns
assert (await conn.execute(sa.text("SELECT result FROM subagent_batch_items"))).scalar_one() == "old report"
await bootstrap.bootstrap_schema(engine, backend=backend, postgres_schema=schema or "")
async with engine.connect() as conn:
assert (await conn.execute(sa.text("SELECT result_artifact FROM subagent_batch_items"))).scalar_one() is None
finally:
if schema:
async with engine.begin() as conn:
await conn.execute(sa.text(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE'))
await engine.dispose()
class _StopBeforeDatabase(Exception):
"""End a connection-contract probe before acquiring database resources."""
@pytest.mark.asyncio
@pytest.mark.parametrize("sslmode", [None, "disable", "allow", "prefer", "require", "verify-ca", "verify-full"])
async def test_postgres_migration_setup_preserves_tls_policy(monkeypatch, tmp_path, sslmode):
uri = "postgresql://user:password@localhost:5432/test?target_session_attrs=read-write&channel_binding=prefer"
if sslmode is not None:
uri += f"&sslmode={sslmode}"
monkeypatch.setenv("TEST_POSTGRES_URI", uri)
def capture_engine(url, **kwargs):
parsed = sa.engine.make_url(url)
_args, connect_options = parsed.get_dialect()().create_connect_args(parsed)
assert connect_options["target_session_attrs"] == "read-write"
assert "sslmode" not in connect_options and "channel_binding" not in connect_options
if sslmode is None:
assert "ssl" not in connect_options
else:
assert connect_options.get("ssl") == sslmode
raise _StopBeforeDatabase
monkeypatch.setitem(globals(), "create_async_engine", capture_engine)
with pytest.raises(_StopBeforeDatabase):
await test_upgrade_downgrade_and_reupgrade_preserve_report(tmp_path, "postgres")
@pytest.mark.asyncio
async def test_postgres_migration_setup_rejects_conflicting_tls_policy(monkeypatch, tmp_path):
monkeypatch.setenv("TEST_POSTGRES_URI", "postgresql://user:password@localhost:5432/test?ssl=require&sslmode=disable")
def reject_engine_creation(*args, **kwargs):
pytest.fail("Conflicting TLS options must be rejected before engine creation")
monkeypatch.setitem(globals(), "create_async_engine", reject_engine_creation)
with pytest.raises(ValueError, match="Conflicting ssl and sslmode"):
await test_upgrade_downgrade_and_reupgrade_preserve_report(tmp_path, "postgres")