mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* 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>
126 lines
6.0 KiB
Python
126 lines
6.0 KiB
Python
"""Migration 0037 adds the nullable project_documents.summary column and removes it again."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import uuid
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
from alembic import command
|
|
from alembic.script import ScriptDirectory
|
|
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
|
|
|
|
REVISION = "0037_project_document_summaries"
|
|
PREVIOUS = "0036_run_idempotency_request"
|
|
CHANNEL_BINDINGS = "0038_channel_thread_bindings"
|
|
USER_DISABLED = "0039_user_disabled"
|
|
GROUP_NOTICE = "0040_scheduled_group_notice"
|
|
IM_NOTICE_PREFERENCES = "0041_im_notice_preferences"
|
|
CHANNEL_LEASES = "0042_channel_leases"
|
|
RUNTIME_IM_NOTICES = "0043_runtime_im_notices"
|
|
CHANNEL_RUNTIME_CONFIGS = "0044_channel_runtime_configs"
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def test_0037_chains_after_0036_in_single_migration_chain():
|
|
script = ScriptDirectory(str(bootstrap._MIGRATIONS_DIR))
|
|
assert len(script.get_heads()) == 1
|
|
assert REVISION in {revision.revision for revision in script.walk_revisions()}
|
|
assert script.get_revision(CHANNEL_BINDINGS).down_revision == REVISION
|
|
assert script.get_revision(USER_DISABLED).down_revision == CHANNEL_BINDINGS
|
|
assert script.get_revision(GROUP_NOTICE).down_revision == USER_DISABLED
|
|
assert script.get_revision(IM_NOTICE_PREFERENCES).down_revision == GROUP_NOTICE
|
|
assert script.get_revision(CHANNEL_LEASES).down_revision == IM_NOTICE_PREFERENCES
|
|
assert script.get_revision(RUNTIME_IM_NOTICES).down_revision == CHANNEL_LEASES
|
|
assert script.get_revision(CHANNEL_RUNTIME_CONFIGS).down_revision == RUNTIME_IM_NOTICES
|
|
assert script.get_revision(REVISION).down_revision == PREVIOUS
|
|
# alembic_version.version_num is VARCHAR(32).
|
|
assert len(REVISION) <= 32
|
|
|
|
|
|
def _engine(tmp_path, backend):
|
|
if backend == "sqlite":
|
|
return create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'migration.db'}"), None
|
|
uri = os.environ.get("TEST_POSTGRES_URI")
|
|
if not uri:
|
|
pytest.skip("requires TEST_POSTGRES_URI (real Postgres migration)")
|
|
# Translate libpq sslmode to asyncpg ssl rather than dropping it: a URI
|
|
# that requests verify-ca/verify-full must not silently fall back to an
|
|
# unverified connection (tests/AGENTS.md "PostgreSQL batch fixtures").
|
|
schema = f"doc_summaries_{uuid.uuid4().hex}"
|
|
return create_async_engine(asyncpg_test_url(uri), connect_args=build_asyncpg_connect_args(schema)), schema
|
|
|
|
|
|
@pytest.mark.parametrize("backend", ["sqlite", "postgres"])
|
|
async def test_0037_upgrade_keeps_rows_and_downgrade_drops_summary(tmp_path, backend):
|
|
engine, schema = _engine(tmp_path, backend)
|
|
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(command.upgrade, cfg, PREVIOUS)
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
sa.text(
|
|
"INSERT INTO project_documents (id, project_id, user_id, name, stored_relpath, sha256, size_bytes, created_at, updated_at) "
|
|
"VALUES ('d1','p1','u1','notes.txt','u1/p1/d1-notes.txt','ab12',10,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
|
|
)
|
|
)
|
|
|
|
async def doc_columns():
|
|
async with engine.connect() as conn:
|
|
return await conn.run_sync(lambda sync: {col["name"]: col for col in sa.inspect(sync).get_columns("project_documents")})
|
|
|
|
await asyncio.to_thread(command.upgrade, cfg, REVISION)
|
|
columns = await doc_columns()
|
|
assert columns["summary"]["nullable"] and columns["summary"]["default"] is None
|
|
async with engine.connect() as conn:
|
|
row = (await conn.execute(sa.text("SELECT name, size_bytes, summary FROM project_documents WHERE id='d1'"))).one()
|
|
# No backfill: the pre-existing row keeps a NULL summary.
|
|
assert tuple(row) == ("notes.txt", 10, None)
|
|
|
|
async with engine.begin() as conn:
|
|
await conn.execute(sa.text("UPDATE project_documents SET summary='one line' WHERE id='d1'"))
|
|
await asyncio.to_thread(command.downgrade, cfg, PREVIOUS)
|
|
assert "summary" not in await doc_columns()
|
|
async with engine.connect() as conn:
|
|
assert (await conn.execute(sa.text("SELECT name, size_bytes FROM project_documents WHERE id='d1'"))).one() == ("notes.txt", 10)
|
|
await asyncio.to_thread(command.upgrade, cfg, REVISION)
|
|
assert "summary" in await doc_columns()
|
|
finally:
|
|
if schema:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(sa.text(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE'))
|
|
await engine.dispose()
|
|
|
|
|
|
def test_postgres_uri_tls_policy_is_preserved(tmp_path, monkeypatch):
|
|
"""The fixture must translate, never drop, an explicit sslmode (tests/AGENTS.md)."""
|
|
monkeypatch.setenv("TEST_POSTGRES_URI", "postgresql://u:p@db:5432/deerflow?sslmode=verify-full&channel_binding=require")
|
|
captured = {}
|
|
|
|
def capture_engine(url, **kwargs):
|
|
captured["url"] = url
|
|
raise RuntimeError("stop before database acquisition")
|
|
|
|
monkeypatch.setitem(globals(), "create_async_engine", capture_engine)
|
|
with pytest.raises(RuntimeError, match="stop before database acquisition"):
|
|
_engine(tmp_path, "postgres")
|
|
assert "ssl=verify-full" in captured["url"]
|
|
assert "sslmode" not in captured["url"]
|
|
assert "channel_binding" not in captured["url"]
|
|
|
|
|
|
def test_postgres_uri_conflicting_ssl_settings_rejected(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("TEST_POSTGRES_URI", "postgresql://u:p@db:5432/deerflow?sslmode=require&ssl=disable")
|
|
monkeypatch.setitem(globals(), "create_async_engine", lambda *a, **k: None)
|
|
with pytest.raises(ValueError, match="Conflicting ssl and sslmode"):
|
|
_engine(tmp_path, "postgres")
|