test: migrate snippet service ORM models to SQLite (#40567)

This commit is contained in:
Asuka Minato
2026-08-20 05:12:30 +00:00
committed by GitHub
parent e27eb51f8a
commit 6e890524db
2 changed files with 104 additions and 69 deletions
@@ -1,5 +1,4 @@
import json
from contextlib import nullcontext
from types import SimpleNamespace
from unittest.mock import Mock
@@ -7,7 +6,9 @@ import pytest
from sqlalchemy.orm import Session, sessionmaker
from core.workflow.snippet_start import SNIPPET_VIRTUAL_START_NODE_ID
from models.workflow import Workflow, WorkflowKind, WorkflowType
from models.account import Account
from models.snippet import CustomizedSnippet, SnippetType
from models.workflow import Workflow, WorkflowKind, WorkflowNodeExecutionModel, WorkflowType
from services.snippet_generate_service import SnippetGenerateService
@@ -28,8 +29,22 @@ def _workflow(graph: dict) -> Workflow:
)
def _session_maker(session: object | None = None) -> Mock:
return Mock(return_value=nullcontext(session or Mock()))
def _snippet(*, input_fields: list[dict] | None = None) -> CustomizedSnippet:
return CustomizedSnippet(
id="snippet-1",
tenant_id="tenant-1",
name="Snippet",
description="",
type=SnippetType.NODE,
created_by="account-1",
input_fields=json.dumps(input_fields) if input_fields else None,
)
def _account(account_id: str = "user-1") -> Account:
account = Account(name="Test User", email=f"{account_id}@example.com")
account.id = account_id
return account
def test_filter_virtual_start_events_keeps_blocking_response_unchanged():
@@ -67,7 +82,7 @@ def test_is_virtual_start_event(message, expected):
def test_ensure_start_node_returns_workflow_when_start_already_exists():
workflow = _workflow({"nodes": [{"id": "start", "data": {"type": "start"}}], "edges": []})
snippet = SimpleNamespace(input_fields_list=[])
snippet = _snippet()
result = SnippetGenerateService._ensure_start_node(workflow, snippet)
@@ -83,8 +98,8 @@ def test_ensure_start_node_injects_virtual_start_for_root_candidates(monkeypatch
"edges": [{"source": "llm-1", "target": "answer-1"}],
}
workflow = _workflow(graph)
snippet = SimpleNamespace(
input_fields_list=[
snippet = _snippet(
input_fields=[
{
"variable": "query",
"label": "Query",
@@ -139,8 +154,8 @@ def test_generate_raises_when_draft_workflow_missing(monkeypatch: pytest.MonkeyP
with pytest.raises(ValueError, match="Workflow not initialized"):
SnippetGenerateService.generate(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
user=SimpleNamespace(id="user-1"),
snippet=_snippet(),
user=_account(),
args={"inputs": {}},
invoke_from="debugger",
)
@@ -148,8 +163,8 @@ def test_generate_raises_when_draft_workflow_missing(monkeypatch: pytest.MonkeyP
def test_generate_delegates_to_workflow_generator_and_filters_stream(monkeypatch: pytest.MonkeyPatch):
workflow = _workflow({"nodes": [{"id": "llm-1", "data": {"type": "llm"}}], "edges": []})
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1", input_fields_list=[])
user = SimpleNamespace(id="user-1")
snippet = _snippet()
user = _account()
raw_stream = iter(
[
{"event": "node_started", "data": {"node_id": SNIPPET_VIRTUAL_START_NODE_ID}},
@@ -189,8 +204,8 @@ def test_generate_delegates_to_workflow_generator_and_filters_stream(monkeypatch
def test_run_published_delegates_to_workflow_generator_non_streaming(monkeypatch: pytest.MonkeyPatch):
workflow = _workflow({"nodes": [{"id": "llm-1", "data": {"type": "llm"}}], "edges": []})
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1", input_fields_list=[])
user = SimpleNamespace(id="user-1")
snippet = _snippet()
user = _account()
generator = SimpleNamespace(generate=Mock(return_value={"data": {"outputs": {"answer": "ok"}}}))
monkeypatch.setattr(
@@ -219,7 +234,7 @@ def test_run_published_delegates_to_workflow_generator_non_streaming(monkeypatch
def test_ensure_start_node_for_worker_delegates(monkeypatch: pytest.MonkeyPatch):
workflow = _workflow({"nodes": [], "edges": []})
snippet = SimpleNamespace(input_fields_list=[])
snippet = _snippet()
ensure_start_node = Mock(return_value=workflow)
monkeypatch.setattr(SnippetGenerateService, "_ensure_start_node", ensure_start_node)
@@ -231,9 +246,9 @@ def test_ensure_start_node_for_worker_delegates(monkeypatch: pytest.MonkeyPatch)
def test_run_draft_node_delegates_to_workflow_service(monkeypatch: pytest.MonkeyPatch):
workflow = _workflow({"nodes": [{"id": "llm-1", "data": {"type": "llm"}}], "edges": []})
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
account = SimpleNamespace(id="account-1")
execution = SimpleNamespace(id="execution-1")
snippet = _snippet()
account = _account("account-1")
execution = WorkflowNodeExecutionModel(id="execution-1")
workflow_service = SimpleNamespace(run_draft_workflow_node=Mock(return_value=execution))
monkeypatch.setattr(
@@ -271,10 +286,10 @@ def test_run_draft_node_raises_when_draft_workflow_missing(monkeypatch: pytest.M
with pytest.raises(ValueError, match="Workflow not initialized"):
SnippetGenerateService.run_draft_node(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
snippet=_snippet(),
node_id="llm-1",
user_inputs={},
account=SimpleNamespace(id="account-1"),
account=_account("account-1"),
)
@@ -283,8 +298,8 @@ def test_generate_single_iteration_delegates_to_workflow_generator(
sqlite_session_factory: sessionmaker[Session],
) -> None:
workflow = _workflow({"nodes": [{"id": "iteration-1", "data": {"type": "iteration"}}], "edges": []})
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
user = SimpleNamespace(id="user-1")
snippet = _snippet()
user = _account()
response = iter(["event"])
generator = SimpleNamespace(single_iteration_generate=Mock(return_value=response))
workflow_generator_class = Mock(return_value=generator)
@@ -316,7 +331,9 @@ def test_generate_single_iteration_delegates_to_workflow_generator(
workflow_generator_class.convert_to_event_stream.assert_called_once_with(response)
def test_generate_single_iteration_raises_when_draft_workflow_missing(monkeypatch: pytest.MonkeyPatch):
def test_generate_single_iteration_raises_when_draft_workflow_missing(
monkeypatch: pytest.MonkeyPatch, unbound_session_factory: sessionmaker[Session]
):
monkeypatch.setattr(
"services.snippet_generate_service.SnippetService",
lambda *_args, **_kwargs: SimpleNamespace(get_draft_workflow=Mock(return_value=None)),
@@ -324,11 +341,11 @@ def test_generate_single_iteration_raises_when_draft_workflow_missing(monkeypatc
with pytest.raises(ValueError, match="Workflow not initialized"):
SnippetGenerateService.generate_single_iteration(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
user=SimpleNamespace(id="user-1"),
snippet=_snippet(),
user=_account(),
node_id="iteration-1",
args={"inputs": {}},
session_maker=_session_maker(),
session_maker=unbound_session_factory,
)
@@ -337,8 +354,8 @@ def test_generate_single_loop_delegates_to_workflow_generator(
sqlite_session_factory: sessionmaker[Session],
) -> None:
workflow = _workflow({"nodes": [{"id": "loop-1", "data": {"type": "loop"}}], "edges": []})
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
user = SimpleNamespace(id="user-1")
snippet = _snippet()
user = _account()
response = iter(["event"])
generator = SimpleNamespace(single_loop_generate=Mock(return_value=response))
workflow_generator_class = Mock(return_value=generator)
@@ -370,7 +387,9 @@ def test_generate_single_loop_delegates_to_workflow_generator(
workflow_generator_class.convert_to_event_stream.assert_called_once_with(response)
def test_generate_single_loop_raises_when_draft_workflow_missing(monkeypatch: pytest.MonkeyPatch):
def test_generate_single_loop_raises_when_draft_workflow_missing(
monkeypatch: pytest.MonkeyPatch, unbound_session_factory: sessionmaker[Session]
):
monkeypatch.setattr(
"services.snippet_generate_service.SnippetService",
lambda *_args, **_kwargs: SimpleNamespace(get_draft_workflow=Mock(return_value=None)),
@@ -378,11 +397,11 @@ def test_generate_single_loop_raises_when_draft_workflow_missing(monkeypatch: py
with pytest.raises(ValueError, match="Workflow not initialized"):
SnippetGenerateService.generate_single_loop(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
user=SimpleNamespace(id="user-1"),
snippet=_snippet(),
user=_account(),
node_id="loop-1",
args=SimpleNamespace(inputs={}),
session_maker=_session_maker(),
session_maker=unbound_session_factory,
)
@@ -394,8 +413,8 @@ def test_run_published_raises_when_published_workflow_missing(monkeypatch: pytes
with pytest.raises(ValueError, match="No published workflow found"):
SnippetGenerateService.run_published(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
user=SimpleNamespace(id="user-1"),
snippet=_snippet(),
user=_account(),
args={"inputs": {}},
invoke_from="service-api",
)
@@ -13,6 +13,7 @@ from enums import DeploymentEdition
from extensions.storage.storage_type import StorageType
from graphon.variables.segments import StringSegment
from graphon.variables.types import SegmentType
from models.account import Account
from models.agent import Agent, AgentScope, AgentSource, AgentStatus
from models.enums import CreatorUserRole
from models.model import UploadFile
@@ -22,6 +23,8 @@ from models.workflow import (
WorkflowDraftVariable,
WorkflowDraftVariableFile,
WorkflowKind,
WorkflowNodeExecutionModel,
WorkflowRun,
WorkflowType,
)
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
@@ -45,21 +48,29 @@ def _create_workflow(*, workflow_id: str, version: str, graph: dict, features: d
)
def _snippet() -> CustomizedSnippet:
return CustomizedSnippet(
id="snippet-1",
tenant_id="tenant-1",
name="Snippet",
description="",
type=SnippetType.NODE,
created_by="account-1",
)
def _snippet(**overrides) -> CustomizedSnippet:
values = {
"id": "snippet-1",
"tenant_id": "tenant-1",
"name": "Snippet",
"description": "",
"type": SnippetType.NODE,
"created_by": "account-1",
}
values.update(overrides)
return CustomizedSnippet(**values)
def _account(account_id: str = "account-1") -> Account:
account = Account(name="Test User", email=f"{account_id}@example.com")
account.id = account_id
return account
def test_create_snippet_allows_duplicate_names(
sqlite_session_factory: sessionmaker[Session], sqlite_session: Session
) -> None:
account = SimpleNamespace(id="account-1")
account = _account()
existing = _snippet()
existing.name = "shared name"
sqlite_session.add(existing)
@@ -208,7 +219,7 @@ def test_sync_draft_workflow_creates_draft_and_updates_input_fields(
service = SnippetService(session_maker=sqlite_session_factory)
monkeypatch.setattr(service, "get_draft_workflow", Mock(return_value=None))
snippet = _snippet()
account = SimpleNamespace(id="account-1")
account = _account()
workflow = service.sync_draft_workflow(
snippet=snippet,
@@ -233,14 +244,17 @@ def test_sync_draft_workflow_raises_when_hash_mismatches(
sqlite_session_factory: sessionmaker[Session],
) -> None:
service = SnippetService(session_maker=sqlite_session_factory)
service.get_draft_workflow = Mock(return_value=SimpleNamespace(unique_hash="server-hash"))
draft_workflow = _create_workflow(
workflow_id="workflow-1", version=Workflow.VERSION_DRAFT, graph={"nodes": []}, features={}
)
service.get_draft_workflow = Mock(return_value=draft_workflow)
with pytest.raises(WorkflowHashNotEqualError):
service.sync_draft_workflow(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
snippet=_snippet(),
graph={"nodes": [], "edges": []},
unique_hash="client-hash",
account=SimpleNamespace(id="account-1"),
account=_account(),
)
@@ -258,7 +272,7 @@ def test_sync_draft_workflow_updates_existing_draft_and_clears_variables(
)
unique_hash = workflow.unique_hash
snippet = _snippet()
account = SimpleNamespace(id="account-1")
account = _account()
monkeypatch.setattr(service, "get_draft_workflow", Mock(return_value=workflow))
result = service.sync_draft_workflow(
@@ -293,7 +307,7 @@ def test_update_workflow_updates_marked_fields(sqlite_session: Session) -> None:
snippet = _snippet()
sqlite_session.add_all([snippet, workflow])
sqlite_session.flush()
account = SimpleNamespace(id="account-1")
account = _account()
result = service.update_workflow(
session=sqlite_session,
@@ -318,9 +332,9 @@ def test_update_workflow_returns_none_when_missing(sqlite_session: Session) -> N
result = service.update_workflow(
session=sqlite_session,
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
snippet=_snippet(),
workflow_id="missing-workflow",
account=SimpleNamespace(id="account-1"),
account=_account(),
data={"marked_name": "v1"},
)
@@ -375,7 +389,7 @@ def test_restore_published_snippet_workflow_to_draft_copies_source_snapshot(
sqlite_session: Session,
) -> None:
snippet = _snippet()
account = SimpleNamespace(id="account-2")
account = _account("account-2")
source_graph = {"nodes": [{"id": "llm-1", "data": {"type": "llm"}}], "edges": []}
source_features = {"opening_statement": "hello"}
source_workflow = _create_workflow(
@@ -415,8 +429,8 @@ def test_restore_published_snippet_workflow_to_draft_raises_when_source_missing(
monkeypatch: pytest.MonkeyPatch,
sqlite_session_factory: sessionmaker[Session],
) -> None:
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
account = SimpleNamespace(id="account-2")
snippet = _snippet()
account = _account("account-2")
service = SnippetService(session_maker=sqlite_session_factory)
monkeypatch.setattr(service, "get_published_workflow_by_id", Mock(return_value=None))
@@ -435,7 +449,7 @@ def test_restore_published_snippet_workflow_to_draft_adds_new_draft(
sqlite_session: Session,
) -> None:
snippet = _snippet()
account = SimpleNamespace(id="account-2")
account = _account("account-2")
source_workflow = _create_workflow(
workflow_id="published-workflow",
version="2026-04-28 00:00:00",
@@ -471,7 +485,7 @@ def test_restore_published_snippet_workflow_to_draft_adds_new_draft(
def test_get_published_workflow_returns_none_without_workflow_id() -> None:
service = SnippetService.__new__(SnippetService)
result = service.get_published_workflow(SimpleNamespace(id="snippet-1", tenant_id="tenant-1", workflow_id=None))
result = service.get_published_workflow(_snippet())
assert result is None
@@ -488,7 +502,7 @@ def test_get_published_workflow_by_id_raises_for_draft(
with pytest.raises(IsDraftWorkflowError):
service.get_published_workflow_by_id(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
snippet=_snippet(),
workflow_id="workflow-1",
)
@@ -499,8 +513,8 @@ def test_publish_workflow_raises_when_draft_missing(sqlite_session: Session) ->
with pytest.raises(ValueError, match="No valid workflow found"):
service.publish_workflow(
session=sqlite_session,
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
account=SimpleNamespace(id="account-1"),
snippet=_snippet(),
account=_account(),
)
@@ -537,7 +551,7 @@ def test_publish_workflow_creates_snapshot_and_updates_snippet(
result, retirement_candidates = service.publish_workflow(
session=sqlite_session,
snippet=snippet,
account=SimpleNamespace(id="account-1"),
account=_account(),
)
assert result.kind == WorkflowKind.SNIPPET
@@ -556,7 +570,7 @@ def test_get_all_published_workflows_returns_empty_without_current_workflow(unbo
result = service.get_all_published_workflows(
session=unbound_session,
snippet=SimpleNamespace(id="snippet-1", workflow_id=None),
snippet=_snippet(),
page=1,
limit=20,
)
@@ -580,7 +594,7 @@ def test_get_all_published_workflows_paginates(sqlite_session: Session) -> None:
result, has_more = service.get_all_published_workflows(
session=sqlite_session,
snippet=SimpleNamespace(id="snippet-1", workflow_id="workflow-current"),
snippet=_snippet(workflow_id="workflow-current"),
page=1,
limit=2,
)
@@ -703,7 +717,7 @@ def test_delete_draft_variable_files_removes_storage_objects(
def test_delete_archived_workflow_run_files_removes_prefixed_objects(monkeypatch: pytest.MonkeyPatch) -> None:
from configs import dify_config
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
snippet = _snippet()
archive_storage = SimpleNamespace(
list_objects=Mock(return_value=["tenant-1/app_id=snippet-1/run.json"]),
delete_object=Mock(),
@@ -722,15 +736,15 @@ def test_workflow_run_queries_delegate_to_repositories(monkeypatch: pytest.Monke
service = SnippetService.__new__(SnippetService)
workflow_run_repo = SimpleNamespace(
get_paginated_workflow_runs=Mock(return_value=SimpleNamespace(data=[])),
get_workflow_run_by_id=Mock(return_value=SimpleNamespace(id="run-1")),
get_workflow_run_by_id=Mock(return_value=WorkflowRun(id="run-1")),
)
node_execution_repo = SimpleNamespace(
get_executions_by_workflow_run=Mock(return_value=[SimpleNamespace(id="node-execution-1")]),
get_node_last_execution=Mock(return_value=SimpleNamespace(id="last-run-1")),
get_executions_by_workflow_run=Mock(return_value=[WorkflowNodeExecutionModel(id="node-execution-1")]),
get_node_last_execution=Mock(return_value=WorkflowNodeExecutionModel(id="last-run-1")),
)
service._workflow_run_repo = workflow_run_repo
service._node_execution_service_repo = node_execution_repo
snippet = SimpleNamespace(id="snippet-1", tenant_id="tenant-1")
snippet = _snippet()
expected_traces = [SimpleNamespace(id="node-execution-1:retry:1"), SimpleNamespace(id="node-execution-1")]
mock_assemble = Mock(return_value=expected_traces)
monkeypatch.setattr("services.snippet_service.assemble_workflow_node_execution_traces", mock_assemble)
@@ -741,7 +755,9 @@ def test_workflow_run_queries_delegate_to_repositories(monkeypatch: pytest.Monke
assert (
service.get_snippet_node_last_run(
snippet=snippet,
workflow=SimpleNamespace(id="workflow-1"),
workflow=_create_workflow(
workflow_id="workflow-1", version=Workflow.VERSION_DRAFT, graph={"nodes": []}, features={}
),
node_id="llm-1",
).id
== "last-run-1"
@@ -774,7 +790,7 @@ def test_workflow_run_node_executions_returns_empty_when_run_missing() -> None:
service.get_snippet_workflow_run = Mock(return_value=None)
result = service.get_snippet_workflow_run_node_executions(
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
snippet=_snippet(),
run_id="missing-run",
)