@@ -164,6 +164,10 @@ public function test_it_publishes_external_execution_surface_contract_manifest()
164164 'worker_protocol.external_execution_surface_contract.contract_seams.handler_mappings.schema ' ,
165165 'durable-workflow.v2.external-executor-config.contract ' ,
166166 )
167+ ->assertJsonPath (
168+ 'worker_protocol.external_execution_surface_contract.contract_seams.invocable_http_carrier.status ' ,
169+ 'published ' ,
170+ )
167171 ->assertJsonPath (
168172 'worker_protocol.external_execution_surface_contract.contract_seams.bridge_adapters.status ' ,
169173 'planned ' ,
@@ -178,6 +182,35 @@ public function test_it_publishes_external_execution_surface_contract_manifest()
178182 );
179183 }
180184
185+ public function test_it_publishes_invocable_carrier_contract_manifest (): void
186+ {
187+ $ this ->getJson ('/api/cluster/info ' )
188+ ->assertOk ()
189+ ->assertJsonPath (
190+ 'worker_protocol.invocable_carrier_contract.schema ' ,
191+ 'durable-workflow.v2.invocable-carrier.contract ' ,
192+ )
193+ ->assertJsonPath ('worker_protocol.invocable_carrier_contract.carrier_type ' , 'invocable_http ' )
194+ ->assertJsonPath ('worker_protocol.invocable_carrier_contract.scope.task_kinds.0 ' , 'activity_task ' )
195+ ->assertJsonPath (
196+ 'worker_protocol.invocable_carrier_contract.request.body_schema ' ,
197+ 'durable-workflow.v2.external-task-input.contract ' ,
198+ )
199+ ->assertJsonPath (
200+ 'worker_protocol.invocable_carrier_contract.response.body_schema ' ,
201+ 'durable-workflow.v2.external-task-result.contract ' ,
202+ )
203+ ->assertJsonPath (
204+ 'worker_protocol.server_capabilities.invocable_carrier.schema ' ,
205+ 'durable-workflow.v2.invocable-carrier.contract ' ,
206+ )
207+ ->assertJsonPath ('capabilities.invocable_carrier_contract ' , true )
208+ ->assertJsonPath (
209+ 'client_compatibility.required_protocols.worker_protocol.invocable_carrier_contract.version ' ,
210+ 1 ,
211+ );
212+ }
213+
181214 public function test_it_publishes_external_executor_config_contract_when_no_config_is_set (): void
182215 {
183216 $ this ->getJson ('/api/cluster/info ' )
@@ -306,6 +339,44 @@ public function test_it_reports_named_external_executor_config_validation_errors
306339 $ response ->assertJsonPath ('worker_protocol.external_executor_config_contract.runtime.status ' , 'invalid ' );
307340 }
308341
342+ public function test_it_fails_closed_on_malformed_invocable_http_carrier_config (): void
343+ {
344+ $ this ->useExternalExecutorConfigFixture ([
345+ 'schema ' => 'durable-workflow.external-executor.config ' ,
346+ 'version ' => 1 ,
347+ 'defaults ' => [
348+ 'task_queue ' => 'operator-tasks ' ,
349+ ],
350+ 'carriers ' => [
351+ 'bad-invocable ' => [
352+ 'type ' => 'invocable_http ' ,
353+ 'method ' => 'GET ' ,
354+ 'timeout_seconds ' => true ,
355+ 'capabilities ' => ['activity_task ' , 'workflow_task ' ],
356+ ],
357+ ],
358+ 'mappings ' => [
359+ [
360+ 'name ' => 'billing.backfill ' ,
361+ 'kind ' => 'activity ' ,
362+ 'activity_type ' => 'billing.backfill ' ,
363+ 'carrier ' => 'bad-invocable ' ,
364+ 'handler ' => 'billing.backfill ' ,
365+ ],
366+ ],
367+ ]);
368+
369+ $ response = $ this ->getJson ('/api/cluster/info ' )->assertOk ();
370+ $ codes = array_column (
371+ $ response ->json ('worker_protocol.external_executor_config_contract.runtime.errors ' ),
372+ 'code ' ,
373+ );
374+
375+ $ this ->assertContains ('invalid_carrier_target ' , $ codes );
376+ $ this ->assertContains ('invalid_invocable_carrier_scope ' , $ codes );
377+ $ response ->assertJsonPath ('worker_protocol.external_executor_config_contract.runtime.status ' , 'invalid ' );
378+ }
379+
309380 public function test_it_applies_named_external_executor_config_overlay_before_validation (): void
310381 {
311382 config (['server.external_executor.overlay ' => 'prod ' ]);
0 commit comments