mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 09:08:38 +00:00
* feat(subagents): check and persist durable batch acceptance Carry optional per-item criteria into native subagents, reuse the deterministic checker, and expose separate verdicts through item queries and exports. Preserve execution and retry semantics, renew leases during checks, and migrate existing batch rows with nullable acceptance fields. * fix(subagents): align batch acceptance normalization and sandbox admission * test(auth): include project permissions in the full-stack contract
82 lines
4.5 KiB
Python
82 lines
4.5 KiB
Python
"""Upgrade legacy/project schemas, preserve rows, and reject missing batch fields."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
from alembic import command
|
|
from alembic.util.exc import CommandError
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
from deerflow.persistence import bootstrap
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("source_revision", ["0018_oauth_identity_pg_partial", "0020_threads_meta_project_id"])
|
|
async def test_upgrade_and_downgrade_preserve_legacy_batch_item(tmp_path, source_revision):
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'batch.db'}")
|
|
cfg = bootstrap._get_alembic_config(engine)
|
|
try:
|
|
await asyncio.to_thread(bootstrap._upgrade, cfg, source_revision)
|
|
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)"
|
|
)
|
|
)
|
|
if source_revision == "0020_threads_meta_project_id":
|
|
await conn.execute(sa.text("INSERT INTO projects (id,user_id,name,instructions,presentation,status,created_at,updated_at) VALUES ('p','u','existing project','','{}','active',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 result',0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
|
|
)
|
|
)
|
|
await bootstrap.bootstrap_schema(engine, backend="sqlite")
|
|
await bootstrap.bootstrap_schema(engine, backend="sqlite")
|
|
async with engine.connect() as conn:
|
|
columns = await conn.run_sync(lambda sync: {col["name"] for col in sa.inspect(sync).get_columns("subagent_batch_items")})
|
|
assert {"acceptance_criteria", "acceptance_verdict"} <= columns
|
|
row = (await conn.execute(sa.text("SELECT result,status,acceptance_criteria,acceptance_verdict FROM subagent_batch_items"))).one()
|
|
assert tuple(row) == ("old result", "succeeded", None, None)
|
|
await asyncio.to_thread(command.downgrade, cfg, source_revision)
|
|
async with engine.connect() as conn:
|
|
columns = await conn.run_sync(lambda sync: {col["name"] for col in sa.inspect(sync).get_columns("subagent_batch_items")})
|
|
assert "acceptance_verdict" not in columns
|
|
assert await conn.scalar(sa.text("SELECT result FROM subagent_batch_items")) == "old result"
|
|
if source_revision == "0020_threads_meta_project_id":
|
|
assert await conn.scalar(sa.text("SELECT name FROM projects WHERE id='p'")) == "existing project"
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("race", [False, True])
|
|
async def test_forward_revision_cannot_skip_required_batch_columns(tmp_path, monkeypatch, race):
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'forward.db'}")
|
|
cfg = bootstrap._get_alembic_config(engine)
|
|
try:
|
|
# Keep the project schema present so only the batch-column guard can
|
|
# reject this database, on both direct and concurrent-startup paths.
|
|
await asyncio.to_thread(bootstrap._upgrade, cfg, "0020_threads_meta_project_id")
|
|
if race:
|
|
|
|
def raced_upgrade(*args):
|
|
sync = sa.create_engine(f"sqlite:///{tmp_path / 'forward.db'}")
|
|
try:
|
|
with sync.begin() as conn:
|
|
conn.execute(sa.text("UPDATE alembic_version SET version_num='0019_thread_incarnations'"))
|
|
finally:
|
|
sync.dispose()
|
|
raise CommandError("another deployment migrated first")
|
|
|
|
monkeypatch.setattr(bootstrap, "_upgrade", raced_upgrade)
|
|
else:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(sa.text("UPDATE alembic_version SET version_num='0019_thread_incarnations'"))
|
|
with pytest.raises(RuntimeError, match="missing required local schema: subagent_batch_items.acceptance_criteria, subagent_batch_items.acceptance_verdict"):
|
|
await bootstrap.bootstrap_schema(engine, backend="sqlite")
|
|
finally:
|
|
await engine.dispose()
|