mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 06:41:07 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
583 lines
24 KiB
Python
583 lines
24 KiB
Python
"""Trigger worker status transitions against real SQLite and graph events."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from contextvars import Context
|
|
from dataclasses import replace
|
|
from datetime import UTC, datetime, timedelta
|
|
from threading import Barrier
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from celery import Task
|
|
from flask import Flask
|
|
from redis import Redis
|
|
from sqlalchemy import Engine, delete, inspect, select
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
from sqlalchemy.pool import QueuePool
|
|
|
|
from core.app.workflow.file_runtime import create_dify_workflow_file_runtime
|
|
from core.workflow.system_variables import build_system_variables
|
|
from enums import DeploymentEdition
|
|
from extensions.ext_application_services import build_application_services
|
|
from extensions.ext_redis import RedisClientWrapper
|
|
from graphon.engine.command.builtin.in_memory import InMemoryChannel
|
|
from graphon.engine_events import (
|
|
EngineEvent,
|
|
GraphRunAbortedEvent,
|
|
GraphRunFailedEvent,
|
|
GraphRunPausedEvent,
|
|
GraphRunStartedEvent,
|
|
GraphRunSucceededEvent,
|
|
)
|
|
from graphon.file.runtime import use_workflow_file_runtime
|
|
from graphon.runtime import RuntimeState, VariablePool
|
|
from graphon.runtime.read_only_wrappers import ReadOnlyRuntimeStateWrapper
|
|
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
|
from models.enums import CreatorUserRole, EndUserType, WorkflowTriggerStatus
|
|
from models.model import App, AppMode, EndUser
|
|
from models.trigger import WorkflowTriggerLog
|
|
from models.workflow import Workflow
|
|
from repositories.sqlalchemy_workflow_trigger_log_repository import (
|
|
SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
)
|
|
from services.errors.app import WorkflowNotFoundError
|
|
from services.trigger.execution_gateway import WorkflowTriggerExecutionGateway
|
|
from services.trigger.execution_layer import TriggerPostLayer
|
|
from services.trigger.execution_service import TriggerExecutionService
|
|
from services.trigger.execution_store import TriggerExecution, TriggerExecutionRef
|
|
from services.workflow.entities import ScheduleTriggerData, TriggerActor
|
|
from tasks.async_workflow_tasks import execute_workflow_professional, execute_workflow_sandbox, execute_workflow_team
|
|
from tests.helpers.unit.model_factories import make_workflow
|
|
from tests.unit_tests.controllers.console.workspace import test_trigger_oauth_client as oauth_tests
|
|
from tests.unit_tests.controllers.console.workspace.test_trigger_oauth_client import _Harness
|
|
|
|
application_redis = oauth_tests.application_redis
|
|
harness = oauth_tests.harness
|
|
|
|
|
|
@pytest.fixture
|
|
def logs(sqlite_session_factory: sessionmaker[Session]) -> SQLAlchemyWorkflowTriggerExecutionRepository:
|
|
return SQLAlchemyWorkflowTriggerExecutionRepository(session_factory=sqlite_session_factory)
|
|
|
|
|
|
@pytest.fixture
|
|
def execution(sqlite_session_factory: sessionmaker[Session]) -> TriggerExecution:
|
|
tenant = Tenant(name="Trigger tenant")
|
|
account = Account(name="Owner", email="trigger-owner@example.com")
|
|
with sqlite_session_factory.begin() as session:
|
|
session.add_all([tenant, account])
|
|
session.flush()
|
|
app = App(
|
|
tenant_id=tenant.id,
|
|
name="Trigger app",
|
|
mode=AppMode.WORKFLOW,
|
|
enable_site=True,
|
|
enable_api=True,
|
|
max_active_requests=0,
|
|
)
|
|
session.add(app)
|
|
session.flush()
|
|
workflow = make_workflow(
|
|
workflow_id=str(uuid4()),
|
|
tenant_id=tenant.id,
|
|
app_id=app.id,
|
|
version="1",
|
|
graph='{"nodes": [], "edges": []}',
|
|
created_by=account.id,
|
|
)
|
|
session.add(workflow)
|
|
session.add(TenantAccountJoin(tenant_id=tenant.id, account_id=account.id, role=TenantAccountRole.OWNER))
|
|
app.workflow_id = workflow.id
|
|
data = ScheduleTriggerData(tenant_id=tenant.id, app_id=app.id, root_node_id="schedule", inputs={"text": "hello"})
|
|
actor = TriggerActor(id=account.id, role=CreatorUserRole.ACCOUNT)
|
|
log_id = SQLAlchemyWorkflowTriggerExecutionRepository(session_factory=sqlite_session_factory).create_pending(
|
|
actor=actor, trigger_data=data, workflow_id=workflow.id, queue="workflow_sandbox"
|
|
)
|
|
return TriggerExecution(
|
|
log=TriggerExecutionRef(id=log_id, tenant_id=tenant.id, app_id=app.id, queue_name="workflow_sandbox"),
|
|
workflow_id=workflow.id,
|
|
actor=actor,
|
|
data=data,
|
|
)
|
|
|
|
|
|
def _layer(logs: SQLAlchemyWorkflowTriggerExecutionRepository, execution: TriggerExecution) -> TriggerPostLayer:
|
|
layer = TriggerPostLayer(logs=logs, log=execution.log, start_time=datetime.now(UTC) - timedelta(seconds=10))
|
|
state = RuntimeState(
|
|
workflow_id=execution.workflow_id,
|
|
variable_pool=VariablePool.from_bootstrap(
|
|
system_variables=build_system_variables(workflow_execution_id="run-1")
|
|
),
|
|
start_at=0,
|
|
outputs={"answer": "ok", "number": 0},
|
|
)
|
|
layer.initialize(ReadOnlyRuntimeStateWrapper(state), InMemoryChannel())
|
|
return layer
|
|
|
|
|
|
@pytest.mark.parametrize("failure", [False, True])
|
|
def test_runtime_starts_after_status_commit_and_connection_release(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_engine: Engine,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
failure: bool,
|
|
) -> None:
|
|
invoked: list[TriggerExecution] = []
|
|
|
|
def run(value: TriggerExecution, started_at: datetime) -> None:
|
|
assert isinstance(sqlite_engine.pool, QueuePool)
|
|
assert sqlite_engine.pool.checkedout() == 0
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, value.log.id)
|
|
assert row is not None
|
|
assert row.status == WorkflowTriggerStatus.RUNNING
|
|
assert started_at <= datetime.now(UTC)
|
|
assert value.data.inputs == {"text": "hello"}
|
|
invoked.append(value)
|
|
if failure:
|
|
raise RuntimeError("provider execution failed")
|
|
_layer(logs, value).on_event(GraphRunSucceededEvent())
|
|
|
|
TriggerExecutionService(logs=logs, run=run).execute(execution.log.id)
|
|
assert len(invoked) == 1
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == (WorkflowTriggerStatus.FAILED if failure else WorkflowTriggerStatus.SUCCEEDED)
|
|
assert row.error == ("provider execution failed" if failure else None)
|
|
assert row.elapsed_time is not None
|
|
assert row.elapsed_time >= 0
|
|
assert row.finished_at is not None
|
|
|
|
|
|
def test_missing_log_does_not_invoke_runtime(logs: SQLAlchemyWorkflowTriggerExecutionRepository) -> None:
|
|
invoked: list[str] = []
|
|
TriggerExecutionService(logs=logs, run=lambda value, _at: invoked.append(value.log.id)).execute(str(uuid4()))
|
|
assert invoked == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event",
|
|
[None, GraphRunSucceededEvent(), GraphRunFailedEvent(error="failed"), GraphRunPausedEvent(reasons=[])],
|
|
)
|
|
def test_late_publication_receipt_preserves_worker_progress(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
event: EngineEvent | None,
|
|
) -> None:
|
|
# A worker can consume the published task before the producer records its receipt.
|
|
assert logs.start(execution.log.id) is not None
|
|
if event is not None:
|
|
_layer(logs, execution).on_event(event)
|
|
with sqlite_session_factory() as session:
|
|
before = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert before is not None
|
|
logs.mark_queued(
|
|
tenant_id=execution.log.tenant_id,
|
|
app_id=execution.log.app_id,
|
|
log_id=execution.log.id,
|
|
task_id="published-task",
|
|
)
|
|
with sqlite_session_factory() as session:
|
|
after = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert after is not None
|
|
assert after.status == before.status
|
|
assert after.workflow_run_id == before.workflow_run_id
|
|
assert after.outputs == before.outputs
|
|
assert after.elapsed_time == before.elapsed_time
|
|
assert after.finished_at == before.finished_at
|
|
assert after.celery_task_id == "published-task"
|
|
assert after.triggered_at is not None
|
|
|
|
|
|
@pytest.mark.parametrize("queued", [False, True])
|
|
def test_concurrent_workers_claim_execution_only_once(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
queued: bool,
|
|
) -> None:
|
|
if queued:
|
|
logs.mark_queued(
|
|
tenant_id=execution.log.tenant_id, app_id=execution.log.app_id, log_id=execution.log.id, task_id="task"
|
|
)
|
|
ready = Barrier(2)
|
|
|
|
def start() -> TriggerExecution | None:
|
|
ready.wait(timeout=5)
|
|
return logs.start(execution.log.id)
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as workers:
|
|
attempts = [workers.submit(start) for _ in range(2)]
|
|
claims = [attempt.result(timeout=10) for attempt in attempts]
|
|
assert sum(claim is not None for claim in claims) == 1
|
|
claimed = next(claim for claim in claims if claim is not None)
|
|
assert claimed.log == execution.log
|
|
assert claimed.actor == execution.actor
|
|
assert claimed.workflow_id == execution.workflow_id
|
|
assert claimed.data.model_dump() == execution.data.model_dump()
|
|
assert logs.start(execution.log.id) is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"status",
|
|
[
|
|
status
|
|
for status in WorkflowTriggerStatus
|
|
if status not in (WorkflowTriggerStatus.PENDING, WorkflowTriggerStatus.QUEUED)
|
|
],
|
|
)
|
|
def test_redelivery_of_claimed_or_finished_log_does_not_invoke_runtime(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
status: WorkflowTriggerStatus,
|
|
) -> None:
|
|
with sqlite_session_factory.begin() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
row.status = status
|
|
invoked: list[str] = []
|
|
TriggerExecutionService(logs=logs, run=lambda value, _at: invoked.append(value.log.id)).execute(execution.log.id)
|
|
assert invoked == []
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == status
|
|
|
|
|
|
@pytest.mark.parametrize("event", [GraphRunSucceededEvent(), GraphRunAbortedEvent(reason="original failure")])
|
|
def test_terminal_result_survives_late_failure_and_duplicate_completion(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
event: EngineEvent,
|
|
) -> None:
|
|
assert logs.start(execution.log.id) is not None
|
|
layer = _layer(logs, execution)
|
|
layer.on_event(event)
|
|
with sqlite_session_factory() as session:
|
|
before = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert before is not None
|
|
logs.fail(log=execution.log, error="late worker failure", finished_at=datetime.now(UTC), elapsed_time=999)
|
|
logs.mark_failed(
|
|
tenant_id=execution.log.tenant_id,
|
|
app_id=execution.log.app_id,
|
|
log_id=execution.log.id,
|
|
status=WorkflowTriggerStatus.FAILED,
|
|
error="late producer failure",
|
|
)
|
|
layer.on_event(GraphRunSucceededEvent())
|
|
with sqlite_session_factory() as session:
|
|
after = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert after is not None
|
|
assert after.status == before.status
|
|
assert after.error == before.error
|
|
assert after.finished_at == before.finished_at
|
|
assert after.elapsed_time == before.elapsed_time
|
|
|
|
|
|
@pytest.mark.parametrize("queued", [False, True])
|
|
def test_worker_cannot_finish_or_fail_before_claiming_execution(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
queued: bool,
|
|
) -> None:
|
|
if queued:
|
|
logs.mark_queued(
|
|
tenant_id=execution.log.tenant_id, app_id=execution.log.app_id, log_id=execution.log.id, task_id="task"
|
|
)
|
|
_layer(logs, execution).on_event(GraphRunSucceededEvent())
|
|
logs.fail(log=execution.log, error="unclaimed", finished_at=datetime.now(UTC), elapsed_time=1)
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == (WorkflowTriggerStatus.QUEUED if queued else WorkflowTriggerStatus.PENDING)
|
|
assert row.error is None
|
|
assert row.workflow_run_id is None
|
|
assert row.finished_at is None
|
|
|
|
|
|
def test_late_producer_failure_does_not_stop_running_worker(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
) -> None:
|
|
assert logs.start(execution.log.id) is not None
|
|
logs.mark_failed(
|
|
tenant_id=execution.log.tenant_id,
|
|
app_id=execution.log.app_id,
|
|
log_id=execution.log.id,
|
|
status=WorkflowTriggerStatus.FAILED,
|
|
error="broker reply lost",
|
|
)
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == WorkflowTriggerStatus.RUNNING
|
|
assert row.error is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("event", "status", "error"),
|
|
[
|
|
(GraphRunSucceededEvent(), WorkflowTriggerStatus.SUCCEEDED, None),
|
|
(GraphRunFailedEvent(error="runtime failed"), WorkflowTriggerStatus.FAILED, None),
|
|
(GraphRunPausedEvent(reasons=[]), WorkflowTriggerStatus.PAUSED, None),
|
|
(GraphRunAbortedEvent(reason="timeout"), WorkflowTriggerStatus.FAILED, "timeout"),
|
|
(GraphRunAbortedEvent(reason=""), WorkflowTriggerStatus.FAILED, "Workflow execution aborted"),
|
|
],
|
|
)
|
|
def test_terminal_event_preserves_output_status_and_accumulated_elapsed_time(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
event: EngineEvent,
|
|
status: WorkflowTriggerStatus,
|
|
error: str | None,
|
|
) -> None:
|
|
assert logs.start(execution.log.id) is not None
|
|
with sqlite_session_factory.begin() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
row.elapsed_time = 5
|
|
_layer(logs, execution).on_event(event)
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == status
|
|
assert row.error == error
|
|
assert row.workflow_run_id == "run-1"
|
|
assert json.loads(row.outputs or "{}") == {"answer": "ok", "number": 0}
|
|
assert row.total_tokens == 0
|
|
assert row.elapsed_time is not None
|
|
assert row.elapsed_time >= 15
|
|
assert row.finished_at is not None
|
|
|
|
|
|
def test_pause_then_finish_only_adds_new_elapsed_time(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
) -> None:
|
|
assert logs.start(execution.log.id) is not None
|
|
layer = _layer(logs, execution)
|
|
layer.on_event(GraphRunPausedEvent(reasons=[]))
|
|
with sqlite_session_factory() as session:
|
|
first = session.scalar(select(WorkflowTriggerLog.elapsed_time).where(WorkflowTriggerLog.id == execution.log.id))
|
|
resumed_at = datetime.now(UTC)
|
|
layer.on_event(GraphRunSucceededEvent())
|
|
with sqlite_session_factory() as session:
|
|
second = session.scalar(
|
|
select(WorkflowTriggerLog.elapsed_time).where(WorkflowTriggerLog.id == execution.log.id)
|
|
)
|
|
assert first is not None
|
|
assert second is not None
|
|
assert first <= second <= first + (datetime.now(UTC) - resumed_at).total_seconds() + 1
|
|
|
|
|
|
@pytest.mark.parametrize("scope", ["tenant_id", "app_id"])
|
|
def test_finish_fail_and_resume_query_are_owner_scoped(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
scope: str,
|
|
) -> None:
|
|
assert logs.start(execution.log.id) is not None
|
|
foreign = replace(execution.log, **{scope: str(uuid4())})
|
|
_layer(logs, replace(execution, log=foreign)).on_event(GraphRunSucceededEvent())
|
|
logs.fail(log=foreign, error="foreign", finished_at=datetime.now(UTC), elapsed_time=1)
|
|
with sqlite_session_factory.begin() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == WorkflowTriggerStatus.RUNNING
|
|
row.workflow_run_id = "run-1"
|
|
assert logs.find_for_run(tenant_id=foreign.tenant_id, app_id=foreign.app_id, workflow_run_id="run-1") is None
|
|
assert (
|
|
logs.find_for_run(tenant_id=execution.log.tenant_id, app_id=execution.log.app_id, workflow_run_id="run-1")
|
|
== execution.log
|
|
)
|
|
|
|
|
|
def test_nonterminal_events_do_not_write(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
) -> None:
|
|
_layer(logs, execution).on_event(GraphRunStartedEvent())
|
|
with sqlite_session_factory() as session:
|
|
row = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert row is not None
|
|
assert row.status == WorkflowTriggerStatus.PENDING
|
|
|
|
|
|
@pytest.mark.parametrize("actor_role", [CreatorUserRole.ACCOUNT, CreatorUserRole.END_USER])
|
|
def test_legacy_models_are_loaded_and_detached_before_generator_entry(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
sqlite_engine: Engine,
|
|
actor_role: CreatorUserRole,
|
|
) -> None:
|
|
if actor_role == CreatorUserRole.END_USER:
|
|
with sqlite_session_factory.begin() as session:
|
|
actor = EndUser(
|
|
tenant_id=execution.log.tenant_id,
|
|
app_id=execution.log.app_id,
|
|
session_id=str(uuid4()),
|
|
type=EndUserType.TRIGGER,
|
|
)
|
|
session.add(actor)
|
|
session.flush()
|
|
execution = replace(execution, actor=TriggerActor(id=actor.id, role=actor_role))
|
|
app, workflow, user = logs.get_execution_models(execution)
|
|
assert isinstance(sqlite_engine.pool, QueuePool)
|
|
assert sqlite_engine.pool.checkedout() == 0
|
|
assert all(inspect(model).detached for model in (app, workflow, user))
|
|
assert app.tenant_id == execution.log.tenant_id
|
|
assert workflow.graph_dict == {"nodes": [], "edges": []}
|
|
assert workflow.features_dict == {}
|
|
assert user.id == execution.actor.id
|
|
if isinstance(user, Account):
|
|
assert user.current_tenant_id == execution.log.tenant_id
|
|
assert user.role == TenantAccountRole.OWNER
|
|
|
|
|
|
@pytest.mark.parametrize("broken", ["app-tenant", "workflow-app", "workflow-tenant", "tenant", "membership", "account"])
|
|
def test_runtime_hydration_rejects_broken_owner_chain(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
broken: str,
|
|
) -> None:
|
|
with sqlite_session_factory.begin() as session:
|
|
app = session.get(App, execution.log.app_id)
|
|
workflow = session.get(Workflow, execution.workflow_id)
|
|
assert app is not None
|
|
assert workflow is not None
|
|
if broken == "app-tenant":
|
|
app.tenant_id = str(uuid4())
|
|
elif broken == "workflow-app":
|
|
workflow.app_id = str(uuid4())
|
|
elif broken == "workflow-tenant":
|
|
workflow.tenant_id = str(uuid4())
|
|
elif broken == "tenant":
|
|
session.execute(delete(Tenant).where(Tenant.id == execution.log.tenant_id))
|
|
elif broken == "membership":
|
|
session.execute(delete(TenantAccountJoin).where(TenantAccountJoin.account_id == execution.actor.id))
|
|
else:
|
|
session.execute(delete(Account).where(Account.id == execution.actor.id))
|
|
with pytest.raises(WorkflowNotFoundError):
|
|
logs.get_execution_models(execution)
|
|
|
|
|
|
@pytest.mark.parametrize("foreign", ["tenant", "app", "missing"])
|
|
def test_end_user_runtime_hydration_is_scoped(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
foreign: str,
|
|
) -> None:
|
|
with sqlite_session_factory.begin() as session:
|
|
user = EndUser(
|
|
tenant_id=str(uuid4()) if foreign == "tenant" else execution.log.tenant_id,
|
|
app_id=str(uuid4()) if foreign == "app" else execution.log.app_id,
|
|
session_id=str(uuid4()),
|
|
type=EndUserType.TRIGGER,
|
|
)
|
|
session.add(user)
|
|
session.flush()
|
|
actor = TriggerActor(id=str(uuid4()) if foreign == "missing" else user.id, role=CreatorUserRole.END_USER)
|
|
with pytest.raises(WorkflowNotFoundError):
|
|
logs.get_execution_models(replace(execution, actor=actor))
|
|
|
|
|
|
@pytest.mark.parametrize("task", [execute_workflow_professional, execute_workflow_team, execute_workflow_sandbox])
|
|
def test_each_worker_entrypoint_uses_injected_execution_service(
|
|
app: Flask,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
task: Task[[dict[str, object]], None],
|
|
) -> None:
|
|
calls: list[TriggerExecution] = []
|
|
service = TriggerExecutionService(logs=logs, run=lambda value, _at: calls.append(value))
|
|
redis = RedisClientWrapper()
|
|
client = Redis()
|
|
redis.initialize(client)
|
|
services = build_application_services(
|
|
database_client=sqlite_session_factory,
|
|
deployment_edition=DeploymentEdition.COMMUNITY,
|
|
initialization_password="",
|
|
redis=redis,
|
|
)
|
|
previous = app.extensions.get("application_services")
|
|
app.extensions["application_services"] = replace(services, triggers=replace(services.triggers, executions=service))
|
|
try:
|
|
task.run({"workflow_trigger_log_id": execution.log.id})
|
|
finally:
|
|
client.close()
|
|
if previous is None:
|
|
app.extensions.pop("application_services")
|
|
else:
|
|
app.extensions["application_services"] = previous
|
|
assert len(calls) == 1
|
|
assert calls[0].log == execution.log
|
|
|
|
|
|
@pytest.mark.parametrize("actor_role", [CreatorUserRole.ACCOUNT, CreatorUserRole.END_USER])
|
|
def test_real_generator_runs_with_detached_models(
|
|
logs: SQLAlchemyWorkflowTriggerExecutionRepository,
|
|
execution: TriggerExecution,
|
|
sqlite_session_factory: sessionmaker[Session],
|
|
harness: _Harness,
|
|
actor_role: CreatorUserRole,
|
|
) -> None:
|
|
with sqlite_session_factory.begin() as session:
|
|
workflow = session.get(Workflow, execution.workflow_id)
|
|
assert workflow is not None
|
|
if actor_role == CreatorUserRole.END_USER:
|
|
user = EndUser(
|
|
tenant_id=execution.log.tenant_id,
|
|
app_id=execution.log.app_id,
|
|
type=EndUserType.TRIGGER,
|
|
session_id=str(uuid4()),
|
|
)
|
|
session.add(user)
|
|
session.flush()
|
|
log = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert log is not None
|
|
log.created_by = user.id
|
|
log.created_by_role = actor_role
|
|
execution = replace(execution, actor=TriggerActor(id=user.id, role=actor_role))
|
|
workflow.graph = json.dumps(
|
|
{
|
|
"nodes": [
|
|
{"id": "start", "data": {"type": "start", "title": "Start", "variables": []}},
|
|
{"id": "end", "data": {"type": "end", "title": "End", "outputs": []}},
|
|
],
|
|
"edges": [{"id": "start-end", "source": "start", "target": "end"}],
|
|
}
|
|
)
|
|
execution = replace(execution, data=execution.data.model_copy(update={"root_node_id": "start", "inputs": {}}))
|
|
assert logs.start(execution.log.id) is not None
|
|
gateway = WorkflowTriggerExecutionGateway(models=logs, logs=logs, session_factory=sqlite_session_factory)
|
|
|
|
def run() -> None:
|
|
with harness.app.app_context(), use_workflow_file_runtime(create_dify_workflow_file_runtime()):
|
|
gateway.run(execution, datetime.now(UTC))
|
|
|
|
Context().run(run)
|
|
with sqlite_session_factory() as session:
|
|
log = session.get(WorkflowTriggerLog, execution.log.id)
|
|
assert log is not None
|
|
assert log.status == WorkflowTriggerStatus.SUCCEEDED
|
|
assert log.workflow_run_id is not None
|
|
assert log.outputs == "{}"
|
|
assert all(not session.in_transaction() and not session.identity_map for session in harness.metadata.sessions)
|