test: migrate step-by-step tour sessions to SQLite (#40636)

This commit is contained in:
Asuka Minato
2026-08-25 06:20:13 +00:00
committed by GitHub
parent 39ce9f68fc
commit a9035a3264
@@ -3,7 +3,8 @@ from __future__ import annotations
from datetime import UTC, datetime
import pytest
from sqlalchemy.exc import IntegrityError
from sqlalchemy import event, select
from sqlalchemy.orm import Session, sessionmaker
from enums import DeploymentEdition
from models.account import Account, AccountStatus
@@ -12,58 +13,6 @@ from services import step_by_step_tour_service as service_module
from services.step_by_step_tour_service import StepByStepTourService
class _ScalarResult:
def __init__(self, state: AccountStepByStepTourState | None) -> None:
self._state = state
def scalar_one_or_none(self) -> AccountStepByStepTourState | None:
return self._state
class _FakeSession:
def __init__(self, state: AccountStepByStepTourState | None = None) -> None:
self.state = state
self.added: list[AccountStepByStepTourState] = []
self.commit_count = 0
self.flush_count = 0
self.refresh_count = 0
self.rollback_count = 0
def execute(self, _stmt) -> _ScalarResult:
return _ScalarResult(self.state)
def add(self, state: AccountStepByStepTourState) -> None:
self.state = state
self.added.append(state)
def flush(self) -> None:
self.flush_count += 1
def commit(self) -> None:
self.commit_count += 1
def refresh(self, state: AccountStepByStepTourState) -> None:
self.refresh_count += 1
state.updated_at = datetime(2026, 6, 28, tzinfo=UTC)
def rollback(self) -> None:
self.rollback_count += 1
class _RaceInsertSession(_FakeSession):
def __init__(self, state_after_rollback: AccountStepByStepTourState) -> None:
super().__init__(state=None)
self.state_after_rollback = state_after_rollback
def flush(self) -> None:
self.flush_count += 1
raise IntegrityError("insert", {}, Exception("duplicate"))
def rollback(self) -> None:
super().rollback()
self.state = self.state_after_rollback
def _account(*, initialized_at: datetime | None = None, created_at: datetime | None = None) -> Account:
account = Account(name="User", email="user@example.com", status=AccountStatus.ACTIVE)
account.id = "account-1"
@@ -78,6 +27,17 @@ def _state() -> AccountStepByStepTourState:
return state
def _persist_state(session: Session, state: AccountStepByStepTourState) -> None:
session.add(state)
session.commit()
def _load_state(session: Session) -> AccountStepByStepTourState | None:
return session.scalar(
select(AccountStepByStepTourState).where(AccountStepByStepTourState.account_id == "account-1")
)
def _set_tour_config(monkeypatch: pytest.MonkeyPatch, *, enabled: bool, rollout_started_at: datetime | None) -> None:
monkeypatch.setattr(service_module.dify_config, "ENABLE_STEP_BY_STEP_TOUR", enabled)
monkeypatch.setattr(service_module.dify_config, "STEP_BY_STEP_TOUR_ROLLOUT_STARTED_AT", rollout_started_at)
@@ -85,22 +45,24 @@ def _set_tour_config(monkeypatch: pytest.MonkeyPatch, *, enabled: bool, rollout_
def test_get_state_creates_state_and_records_first_workspace_for_eligible_account(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
sqlite_session_factory: sessionmaker[Session],
) -> None:
_set_tour_config(monkeypatch, enabled=True, rollout_started_at=datetime(2026, 6, 1))
session = _FakeSession()
result = StepByStepTourService.get_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-1",
session=session,
session=sqlite_session,
)
assert result["first_workspace_id"] == "workspace-1"
assert result["completed_task_ids"] == []
assert len(session.added) == 1
assert session.added[0].account_id == "account-1"
assert session.commit_count == 1
assert session.refresh_count == 1
with sqlite_session_factory() as observer:
persisted = _load_state(observer)
assert persisted is not None
assert persisted.account_id == "account-1"
assert persisted.first_workspace_id == "workspace-1"
def test_is_eligible_does_not_depend_on_cloud_edition(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -114,14 +76,15 @@ def test_is_eligible_does_not_depend_on_cloud_edition(monkeypatch: pytest.Monkey
def test_get_state_does_not_create_state_for_ineligible_account_without_existing_state(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
sqlite_session_factory: sessionmaker[Session],
) -> None:
_set_tour_config(monkeypatch, enabled=True, rollout_started_at=datetime(2026, 6, 1))
session = _FakeSession()
result = StepByStepTourService.get_state(
account=_account(initialized_at=datetime(2026, 5, 31)),
current_tenant_id="workspace-1",
session=session,
session=sqlite_session,
)
assert result == {
@@ -132,87 +95,96 @@ def test_get_state_does_not_create_state_for_ineligible_account_without_existing
"manually_disabled_workspace_ids": [],
"updated_at": None,
}
assert session.added == []
assert session.commit_count == 0
with sqlite_session_factory() as observer:
assert _load_state(observer) is None
def test_patch_state_persists_even_when_account_is_not_eligible(monkeypatch: pytest.MonkeyPatch) -> None:
def test_patch_state_persists_even_when_account_is_not_eligible(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
sqlite_session_factory: sessionmaker[Session],
) -> None:
_set_tour_config(monkeypatch, enabled=False, rollout_started_at=datetime(2026, 6, 1))
session = _FakeSession()
result = StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-2",
patch={"action": "enable_current_workspace"},
session=session,
session=sqlite_session,
)
assert result["skipped"] is False
assert result["manually_enabled_workspace_ids"] == ["workspace-2"]
assert result["manually_disabled_workspace_ids"] == []
assert len(session.added) == 1
assert session.commit_count == 1
with sqlite_session_factory() as observer:
persisted = _load_state(observer)
assert persisted is not None
assert persisted.manually_enabled_workspace_ids == ["workspace-2"]
def test_patch_state_skip_action_sets_skipped_and_removes_current_workspace_enable(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
) -> None:
_set_tour_config(monkeypatch, enabled=False, rollout_started_at=datetime(2026, 6, 1))
state = _state()
state.manually_enabled_workspace_ids = ["workspace-1", "workspace-2"]
session = _FakeSession(state=state)
_persist_state(sqlite_session, state)
result = StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-1",
patch={"action": "skip"},
session=session,
session=sqlite_session,
)
assert result["skipped"] is True
assert result["manually_enabled_workspace_ids"] == ["workspace-2"]
assert result["manually_disabled_workspace_ids"] == []
assert session.added == []
assert session.commit_count == 1
assert _load_state(sqlite_session) is state
def test_patch_state_disable_action_moves_current_workspace_to_disabled(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
) -> None:
_set_tour_config(monkeypatch, enabled=False, rollout_started_at=datetime(2026, 6, 1))
state = _state()
state.manually_enabled_workspace_ids = ["workspace-1", "workspace-2"]
session = _FakeSession(state=state)
_persist_state(sqlite_session, state)
result = StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-1",
patch={"action": "disable_current_workspace"},
session=session,
session=sqlite_session,
)
assert result["manually_enabled_workspace_ids"] == ["workspace-2"]
assert result["manually_disabled_workspace_ids"] == ["workspace-1"]
assert session.commit_count == 1
assert _load_state(sqlite_session) is state
def test_patch_state_complete_and_uncomplete_task(monkeypatch: pytest.MonkeyPatch) -> None:
def test_patch_state_complete_and_uncomplete_task(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
) -> None:
_set_tour_config(monkeypatch, enabled=False, rollout_started_at=datetime(2026, 6, 1))
state = _state()
state.completed_task_ids = ["home"]
session = _FakeSession(state=state)
_persist_state(sqlite_session, state)
StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-1",
patch={"action": "complete_task", "task_id": "studio"},
session=session,
session=sqlite_session,
)
result = StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-1",
patch={"action": "uncomplete_task", "task_id": "home"},
session=session,
session=sqlite_session,
)
assert result["completed_task_ids"] == ["studio"]
@@ -220,20 +192,36 @@ def test_patch_state_complete_and_uncomplete_task(monkeypatch: pytest.MonkeyPatc
def test_patch_state_recovers_when_concurrent_request_created_state(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
sqlite_session_factory: sessionmaker[Session],
) -> None:
_set_tour_config(monkeypatch, enabled=False, rollout_started_at=datetime(2026, 6, 1))
existing_state = _state()
existing_state.manually_enabled_workspace_ids = ["workspace-1"]
session = _RaceInsertSession(state_after_rollback=existing_state)
lifecycle_events: list[str] = []
@event.listens_for(sqlite_session, "before_flush", once=True)
def add_conflicting_pending_state(session: Session, _flush_context, _instances) -> None:
lifecycle_events.append("before_flush")
session.add(AccountStepByStepTourState(account_id="account-1"))
@event.listens_for(sqlite_session, "after_soft_rollback", once=True)
def persist_winning_request(_session: Session, _previous_transaction) -> None:
lifecycle_events.append("after_soft_rollback")
with sqlite_session_factory() as winner:
winner.add(existing_state)
winner.commit()
result = StepByStepTourService.patch_state(
account=_account(initialized_at=datetime(2026, 6, 28)),
current_tenant_id="workspace-2",
patch={"action": "enable_current_workspace"},
session=session,
session=sqlite_session,
)
assert result["manually_enabled_workspace_ids"] == ["workspace-1", "workspace-2"]
assert session.flush_count == 1
assert session.rollback_count == 1
assert session.commit_count == 1
assert lifecycle_events == ["before_flush", "after_soft_rollback"]
with sqlite_session_factory() as observer:
persisted = _load_state(observer)
assert persisted is not None
assert persisted.manually_enabled_workspace_ids == ["workspace-1", "workspace-2"]