deer-flow/backend/tests/test_run_producer_cutover_cli.py
spud 21175ba46f
fix(runtime): persistently fence stale run event producers (#7619)
* fix(runtime): fence revoked run event producers

* fix(runtime): reduce off-mode producer guard locking

* docs: keep inherited guidance within size budget

* chore(runtime): remove development phase labels

* fix(persistence): normalize libpq SSL options for asyncpg connections

* docs(runtime): clarify producer fencing behavior in off mode

* test(persistence): align migration TLS probes with channel-binding rejection

* fix(runtime): authorize fenced Gateway history seeding

* fix(scheduler): preserve takeover identity in recovery publication

* fix(scheduler): report unconfirmed recovery publication

* fix(runtime): preserve history messages before the first human turn

* fix(runtime): clarify run creation failure diagnostic

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-02 15:26:42 +08:00

95 lines
4.2 KiB
Python

"""Offline activation must be explicit and must not disclose database secrets."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from support.postgres import asyncpg_test_url
def test_cli_requires_stopped_cluster_acknowledgement():
from scripts.run_producer_fencing import main
with pytest.raises(SystemExit) as stopped:
main(["--cutover-id", "upgrade-1"])
assert stopped.value.code == 2
def test_cli_redacts_connection_failure(monkeypatch, capsys):
from scripts import run_producer_fencing as cli
monkeypatch.setattr(cli, "activate", AsyncMock(side_effect=RuntimeError("postgresql://user:secret@host/db")))
assert cli.main(["--cutover-id", "upgrade-1", "--all-gateways-stopped"]) == 1
output = capsys.readouterr()
assert "secret" not in output.out + output.err
assert "RuntimeError" in output.err
@pytest.mark.asyncio
async def test_cli_activation_uses_configured_schema_and_closes_engine(monkeypatch):
from deerflow.config.app_config import AppConfig
from deerflow.persistence import engine
from deerflow.persistence.run import producer
from scripts import run_producer_fencing as cli
config = SimpleNamespace(database=SimpleNamespace(backend="postgres", model_copy=lambda **kw: "database-config"))
monkeypatch.setattr(AppConfig, "from_file", lambda path: config)
initialize, close, dispose, activate = AsyncMock(), AsyncMock(), AsyncMock(), AsyncMock()
monkeypatch.setattr(engine, "init_engine_from_config", initialize)
monkeypatch.setattr(engine, "get_engine", lambda: SimpleNamespace(dispose=dispose))
monkeypatch.setattr(engine, "get_session_factory", lambda: "session-factory")
monkeypatch.setattr(engine, "close_engine", close)
monkeypatch.setattr(producer, "activate_producer_fencing", activate)
await cli.activate(config_path="operator.yaml", cutover_id="upgrade-1")
initialize.assert_awaited_once_with("database-config")
dispose.assert_awaited_once()
activate.assert_awaited_once_with("session-factory", cutover_id="upgrade-1")
close.assert_awaited_once()
@pytest.mark.asyncio
async def test_real_offline_command_bootstraps_and_activates_postgres(tmp_path):
import asyncio
import os
import sys
from pathlib import Path
from uuid import uuid4
import yaml
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
uri = os.environ.get("TEST_POSTGRES_URI")
if not uri:
pytest.skip("requires TEST_POSTGRES_URI")
schema = "cutover_cli_" + uuid4().hex
config = tmp_path / "operator.yaml"
config.write_text(
yaml.safe_dump(
{
"database": {"backend": "postgres", "postgres_url": "$TEST_POSTGRES_URI", "postgres_schema": schema},
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
"run_events": {"backend": "db", "producer_fencing": "strict", "producer_cutover_id": "cli-test"},
}
),
encoding="utf-8",
)
engine = create_async_engine(asyncpg_test_url(uri), connect_args={"server_settings": {"search_path": schema}})
try:
script = Path(__file__).resolve().parents[1] / "scripts" / "run_producer_fencing.py"
process = await asyncio.create_subprocess_exec(sys.executable, str(script), "--config", str(config), "--cutover-id", "cli-test", "--all-gateways-stopped", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), 30)
finally:
if process.returncode is None:
process.kill()
await process.wait()
assert process.returncode == 0, (stdout, stderr)
async with engine.connect() as conn:
assert (await conn.execute(text("SELECT strict,cutover_id FROM run_producer_policy WHERE id=1"))).one() == (True, "cli-test")
assert await conn.scalar(text("SELECT count(*) FROM pg_trigger WHERE tgrelid='run_events'::regclass AND tgname='run_event_producer_guard'")) == 1
finally:
async with engine.begin() as conn:
await conn.execute(text(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE'))
await engine.dispose()