mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-16 09:38:41 +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
70 lines
3.8 KiB
Python
70 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
import deerflow.persistence.models # noqa: F401
|
|
from deerflow.persistence.base import Base
|
|
from deerflow.persistence.bootstrap import bootstrap_schema
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def test_migration_interrupts_legacy_queue_and_adds_claim_fields(tmp_path: Path) -> None:
|
|
db_path = tmp_path / "deer.db"
|
|
sync = sa.create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
Base.metadata.create_all(sync)
|
|
with sync.begin() as conn:
|
|
conn.execute(sa.text("DROP INDEX uq_scheduled_task_run_active"))
|
|
conn.execute(sa.text("ALTER TABLE scheduled_task_runs DROP COLUMN attempt_count"))
|
|
conn.execute(sa.text("ALTER TABLE scheduled_task_runs DROP COLUMN lease_expires_at"))
|
|
conn.execute(sa.text("ALTER TABLE scheduled_task_runs DROP COLUMN lease_owner"))
|
|
conn.execute(sa.text("CREATE UNIQUE INDEX uq_scheduled_task_run_active ON scheduled_task_runs (task_id) WHERE status IN ('queued', 'running')"))
|
|
conn.execute(
|
|
sa.text(
|
|
"INSERT INTO scheduled_tasks "
|
|
"(id, user_id, thread_id, context_mode, assistant_id, title, prompt, "
|
|
"schedule_type, schedule_spec, timezone, status, overlap_policy, run_count, created_at, updated_at) "
|
|
"VALUES ('task-legacy', 'user-1', 'thread-1', 'reuse_thread', 'lead_agent', "
|
|
"'Legacy', 'Prompt', 'cron', '{\"cron\":\"0 9 * * *\"}', 'UTC', "
|
|
"'enabled', 'skip', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
|
)
|
|
)
|
|
conn.execute(
|
|
sa.text(
|
|
"INSERT INTO scheduled_task_runs (id, task_id, thread_id, scheduled_for, trigger, status, created_at) VALUES ('run-legacy-queued', 'task-legacy', 'thread-1', CURRENT_TIMESTAMP, 'scheduled', 'queued', CURRENT_TIMESTAMP)"
|
|
)
|
|
)
|
|
conn.execute(sa.text("CREATE TABLE IF NOT EXISTS alembic_version (version_num VARCHAR(32) NOT NULL)"))
|
|
conn.execute(sa.text("DELETE FROM alembic_version"))
|
|
conn.execute(sa.text("INSERT INTO alembic_version (version_num) VALUES ('0013_mcp_task_notifications')"))
|
|
finally:
|
|
sync.dispose()
|
|
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}")
|
|
try:
|
|
await bootstrap_schema(engine, backend="sqlite")
|
|
async with engine.connect() as conn:
|
|
columns = {column["name"]: column for column in await conn.run_sync(lambda connection: sa.inspect(connection).get_columns("scheduled_task_runs"))}
|
|
version = await conn.scalar(sa.text("SELECT version_num FROM alembic_version"))
|
|
overlap_policy = await conn.scalar(sa.text("SELECT overlap_policy FROM scheduled_tasks WHERE id = 'task-legacy'"))
|
|
legacy_run = (await conn.execute(sa.text("SELECT status, error, finished_at FROM scheduled_task_runs WHERE id = 'run-legacy-queued'"))).one()
|
|
index_sql = await conn.scalar(sa.text("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'uq_scheduled_task_run_active'"))
|
|
|
|
# Bootstrap always advances to the repository head after exercising
|
|
# the 0015 migration behavior below.
|
|
assert version == "0021_batch_acceptance"
|
|
assert {"lease_owner", "lease_expires_at", "attempt_count"} <= columns.keys()
|
|
assert columns["attempt_count"]["nullable"] is False
|
|
assert overlap_policy == "enqueue"
|
|
assert legacy_run.status == "interrupted"
|
|
assert "gateway upgraded" in legacy_run.error
|
|
assert legacy_run.finished_at is not None
|
|
assert "'launching'" in index_sql
|
|
finally:
|
|
await engine.dispose()
|