mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* 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
112 lines
6.0 KiB
Python
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")
|