diff --git a/src/backend/workflows/durable/workflow_creation_workflow.py b/src/backend/workflows/durable/workflow_creation_workflow.py index fd08dbb99..59cff32a3 100644 --- a/src/backend/workflows/durable/workflow_creation_workflow.py +++ b/src/backend/workflows/durable/workflow_creation_workflow.py @@ -2540,11 +2540,32 @@ def _handle_verify_discoverability(request: WorkflowActionRequest) -> WorkflowAc postconditions_verified = False optional_test_instance_id: str | None = None + verification_run: dict[str, Any] | None = None if structural_validation_passed: executor = WorkflowExecutor(registry=verification_registry, max_transitions=40) verification_inputs_raw = request.data.get("test_run_inputs") if not isinstance(verification_inputs_raw, Mapping): verification_inputs_raw = spec.get("verification_inputs") + verification_data = ( + dict(verification_inputs_raw) + if isinstance(verification_inputs_raw, Mapping) + else {} + ) + # Test fixtures supply domain inputs, not a replacement execution actor. + # Nested LLM steps consume these runtime fields as well as environment. + if request.environment.user_concept_id: + verification_data.update( + user_concept_id=request.environment.user_concept_id, + org_concept_id=request.environment.org_concept_id, + organisation_concept_id=request.environment.org_concept_id, + namespace=request.environment.user_namespace, + user_namespace=request.environment.user_namespace, + ) + if request.environment.model: + verification_data.setdefault("requested_model", request.environment.model) + for key in ("requested_client_type", "prefer_default_model"): + if key in request.data: + verification_data.setdefault(key, request.data[key]) verification_result = executor.run( definition, environment=WorkflowEnvironment( @@ -2564,10 +2585,13 @@ def _handle_verify_discoverability(request: WorkflowActionRequest) -> WorkflowAc org_concept_id=request.environment.org_concept_id, step_callback=request.environment.step_callback, ), - data=dict(verification_inputs_raw or {}) - if isinstance(verification_inputs_raw, Mapping) - else {}, + data=verification_data, ) + verification_run = { + "completed": verification_result.completed, + "final_state": verification_result.final_state, + "error": verification_result.error, + } optional_test_instance_id = f"local_test_{uuid.uuid4()}" probe: dict[str, Any] = {} probe_raw = spec.get("postcondition_probe") @@ -2605,6 +2629,7 @@ def _handle_verify_discoverability(request: WorkflowActionRequest) -> WorkflowAc "optional_test_instance_id": optional_test_instance_id, "workflow_discoverable_before_publish": discoverable_before_publish, "contract_validation": contract, + "verification_run": verification_run, }, validated_type_name=parent_type_id, ) diff --git a/tests/backend/test_workflow_verification_context.py b/tests/backend/test_workflow_verification_context.py new file mode 100644 index 000000000..86c74840c --- /dev/null +++ b/tests/backend/test_workflow_verification_context.py @@ -0,0 +1,80 @@ +"""Creation probes retain their actor/model and expose an actual runtime failure.""" + +from types import SimpleNamespace + +from src.backend.workflows.action_registry import ( + WorkflowActionRequest, + WorkflowEnvironment, +) +from src.backend.workflows.durable import workflow_creation_workflow as creation + + +def test_verification_inherits_trusted_scope_and_reports_execution_error(monkeypatch): + spec = { + "workflow_id": "#V#slides", + "parent_type_id": "#V#ai_workflow", + "required_effects": ["save source-linked analysis"], + "verification_inputs": { + "file_copy_concept_id": "#V#source", + "user_concept_id": "#V#wrong_actor", + "org_concept_id": "#V#wrong_org", + }, + } + monkeypatch.setattr(creation, "_normalise_workflow_spec", lambda *a, **k: spec) + monkeypatch.setattr(creation, "discover_workflow_ids", lambda: ["#V#slides"]) + monkeypatch.setattr( + creation, "load_workflow_definition_from_vontology", lambda _: object() + ) + monkeypatch.setattr(creation, "_build_verification_registry", lambda _: object()) + monkeypatch.setattr( + creation, "_supported_action_ids_for_verification", lambda **k: {"llm.action"} + ) + monkeypatch.setattr( + creation, "validate_workflow_definition_contract", lambda **k: {"valid": True} + ) + monkeypatch.setattr( + creation, "_write_workflow_publication_lifecycle", lambda **k: None + ) + captured = {} + + class ProbeExecutor: + def __init__(self, **kwargs): + pass + + def run(self, definition, *, environment, data): + captured.update(data) + return SimpleNamespace( + completed=False, + final_state="read_slides", + error="source_not_accessible", + data={}, + ) + + monkeypatch.setattr(creation, "WorkflowExecutor", ProbeExecutor) + result = creation._handle_verify_discoverability( + WorkflowActionRequest( + action_id="workflow_authoring.validate_workflow_definition", + inputs={}, + environment=WorkflowEnvironment( + llm_client=object(), + model="gpt-5.6-luna", + user_concept_id="#V#owner", + org_concept_id="#V#org", + user_namespace="#V#owner@org", + ), + data={"requested_client_type": "openai", "prefer_default_model": True}, + ) + ) + assert captured["file_copy_concept_id"] == "#V#source" + assert captured["user_concept_id"] == "#V#owner" + assert captured["org_concept_id"] == captured["organisation_concept_id"] == "#V#org" + assert captured["namespace"] == captured["user_namespace"] == "#V#owner@org" + assert captured["requested_model"] == "gpt-5.6-luna" + assert captured["requested_client_type"] == "openai" + assert captured["prefer_default_model"] is True + assert result.status == "failed" + assert result.outputs["verification_run"] == { + "completed": False, + "final_state": "read_slides", + "error": "source_not_accessible", + }