@@ -305,6 +305,142 @@ public function testExplicitPreAdmittedCallStillDispatchesWhenIdempotencyKeyIsPr
305305 $ this ->assertCount (1 , $ fakeWorkflow ->starts );
306306 }
307307
308+ public function testActivityBindingMarksCallStartedWithGeneratedActivityExecutionReference (): void
309+ {
310+ $ controlPlane = new DefaultServiceControlPlane (
311+ new FakeServiceWorkflowControlPlane (),
312+ new DefaultServiceBoundaryPolicy (),
313+ );
314+ [$ endpoint , $ service ] = $ this ->catalog ('billing ' );
315+
316+ $ this ->operation ($ endpoint , $ service , [
317+ 'handler_binding_kind ' => ServiceCallBindingKind::ActivityExecution->value ,
318+ 'handler_target_reference ' => 'billing.invoice.activity ' ,
319+ 'handler_binding ' => [
320+ 'activity_class ' => 'App \\Activities \\IssueInvoice ' ,
321+ 'activity_type ' => 'billing.invoice.issue ' ,
322+ 'queue ' => 'invoices-priority ' ,
323+ ],
324+ ]);
325+
326+ $ result = $ controlPlane ->execute ('billing ' , 'invoices ' , 'create ' , [
327+ 'namespace ' => 'billing ' ,
328+ 'caller_namespace ' => 'finance ' ,
329+ 'principal_subject ' => 'user:operator ' ,
330+ ]);
331+
332+ $ this ->assertTrue ($ result ['accepted ' ]);
333+ $ this ->assertSame (ServiceCallStatus::Started->value , $ result ['status ' ]);
334+ $ this ->assertSame (ServiceCallBindingKind::ActivityExecution->value , $ result ['resolved_binding_kind ' ]);
335+ $ this ->assertNotNull ($ result ['resolved_target_reference ' ]);
336+ $ this ->assertSame ($ result ['resolved_target_reference ' ], $ result ['handler ' ]['activity_execution_id ' ]);
337+ $ this ->assertSame ('App \\Activities \\IssueInvoice ' , $ result ['handler ' ]['activity_class ' ]);
338+ $ this ->assertSame ('billing.invoice.issue ' , $ result ['handler ' ]['activity_type ' ]);
339+ $ this ->assertSame (ServiceCallBindingKind::ActivityExecution->value , $ result ['handler ' ]['kind ' ]);
340+
341+ $ call = WorkflowServiceCall::query ()->firstOrFail ();
342+
343+ $ this ->assertSame (ServiceCallStatus::Started->value , $ call ->status );
344+ $ this ->assertSame (ServiceCallOutcome::Accepted, $ call ->outcome );
345+ $ this ->assertSame ($ result ['resolved_target_reference ' ], $ call ->resolved_target_reference );
346+ $ this ->assertSame ($ result ['resolved_target_reference ' ], $ call ->metadata ['activity_execution_id ' ]);
347+ $ this ->assertSame ('App \\Activities \\IssueInvoice ' , $ call ->metadata ['activity_class ' ]);
348+ $ this ->assertSame ('billing.invoice.issue ' , $ call ->metadata ['activity_type ' ]);
349+ $ this ->assertSame ('invoices-priority ' , $ call ->metadata ['queue ' ]);
350+ }
351+
352+ public function testActivityBindingFailsWhenActivityClassAndReferenceMissing (): void
353+ {
354+ $ controlPlane = new DefaultServiceControlPlane (
355+ new FakeServiceWorkflowControlPlane (),
356+ new DefaultServiceBoundaryPolicy (),
357+ );
358+ [$ endpoint , $ service ] = $ this ->catalog ('billing ' );
359+
360+ $ this ->operation ($ endpoint , $ service , [
361+ 'handler_binding_kind ' => ServiceCallBindingKind::ActivityExecution->value ,
362+ 'handler_target_reference ' => null ,
363+ 'handler_binding ' => [],
364+ ]);
365+
366+ $ result = $ controlPlane ->execute ('billing ' , 'invoices ' , 'create ' , [
367+ 'namespace ' => 'billing ' ,
368+ ]);
369+
370+ $ this ->assertFalse ($ result ['accepted ' ]);
371+ $ this ->assertSame ('handler_target_missing ' , $ result ['reason ' ]);
372+ $ this ->assertSame (ServiceCallStatus::Failed->value , $ result ['status ' ]);
373+ $ this ->assertSame (ServiceCallOutcome::HandlerFailed->value , $ result ['outcome ' ]);
374+ }
375+
376+ public function testInvocableCarrierBindingMarksCallStartedWithGeneratedCarrierRequestReference (): void
377+ {
378+ $ controlPlane = new DefaultServiceControlPlane (
379+ new FakeServiceWorkflowControlPlane (),
380+ new DefaultServiceBoundaryPolicy (),
381+ );
382+ [$ endpoint , $ service ] = $ this ->catalog ('billing ' );
383+
384+ $ this ->operation ($ endpoint , $ service , [
385+ 'handler_binding_kind ' => 'invocable_http ' ,
386+ 'handler_target_reference ' => 'https://carrier.billing.example/handle ' ,
387+ 'handler_binding ' => [
388+ 'carrier ' => 'php-invocable ' ,
389+ 'carrier_handler ' => 'billing.invoice.issue ' ,
390+ 'workflow_instance_id ' => 'invoice-42 ' ,
391+ ],
392+ ]);
393+
394+ $ result = $ controlPlane ->execute ('billing ' , 'invoices ' , 'create ' , [
395+ 'namespace ' => 'billing ' ,
396+ 'caller_namespace ' => 'finance ' ,
397+ 'principal_subject ' => 'user:operator ' ,
398+ ]);
399+
400+ $ this ->assertTrue ($ result ['accepted ' ]);
401+ $ this ->assertSame (ServiceCallStatus::Started->value , $ result ['status ' ]);
402+ $ this ->assertSame (ServiceCallBindingKind::InvocableCarrierRequest->value , $ result ['resolved_binding_kind ' ]);
403+ $ this ->assertNotNull ($ result ['resolved_target_reference ' ]);
404+ $ this ->assertSame ($ result ['resolved_target_reference ' ], $ result ['handler ' ]['carrier_request_id ' ]);
405+ $ this ->assertSame ('https://carrier.billing.example/handle ' , $ result ['handler ' ]['carrier_endpoint ' ]);
406+ $ this ->assertSame ('billing.invoice.issue ' , $ result ['handler ' ]['carrier_handler ' ]);
407+ $ this ->assertSame ('php-invocable ' , $ result ['handler ' ]['carrier ' ]);
408+ $ this ->assertSame ('invoice-42 ' , $ result ['linked_workflow_instance_id ' ]);
409+
410+ $ call = WorkflowServiceCall::query ()->firstOrFail ();
411+
412+ $ this ->assertSame (ServiceCallStatus::Started->value , $ call ->status );
413+ $ this ->assertSame (ServiceCallOutcome::Accepted, $ call ->outcome );
414+ $ this ->assertSame ($ result ['resolved_target_reference ' ], $ call ->metadata ['carrier_request_id ' ]);
415+ $ this ->assertSame ('https://carrier.billing.example/handle ' , $ call ->metadata ['carrier_endpoint ' ]);
416+ $ this ->assertSame ('billing.invoice.issue ' , $ call ->metadata ['carrier_handler ' ]);
417+ $ this ->assertSame ('php-invocable ' , $ call ->metadata ['carrier ' ]);
418+ }
419+
420+ public function testInvocableCarrierBindingFailsWhenEndpointAndReferenceMissing (): void
421+ {
422+ $ controlPlane = new DefaultServiceControlPlane (
423+ new FakeServiceWorkflowControlPlane (),
424+ new DefaultServiceBoundaryPolicy (),
425+ );
426+ [$ endpoint , $ service ] = $ this ->catalog ('billing ' );
427+
428+ $ this ->operation ($ endpoint , $ service , [
429+ 'handler_binding_kind ' => 'invocable_http ' ,
430+ 'handler_target_reference ' => null ,
431+ 'handler_binding ' => [],
432+ ]);
433+
434+ $ result = $ controlPlane ->execute ('billing ' , 'invoices ' , 'create ' , [
435+ 'namespace ' => 'billing ' ,
436+ ]);
437+
438+ $ this ->assertFalse ($ result ['accepted ' ]);
439+ $ this ->assertSame ('handler_target_missing ' , $ result ['reason ' ]);
440+ $ this ->assertSame (ServiceCallStatus::Failed->value , $ result ['status ' ]);
441+ $ this ->assertSame (ServiceCallOutcome::HandlerFailed->value , $ result ['outcome ' ]);
442+ }
443+
308444 public function testCancelCallHonorsCancellationPolicy (): void
309445 {
310446 $ controlPlane = new DefaultServiceControlPlane (
0 commit comments