a2da043cbd
CI / lint (pull_request) Successful in 25s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 21s
CI / typecheck (pull_request) Successful in 39s
CI / security (pull_request) Successful in 37s
CI / build (pull_request) Successful in 26s
CI / integration_tests (pull_request) Successful in 4m43s
CI / unit_tests (pull_request) Successful in 11m2s
CI / docker (pull_request) Successful in 58s
CI / benchmark-regression (pull_request) Successful in 18m58s
CI / coverage (pull_request) Successful in 24m24s
Adds PlanExecutionContext carrying plan metadata and delegating changeset ops to ChangeSetStore. RuntimeExecuteResult captures execution output (changeset_id, tool_call_count, sandbox_refs, decision_ids_processed, execution_duration_ms). RuntimeExecuteActor dispatches StrategyDecision lists through ToolRunner with full changeset capture and optional streaming callbacks. PlanExecutor gains execution_context param with has_runtime / changeset_store / execution_context properties and _run_execute_with_runtime / _run_execute_with_stub split. 31 Behave scenarios, 5 Robot smoke tests, ASV benchmark suite, and reference documentation. Ref: Day-14 Rebaseline – M1.2 Plan-execute runtime wiring [Jeff]
590 lines
17 KiB
Python
590 lines
17 KiB
Python
"""Step definitions for plan execute runtime integration tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from pydantic import ValidationError as PydanticValidationError
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.plan_execution_context import (
|
|
PlanExecutionContext,
|
|
RuntimeExecuteActor,
|
|
RuntimeExecuteResult,
|
|
)
|
|
from cleveragents.application.services.plan_executor import (
|
|
PlanExecutor,
|
|
StrategyDecision,
|
|
)
|
|
from cleveragents.core.exceptions import PlanError, ValidationError
|
|
from cleveragents.domain.models.core.change import (
|
|
ChangeEntry,
|
|
ChangeOperation,
|
|
InMemoryChangeSetStore,
|
|
)
|
|
from cleveragents.tool.registry import ToolRegistry
|
|
from cleveragents.tool.runner import ToolRunner
|
|
|
|
|
|
def _runner() -> ToolRunner:
|
|
return ToolRunner(registry=ToolRegistry())
|
|
|
|
|
|
def _make_decision_root_id() -> str:
|
|
return str(ULID())
|
|
|
|
|
|
def _mock_lifecycle() -> MagicMock:
|
|
return MagicMock()
|
|
|
|
|
|
def _entry(
|
|
plan_id: str, tool: str = "test/tool", path: str = "src/t.py"
|
|
) -> ChangeEntry:
|
|
return ChangeEntry(
|
|
plan_id=plan_id,
|
|
resource_id=str(ULID()),
|
|
tool_name=tool,
|
|
operation=ChangeOperation.MODIFY,
|
|
path=path,
|
|
)
|
|
|
|
|
|
def _make_decisions(count: int) -> list[StrategyDecision]:
|
|
root_id = str(ULID())
|
|
decisions = []
|
|
for i in range(count):
|
|
decisions.append(
|
|
StrategyDecision(
|
|
decision_id=root_id if i == 0 else str(ULID()),
|
|
step_text=f"Step {i + 1}",
|
|
sequence=i,
|
|
parent_id=root_id if i > 0 else None,
|
|
)
|
|
)
|
|
return decisions
|
|
|
|
|
|
def _make_ctx(plan_id: str, **kwargs: Any) -> PlanExecutionContext:
|
|
"""Create a PlanExecutionContext with required fields."""
|
|
defaults: dict[str, Any] = {"plan_id": plan_id}
|
|
defaults.update(kwargs)
|
|
return PlanExecutionContext(**defaults)
|
|
|
|
|
|
# -- Background --
|
|
|
|
|
|
@given("a fresh in-memory changeset store")
|
|
def step_bg_store(ctx: Any) -> None:
|
|
ctx.changeset_store = InMemoryChangeSetStore()
|
|
|
|
|
|
@given("a valid plan ID")
|
|
def step_bg_plan_id(ctx: Any) -> None:
|
|
ctx.plan_id = str(ULID())
|
|
|
|
|
|
# -- Context creation --
|
|
|
|
|
|
@when("I create a PlanExecutionContext with a valid plan_id")
|
|
def step_ctx_valid(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id)
|
|
|
|
|
|
@when("I create a PlanExecutionContext with all optional fields")
|
|
def step_ctx_all(ctx: Any) -> None:
|
|
ctx.decision_root_id = str(ULID())
|
|
ctx.sandbox_root = "/tmp/test-sandbox"
|
|
ctx.automation_profile = "trusted"
|
|
ctx.exec_ctx = PlanExecutionContext(
|
|
plan_id=ctx.plan_id,
|
|
decision_root_id=ctx.decision_root_id,
|
|
sandbox_root=ctx.sandbox_root,
|
|
automation_profile=ctx.automation_profile,
|
|
project_resources={"repo": {"path": "/code"}},
|
|
resource_bindings={},
|
|
changeset_store=ctx.changeset_store,
|
|
)
|
|
|
|
|
|
@when("I attempt to create a PlanExecutionContext with empty plan_id")
|
|
def step_ctx_empty(ctx: Any) -> None:
|
|
try:
|
|
PlanExecutionContext(plan_id="")
|
|
ctx.error = None
|
|
except ValidationError as e:
|
|
ctx.error = e
|
|
|
|
|
|
@when("I attempt to create a PlanExecutionContext with None plan_id")
|
|
def step_ctx_none(ctx: Any) -> None:
|
|
try:
|
|
PlanExecutionContext(plan_id=None)
|
|
ctx.error = None
|
|
except (ValidationError, TypeError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@when("I create a PlanExecutionContext without specifying a changeset store")
|
|
def step_ctx_no_store(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id)
|
|
|
|
|
|
@when("I create a PlanExecutionContext with a custom changeset store")
|
|
def step_ctx_custom_store(ctx: Any) -> None:
|
|
ctx.custom_store = InMemoryChangeSetStore()
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.custom_store)
|
|
|
|
|
|
# -- Context assertions --
|
|
|
|
|
|
@then("the context plan_id should match the provided value")
|
|
def step_a_plan_id(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.plan_id == ctx.plan_id
|
|
|
|
|
|
@then("the context should have the correct decision_root_id")
|
|
def step_a_drid(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.decision_root_id == ctx.decision_root_id
|
|
|
|
|
|
@then("the context should have the correct sandbox_root")
|
|
def step_a_sandbox(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.sandbox_root == ctx.sandbox_root
|
|
|
|
|
|
@then("the context should have the correct automation_profile")
|
|
def step_a_profile(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.automation_profile == ctx.automation_profile
|
|
|
|
|
|
@then('the plan execution context should raise a ValidationError containing "{text}"')
|
|
def step_a_valerr(ctx: Any, text: str) -> None:
|
|
assert ctx.error is not None, "Expected error"
|
|
assert text in str(ctx.error), f"'{text}' not in '{ctx.error}'"
|
|
|
|
|
|
@then("the plan execution context should raise a ValidationError")
|
|
def step_a_valerr2(ctx: Any) -> None:
|
|
assert ctx.error is not None
|
|
|
|
|
|
@then("the context changeset_store should be an InMemoryChangeSetStore")
|
|
def step_a_default_store(ctx: Any) -> None:
|
|
assert isinstance(ctx.exec_ctx.changeset_store, InMemoryChangeSetStore)
|
|
|
|
|
|
@then("the context changeset_store should be the provided store")
|
|
def step_a_custom_store(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.changeset_store is ctx.custom_store
|
|
|
|
|
|
@then("the context project_resources should be an empty dict")
|
|
def step_a_empty_res(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.project_resources == {}
|
|
|
|
|
|
@then("the context resource_bindings should be an empty dict")
|
|
def step_a_empty_bind(ctx: Any) -> None:
|
|
assert ctx.exec_ctx.resource_bindings == {}
|
|
|
|
|
|
# -- Changeset operations --
|
|
|
|
|
|
@given("a PlanExecutionContext")
|
|
def step_g_ctx(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.changeset_store)
|
|
|
|
|
|
@given('a PlanExecutionContext with sandbox_root "{path}"')
|
|
def step_g_ctx_sandbox(ctx: Any, path: str) -> None:
|
|
ctx.exec_ctx = _make_ctx(
|
|
ctx.plan_id,
|
|
sandbox_root=path,
|
|
changeset_store=ctx.changeset_store,
|
|
)
|
|
|
|
|
|
@given("a PlanExecutionContext with an active changeset")
|
|
def step_g_ctx_cs(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.changeset_store)
|
|
ctx.active_cs_id = ctx.exec_ctx.start_changeset()
|
|
|
|
|
|
@given("a PlanExecutionContext without an active changeset")
|
|
def step_g_ctx_no_cs(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.changeset_store)
|
|
|
|
|
|
@given("a PlanExecutionContext with an active changeset and one recorded entry")
|
|
def step_g_ctx_entry(ctx: Any) -> None:
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.changeset_store)
|
|
ctx.active_cs_id = ctx.exec_ctx.start_changeset()
|
|
ctx.test_entry = _entry(ctx.plan_id)
|
|
ctx.exec_ctx.record_change(ctx.test_entry)
|
|
|
|
|
|
@when("I call start_changeset")
|
|
def step_w_start_cs(ctx: Any) -> None:
|
|
ctx.changeset_id_result = ctx.exec_ctx.start_changeset()
|
|
|
|
|
|
@when("I record a ChangeEntry into the context")
|
|
def step_w_record(ctx: Any) -> None:
|
|
ctx.test_entry = _entry(ctx.plan_id, path="src/new.py")
|
|
ctx.exec_ctx.record_change(ctx.test_entry)
|
|
|
|
|
|
@when("I attempt to record a ChangeEntry")
|
|
def step_w_record_fail(ctx: Any) -> None:
|
|
try:
|
|
ctx.exec_ctx.record_change(_entry(ctx.plan_id))
|
|
ctx.error = None
|
|
except (PlanError, ValidationError, KeyError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@when("I record three ChangeEntry instances")
|
|
def step_w_record3(ctx: Any) -> None:
|
|
for i in range(3):
|
|
ctx.exec_ctx.record_change(_entry(ctx.plan_id, path=f"src/f{i}.py"))
|
|
|
|
|
|
@when("I call get_changeset with the active changeset_id")
|
|
def step_w_get_cs(ctx: Any) -> None:
|
|
ctx.retrieved_cs = ctx.exec_ctx.get_changeset(ctx.active_cs_id)
|
|
|
|
|
|
@when("I call get_changeset with a nonexistent ID")
|
|
def step_w_get_cs_miss(ctx: Any) -> None:
|
|
ctx.retrieved_cs = ctx.exec_ctx.get_changeset("nonexistent-id")
|
|
|
|
|
|
@then("the returned changeset_id should be a valid ULID string")
|
|
def step_a_ulid(ctx: Any) -> None:
|
|
cid = ctx.changeset_id_result
|
|
assert cid and len(cid) == 26
|
|
|
|
|
|
@then("the active_changeset_ids should contain the new ID")
|
|
def step_a_active_ids(ctx: Any) -> None:
|
|
assert ctx.changeset_id_result in ctx.exec_ctx.active_changeset_ids
|
|
|
|
|
|
@then("the changeset should contain the recorded entry")
|
|
def step_a_entry_in(ctx: Any) -> None:
|
|
cs = ctx.exec_ctx.get_changeset(ctx.active_cs_id)
|
|
assert cs is not None
|
|
assert any(e.entry_id == ctx.test_entry.entry_id for e in cs.entries)
|
|
|
|
|
|
@then("a PlanError should be raised about no active changeset")
|
|
def step_a_plan_err(ctx: Any) -> None:
|
|
assert ctx.error is not None, "Expected error about no changeset"
|
|
|
|
|
|
@then("the changeset should contain exactly three entries")
|
|
def step_a_three(ctx: Any) -> None:
|
|
cs = ctx.exec_ctx.get_changeset(ctx.active_cs_id)
|
|
assert cs and len(cs.entries) == 3
|
|
|
|
|
|
@then("the returned changeset should not be None")
|
|
def step_a_not_none(ctx: Any) -> None:
|
|
assert ctx.retrieved_cs is not None
|
|
|
|
|
|
@then("the changeset plan_id should match")
|
|
def step_a_cs_pid(ctx: Any) -> None:
|
|
assert ctx.retrieved_cs.plan_id == ctx.plan_id
|
|
|
|
|
|
@then("the returned changeset should be None")
|
|
def step_a_none(ctx: Any) -> None:
|
|
assert ctx.retrieved_cs is None
|
|
|
|
|
|
# -- Summarize --
|
|
|
|
|
|
@when("I call summarize on the context")
|
|
def step_w_summarize(ctx: Any) -> None:
|
|
ctx.summary = ctx.exec_ctx.summarize()
|
|
|
|
|
|
@then("the summary should contain plan_id")
|
|
def step_a_sum_pid(ctx: Any) -> None:
|
|
assert ctx.summary["plan_id"] == ctx.plan_id
|
|
|
|
|
|
@then("the summary should contain resource_binding_count of {n:d}")
|
|
def step_a_sum_count(ctx: Any, n: int) -> None:
|
|
assert ctx.summary["resource_binding_count"] == n
|
|
|
|
|
|
@then("the summary should contain decision_root_id")
|
|
def step_a_sum_drid(ctx: Any) -> None:
|
|
assert "decision_root_id" in ctx.summary
|
|
|
|
|
|
@then("the summary resource_binding_count should be {n:d}")
|
|
def step_a_sum_count2(ctx: Any, n: int) -> None:
|
|
assert ctx.summary["resource_binding_count"] == n
|
|
|
|
|
|
# -- RuntimeExecuteResult model --
|
|
|
|
|
|
@when("I create a RuntimeExecuteResult with valid fields")
|
|
def step_w_result_ok(ctx: Any) -> None:
|
|
ctx.result = RuntimeExecuteResult(
|
|
changeset_id=str(ULID()),
|
|
tool_call_count=5,
|
|
sandbox_refs=["/tmp/sb"],
|
|
decision_ids_processed=["d1", "d2"],
|
|
execution_duration_ms=42.5,
|
|
)
|
|
|
|
|
|
@when("I attempt to create a RuntimeExecuteResult with negative tool_call_count")
|
|
def step_w_result_neg(ctx: Any) -> None:
|
|
try:
|
|
RuntimeExecuteResult(
|
|
changeset_id=str(ULID()),
|
|
tool_call_count=-1,
|
|
)
|
|
ctx.error = None
|
|
except (PydanticValidationError, ValidationError, ValueError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@when("I attempt to create a RuntimeExecuteResult with empty changeset_id")
|
|
def step_w_result_empty_csid(ctx: Any) -> None:
|
|
try:
|
|
RuntimeExecuteResult(
|
|
changeset_id="",
|
|
tool_call_count=0,
|
|
)
|
|
ctx.error = None
|
|
except (PydanticValidationError, ValidationError, ValueError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@then("the runtime execute result should have a correct changeset_id")
|
|
def step_a_res_csid(ctx: Any) -> None:
|
|
assert ctx.result.changeset_id
|
|
assert len(ctx.result.changeset_id) == 26
|
|
|
|
|
|
@then("the runtime execute result tool_call_count should be non-negative")
|
|
def step_a_res_count(ctx: Any) -> None:
|
|
assert ctx.result.tool_call_count >= 0
|
|
|
|
|
|
@then("the runtime result should fail validation")
|
|
def step_a_pydantic_err(ctx: Any) -> None:
|
|
assert ctx.error is not None
|
|
|
|
|
|
# -- RuntimeExecuteActor --
|
|
|
|
|
|
@given("a ToolRunner with an empty registry")
|
|
def step_g_runner(ctx: Any) -> None:
|
|
ctx.tool_runner = _runner()
|
|
|
|
|
|
@given("a RuntimeExecuteActor")
|
|
def step_g_actor(ctx: Any) -> None:
|
|
ctx.runtime_actor = RuntimeExecuteActor(
|
|
tool_runner=ctx.tool_runner,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
@given("a stream callback collector")
|
|
def step_g_collector(ctx: Any) -> None:
|
|
ctx.stream_events = []
|
|
ctx.stream_callback = lambda t, d: ctx.stream_events.append((t, d))
|
|
|
|
|
|
@when("I execute with a list of two stub decisions")
|
|
def step_w_exec2(ctx: Any) -> None:
|
|
ctx.result = ctx.runtime_actor.execute(
|
|
decisions=_make_decisions(2),
|
|
)
|
|
|
|
|
|
@when("I execute with one stub decision and the stream callback")
|
|
def step_w_exec1_cb(ctx: Any) -> None:
|
|
ctx.result = ctx.runtime_actor.execute(
|
|
decisions=_make_decisions(1),
|
|
stream_callback=ctx.stream_callback,
|
|
)
|
|
|
|
|
|
@when("I execute with one stub decision")
|
|
def step_w_exec1(ctx: Any) -> None:
|
|
ctx.result = ctx.runtime_actor.execute(
|
|
decisions=_make_decisions(1),
|
|
)
|
|
|
|
|
|
@when("I attempt to create a RuntimeExecuteActor with None tool_runner")
|
|
def step_w_actor_no_run(ctx: Any) -> None:
|
|
try:
|
|
RuntimeExecuteActor(
|
|
tool_runner=None,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
ctx.error = None
|
|
except (ValidationError, TypeError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@when("I attempt to create a RuntimeExecuteActor with None execution_context")
|
|
def step_w_actor_no_ctx(ctx: Any) -> None:
|
|
try:
|
|
RuntimeExecuteActor(
|
|
tool_runner=ctx.tool_runner,
|
|
execution_context=None,
|
|
)
|
|
ctx.error = None
|
|
except (ValidationError, TypeError) as e:
|
|
ctx.error = e
|
|
|
|
|
|
@then("the runtime execute result should be a RuntimeExecuteResult")
|
|
def step_a_type(ctx: Any) -> None:
|
|
assert isinstance(ctx.result, RuntimeExecuteResult)
|
|
|
|
|
|
@then("the runtime execute result changeset_id should be a valid ULID")
|
|
def step_a_csid_ulid(ctx: Any) -> None:
|
|
assert len(ctx.result.changeset_id) == 26
|
|
|
|
|
|
@then("the runtime execute result decision_ids_processed should have two entries")
|
|
def step_a_2ids(ctx: Any) -> None:
|
|
assert len(ctx.result.decision_ids_processed) == 2
|
|
|
|
|
|
@then("the callback should have received runtime_execute_started event")
|
|
def step_a_evt_start(ctx: Any) -> None:
|
|
types = [e[0] for e in ctx.stream_events]
|
|
assert "runtime_execute_started" in types
|
|
|
|
|
|
@then("the callback should have received runtime_execute_step event")
|
|
def step_a_evt_step(ctx: Any) -> None:
|
|
types = [e[0] for e in ctx.stream_events]
|
|
assert "runtime_execute_step" in types
|
|
|
|
|
|
@then("the callback should have received runtime_execute_complete event")
|
|
def step_a_evt_done(ctx: Any) -> None:
|
|
types = [e[0] for e in ctx.stream_events]
|
|
assert "runtime_execute_complete" in types
|
|
|
|
|
|
@then('the runtime execute result sandbox_refs should contain "{path}"')
|
|
def step_a_srefs(ctx: Any, path: str) -> None:
|
|
assert ctx.result.sandbox_refs and path in ctx.result.sandbox_refs
|
|
|
|
|
|
# -- PlanExecutor integration --
|
|
|
|
|
|
@given("a mock lifecycle service")
|
|
def step_g_mock_lc(ctx: Any) -> None:
|
|
ctx.lifecycle = _mock_lifecycle()
|
|
|
|
|
|
@given("a PlanExecutionContext with a known changeset store")
|
|
def step_g_ctx_known(ctx: Any) -> None:
|
|
ctx.known_store = InMemoryChangeSetStore()
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.known_store)
|
|
|
|
|
|
@when("I create a PlanExecutor with a mock tool_calling_runtime")
|
|
def step_w_pe_with_runtime(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(
|
|
lifecycle_service=ctx.lifecycle,
|
|
tool_runner=ctx.tool_runner,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
@when("I create a PlanExecutor with the execution_context")
|
|
def step_w_pe_ctx(ctx: Any) -> None:
|
|
if not hasattr(ctx, "exec_ctx"):
|
|
ctx.exec_ctx = _make_ctx(ctx.plan_id, changeset_store=ctx.changeset_store)
|
|
ctx.executor = PlanExecutor(
|
|
lifecycle_service=ctx.lifecycle,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
@when("I create a PlanExecutor without execution_context")
|
|
def step_w_pe_no_ctx(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(lifecycle_service=ctx.lifecycle)
|
|
|
|
|
|
@then("has_runtime should be True")
|
|
def step_a_hr_true(ctx: Any) -> None:
|
|
assert ctx.executor.has_runtime is True
|
|
|
|
|
|
@then("has_runtime should be False")
|
|
def step_a_hr_false(ctx: Any) -> None:
|
|
assert ctx.executor.has_runtime is False
|
|
|
|
|
|
@then("changeset_store should be the same object as the context store")
|
|
def step_a_cs_same(ctx: Any) -> None:
|
|
assert ctx.executor.changeset_store is ctx.known_store
|
|
|
|
|
|
@then("changeset_store should be None")
|
|
def step_a_cs_none(ctx: Any) -> None:
|
|
assert ctx.executor.changeset_store is None
|
|
|
|
|
|
@then("execution_context property should return the context")
|
|
def step_a_ec_prop(ctx: Any) -> None:
|
|
assert ctx.executor.execution_context is ctx.exec_ctx
|
|
|
|
|
|
# -- Error cases --
|
|
|
|
|
|
@when("I attempt to create a PlanExecutor with None lifecycle_service")
|
|
def step_w_pe_none_lc(ctx: Any) -> None:
|
|
try:
|
|
PlanExecutor(lifecycle_service=None)
|
|
ctx.error = None
|
|
except ValidationError as e:
|
|
ctx.error = e
|
|
|
|
|
|
@given("a PlanExecutor without execution_context")
|
|
def step_g_pe_no_ctx(ctx: Any) -> None:
|
|
if not hasattr(ctx, "lifecycle"):
|
|
ctx.lifecycle = _mock_lifecycle()
|
|
ctx.executor = PlanExecutor(lifecycle_service=ctx.lifecycle)
|
|
|
|
|
|
@when("I attempt to run execute with an empty plan_id")
|
|
def step_w_exec_empty(ctx: Any) -> None:
|
|
try:
|
|
ctx.executor.run_execute("")
|
|
ctx.error = None
|
|
except (ValidationError, PlanError) as e:
|
|
ctx.error = e
|