mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
* feat(persistence): expand thread incarnation storage Add nullable thread and MCP task incarnation columns while preserving mixed-version writes. New thread records receive stable incarnation IDs, and new task rows copy the matching owned or shared thread incarnation without changing any read, claim, session, or deletion behavior. * test(persistence): pin incarnation rollback compatibility * test(api): pin internal thread response boundary * fix(persistence): rebase incarnation rollout after projects --------- Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
96 lines
5.3 KiB
Python
96 lines
5.3 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")
|
|
async with engine.begin() as conn:
|
|
await conn.execute(sa.text("ALTER TABLE threads_meta ADD COLUMN incarnation VARCHAR(32)"))
|
|
await conn.execute(sa.text("ALTER TABLE mcp_tasks ADD COLUMN thread_incarnation VARCHAR(32)"))
|
|
if race:
|
|
current_head, current_revisions = bootstrap._get_revision_metadata()
|
|
assert current_head == "0019_thread_incarnations"
|
|
assert {"0020_threads_meta_project_id", "0021_batch_acceptance", current_head} <= current_revisions
|
|
monkeypatch.setattr(
|
|
bootstrap,
|
|
"_get_revision_metadata",
|
|
lambda: (
|
|
"0020_threads_meta_project_id",
|
|
current_revisions - {"0021_batch_acceptance", current_head},
|
|
),
|
|
)
|
|
|
|
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()
|