deer-flow/backend/tests/test_skill_mutation_postgres.py
Ryker_Feng d1d0958bea
feat(skills): add guarded evolution host APIs (#5645)
* feat(skills): add guarded evolution host APIs

* docs(agents): avoid growing saturated guidance chains

* fix(skills): address mutation host review findings

* fix(skills): order name fence before projection lock

* fix(skills): preserve composite toggle lock order

* fix(skills): address catalog and pagination review feedback

* docs: keep subagent guidance within merged instruction budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 21:17:02 +08:00

70 lines
3.2 KiB
Python

"""Opt-in live PostgreSQL fence test; never substitute SQLite for PG locking."""
import asyncio
import os
import threading
import uuid
import pytest
from sqlalchemy import create_engine, select, text
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from deerflow.config.database_config import DatabaseConfig
from deerflow.persistence.base import Base
from deerflow.persistence.run import RunRepository
from deerflow.persistence.run.model import CompletedRunSnapshotRow, RunRow
from deerflow.skills.mutations.topology import mutation_session_factory
pytestmark = pytest.mark.skipif(not os.environ.get("DEERFLOW_TEST_POSTGRES_URL"), reason="requires an explicit live DEERFLOW_TEST_POSTGRES_URL")
@pytest.mark.asyncio
async def test_run_deletion_waits_for_publication_source_share_lock():
schema = "skill_evolution_test_" + uuid.uuid4().hex
config = DatabaseConfig(backend="postgres", postgres_url=os.environ["DEERFLOW_TEST_POSTGRES_URL"], postgres_schema=schema)
admin = create_engine(config.app_sync_sqlalchemy_url)
with admin.begin() as connection:
connection.execute(text(f'CREATE SCHEMA "{schema}"'))
sync_engine, sessions = mutation_session_factory(config.app_sync_sqlalchemy_url, postgres_schema=schema)
async_engine = create_async_engine(config.app_sqlalchemy_url, connect_args={"server_settings": {"search_path": schema}})
sf = async_sessionmaker(async_engine, expire_on_commit=False)
release, acquired = threading.Event(), threading.Event()
worker = None
deleting = None
try:
Base.metadata.create_all(sync_engine)
with sessions.begin() as session:
session.add(RunRow(run_id="run", thread_id="thread", user_id="owner", status="success"))
session.flush()
session.add(CompletedRunSnapshotRow(snapshot_ref="snapshot", run_id="run", scope_digest="scope", evidence_revision="revision", retention_revision=0, snapshot_json={}))
def publication_fence():
with sessions.begin() as session:
session.scalar(select(RunRow).where(RunRow.run_id == "run").with_for_update(read=True))
acquired.set()
assert release.wait(5)
worker = asyncio.create_task(asyncio.to_thread(publication_fence))
assert await asyncio.to_thread(acquired.wait, 5)
deleting = asyncio.create_task(RunRepository(sf).delete("run", user_id="owner"))
await asyncio.sleep(0.2)
assert not deleting.done(), "source deletion must wait for the accepted publication fence"
release.set()
await asyncio.wait_for(deleting, 5)
await worker
async with sf() as session:
assert await session.get(RunRow, "run") is None
assert await session.get(CompletedRunSnapshotRow, "snapshot") is None
finally:
release.set()
if worker is not None:
await worker
if deleting is not None:
await asyncio.gather(deleting, return_exceptions=True)
await async_engine.dispose()
sync_engine.dispose()
# Exact, randomly-created test schema only; never a configured schema.
with admin.begin() as connection:
connection.execute(text(f'DROP SCHEMA "{schema}" CASCADE'))
admin.dispose()