mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 04:32:22 +02:00
69 lines
2.9 KiB
Python
69 lines
2.9 KiB
Python
"""A workflow the user deleted or switched off must not run from ANY path.
|
|
|
|
Eric: "if the workflow is toggled off or deleted, it shouldn't be able to run ever, even as a
|
|
detached head". Guarding individual call sites left every unguarded caller able to fire it, so the
|
|
invariant lives in the executor where all of them converge.
|
|
"""
|
|
|
|
import asyncio
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from backend.apps.workflows import executor
|
|
from backend.apps.workflows.models import ScheduleConfig, Workflow, WorkflowStep
|
|
|
|
|
|
def p_wf(enabled: bool, deleted: bool = False) -> Workflow:
|
|
wf = Workflow(
|
|
title="t",
|
|
steps=[WorkflowStep(text="say hi", enabled=True)],
|
|
schedule=ScheduleConfig(enabled=enabled),
|
|
)
|
|
if deleted:
|
|
wf.deleted_at = datetime.now()
|
|
return wf
|
|
|
|
|
|
@pytest.mark.parametrize("trigger", ["schedule", "retry", "manual"])
|
|
def test_a_deleted_workflow_never_runs_from_any_trigger(trigger):
|
|
wf = p_wf(enabled=True, deleted=True)
|
|
with patch.object(executor.storage, "get_workflow", return_value=wf):
|
|
run = asyncio.run(executor.execute(wf, triggered_by=trigger))
|
|
assert run.status == "skipped"
|
|
assert run.error == "Workflow deleted"
|
|
|
|
|
|
@pytest.mark.parametrize("trigger", ["schedule", "retry"])
|
|
def test_a_paused_workflow_never_runs_from_background_triggers(trigger):
|
|
"""Off means off for every BACKGROUND path: the scheduler and retries stay locked out."""
|
|
wf = p_wf(enabled=False)
|
|
with patch.object(executor.storage, "get_workflow", return_value=wf):
|
|
run = asyncio.run(executor.execute(wf, triggered_by=trigger))
|
|
assert run.status == "skipped"
|
|
assert run.error == "Workflow is paused"
|
|
|
|
|
|
def test_a_human_pressing_run_gets_past_the_pause_gate():
|
|
"""A fresh workflow's schedule defaults to off, and the Run button silently skipping made the
|
|
whole builder look broken (Eric's repro, 2026-08-17). Manual = the user IS the trigger; the
|
|
zero cost cap proves passage: the run dies at the CAP check, which sits AFTER the pause gate."""
|
|
wf = p_wf(enabled=False)
|
|
wf.cost_cap_usd_monthly = 0.0
|
|
with patch.object(executor.storage, "get_workflow", return_value=wf):
|
|
with patch.object(executor.storage, "record_run"):
|
|
with patch.object(executor, "_monthly_spend_so_far", return_value=0.0):
|
|
run = asyncio.run(executor.execute(wf, triggered_by="manual"))
|
|
assert run.error != "Workflow is paused"
|
|
assert "cost cap" in (run.error or "")
|
|
|
|
|
|
def test_turning_it_back_on_lets_it_run_again():
|
|
"""The guard must be about state, not a permanent block."""
|
|
wf = p_wf(enabled=True)
|
|
with patch.object(executor.storage, "get_workflow", return_value=wf):
|
|
with patch.object(executor.storage, "record_run"):
|
|
with patch.object(executor, "_monthly_spend_so_far", return_value=0.0):
|
|
assert executor.storage.get_workflow(wf.id).schedule.enabled is True
|