"""Step implementations for tool_router.feature.""" from __future__ import annotations import json from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.domain.models.core.tool import ToolCapability from cleveragents.tool.registry import ToolRegistry from cleveragents.tool.router import ( NormalizedToolCallResult, ProviderFormat, StreamingStatus, StreamingToolUpdate, ToolCallRouter, classify_tool_error, detect_provider_format, generate_tool_call_id, normalize_tool_call, normalize_tool_schema_for_provider, ) from cleveragents.tool.runner import ToolRunner from cleveragents.tool.runtime import ToolResult, ToolSpec # -- Helpers ---------------------------------------------------------------- def _echo_handler(inputs: dict[str, Any]) -> dict[str, Any]: return dict(inputs) def _fail_handler(inputs: dict[str, Any]) -> dict[str, Any]: raise RuntimeError("handler exploded") def _validation_pass_handler(inputs: dict[str, Any]) -> dict[str, Any]: return {"passed": True, "message": "All checks passed"} def _validation_fail_handler(inputs: dict[str, Any]) -> dict[str, Any]: return {"passed": False, "message": "Check failed"} def _make_echo_spec(name: str) -> ToolSpec: return ToolSpec( name=name, description="Echo tool for testing", handler=_echo_handler, ) def _make_fail_spec(name: str) -> ToolSpec: return ToolSpec( name=name, description="Failing tool for testing", handler=_fail_handler, ) def _make_validation_pass_spec(name: str) -> ToolSpec: return ToolSpec( name=name, description="Passing validation tool", handler=_validation_pass_handler, capabilities=ToolCapability(read_only=True, writes=False), output_schema={"validation_mode": "required"}, ) def _make_validation_fail_spec(name: str) -> ToolSpec: return ToolSpec( name=name, description="Failing validation tool", handler=_validation_fail_handler, capabilities=ToolCapability(read_only=True, writes=False), output_schema={"validation_mode": "required"}, ) # -- Provider Format Detection ----------------------------------------------- @given("a tool call payload with name \"{name}\" and arguments '{args}'") def step_payload_openai(context: Context, name: str, args: str) -> None: context.payload = {"name": name, "arguments": args} @given('a tool call payload with name "{name}" and input {input_dict}') def step_payload_anthropic(context: Context, name: str, input_dict: str) -> None: context.payload = {"name": name, "input": json.loads(input_dict)} @given( 'a tool call payload with name "{name}" and type "{type_val}" and args {args_dict}' ) def step_payload_langchain( context: Context, name: str, type_val: str, args_dict: str ) -> None: context.payload = {"name": name, "type": type_val, "args": json.loads(args_dict)} @given('a tool call payload with name "{name}" and args key only') def step_payload_langchain_args_only(context: Context, name: str) -> None: context.payload = {"name": name, "args": {"key": "value"}} @given("an empty tool call payload") def step_empty_payload(context: Context) -> None: context.payload = {} @given('a tool call payload with name "{name}" and dict arguments {args_dict}') def step_payload_openai_dict_args(context: Context, name: str, args_dict: str) -> None: context.payload = {"name": name, "arguments": json.loads(args_dict)} @given('a tool call payload with name "{name}" and parameters key {params_dict}') def step_payload_unknown_with_params( context: Context, name: str, params_dict: str ) -> None: context.payload = {"name": name, "parameters": json.loads(params_dict)} @given("a tool call payload without a name") def step_payload_no_name(context: Context) -> None: context.payload = {"arguments": "{}"} @when("I detect the provider format") def step_detect_format(context: Context) -> None: context.detected_format = detect_provider_format(context.payload) @then('the detected format should be "{expected}"') def step_check_format(context: Context, expected: str) -> None: assert context.detected_format.value == expected, ( f"Expected {expected}, got {context.detected_format.value}" ) # -- Payload Normalization --------------------------------------------------- @when("I normalize the tool call") def step_normalize(context: Context) -> None: context.normalized = normalize_tool_call(context.payload) @when("I try to normalize the tool call") def step_try_normalize(context: Context) -> None: try: context.normalized = normalize_tool_call(context.payload) context.raised_error = None except ValueError as exc: context.raised_error = exc @when("I try to normalize a non-dict payload") def step_try_normalize_non_dict(context: Context) -> None: try: normalize_tool_call("not a dict") # type: ignore[arg-type] context.raised_error = None except ValueError as exc: context.raised_error = exc @then('the normalized request tool_name should be "{expected}"') def step_check_normalized_name(context: Context, expected: str) -> None: assert context.normalized.tool_name == expected @then('the normalized request arguments should have key "{key}"') def step_check_normalized_args_key(context: Context, key: str) -> None: assert key in context.normalized.arguments @then('the normalized request provider_format should be "{expected}"') def step_check_normalized_format(context: Context, expected: str) -> None: assert context.normalized.provider_format.value == expected @then('a router ValueError should be raised containing "{text}"') def step_check_router_value_error(context: Context, text: str) -> None: assert context.raised_error is not None, "Expected ValueError but none was raised" assert text.lower() in str(context.raised_error).lower(), ( f"Expected '{text}' in error message, got: {context.raised_error}" ) # -- Stable ID Generation --------------------------------------------------- @when('I generate a tool call ID for plan "{plan}" sequence {seq:d}') def step_generate_id(context: Context, plan: str, seq: int) -> None: context.tool_call_id = generate_tool_call_id(plan, seq) @when('I generate another tool call ID for plan "{plan}" sequence {seq:d}') def step_generate_another_id(context: Context, plan: str, seq: int) -> None: context.tool_call_id_2 = generate_tool_call_id(plan, seq) @then('the tool call ID should start with "{prefix}"') def step_check_id_prefix(context: Context, prefix: str) -> None: assert context.tool_call_id.startswith(prefix), ( f"Expected prefix '{prefix}', got: {context.tool_call_id}" ) @then("the tool call ID should have length {length:d}") def step_check_id_length(context: Context, length: int) -> None: assert len(context.tool_call_id) == length, ( f"Expected length {length}, got {len(context.tool_call_id)}" ) @then("both tool call IDs should be identical") def step_check_ids_same(context: Context) -> None: assert context.tool_call_id == context.tool_call_id_2 @then("the tool call IDs should differ") def step_check_ids_differ(context: Context) -> None: assert context.tool_call_id != context.tool_call_id_2 @when("I try to generate a tool call ID with empty plan_id") def step_try_gen_empty_plan(context: Context) -> None: try: generate_tool_call_id("", 0) context.raised_error = None except ValueError as exc: context.raised_error = exc @when("I try to generate a tool call ID with negative sequence") def step_try_gen_neg_seq(context: Context) -> None: try: generate_tool_call_id("plan-1", -1) context.raised_error = None except ValueError as exc: context.raised_error = exc # -- Router Execution -------------------------------------------------------- @given('a tool registry with an echo tool "{name}"') def step_registry_with_echo(context: Context, name: str) -> None: context.registry = ToolRegistry() spec = _make_echo_spec(name) context.registry.register(spec) context.runner = ToolRunner(context.registry) @given('a tool registry with a failing tool "{name}"') def step_registry_with_fail(context: Context, name: str) -> None: context.registry = ToolRegistry() spec = _make_fail_spec(name) context.registry.register(spec) context.runner = ToolRunner(context.registry) @given('a tool registry with a passing validation tool "{name}"') def step_registry_with_passing_val(context: Context, name: str) -> None: context.registry = ToolRegistry() spec = _make_validation_pass_spec(name) context.registry.register(spec) context.runner = ToolRunner(context.registry) @given('a tool registry with a failing validation tool "{name}"') def step_registry_with_failing_val(context: Context, name: str) -> None: context.registry = ToolRegistry() spec = _make_validation_fail_spec(name) context.registry.register(spec) context.runner = ToolRunner(context.registry) @given('a tool call router for plan "{plan_id}"') def step_create_router(context: Context, plan_id: str) -> None: registry = getattr(context, "tool_registry", None) or context.registry context.router = ToolCallRouter( registry=registry, runner=context.runner, plan_id=plan_id, ) @given("an OpenAI-format tool call for \"{name}\" with arguments '{args}'") def step_openai_call(context: Context, name: str, args: str) -> None: context.payload = {"name": name, "arguments": args} @given('an Anthropic-format tool call for "{name}" with input {input_dict}') def step_anthropic_call(context: Context, name: str, input_dict: str) -> None: context.payload = {"name": name, "input": json.loads(input_dict)} @given('a LangChain-format tool call for "{name}" with args {args_dict}') def step_langchain_call(context: Context, name: str, args_dict: str) -> None: context.payload = {"name": name, "type": "tool_call", "args": json.loads(args_dict)} @given('provider metadata with model "{model}" and provider_id "{pid}"') def step_provider_metadata(context: Context, model: str, pid: str) -> None: context.provider_metadata = {"model": model, "provider_id": pid} @given('a batch of {count:d} OpenAI-format tool calls for "{name}"') def step_batch_calls(context: Context, count: int, name: str) -> None: context.batch_payloads = [ {"name": name, "arguments": json.dumps({"index": i})} for i in range(count) ] @when("I route the tool call") def step_route(context: Context) -> None: context.route_result = context.router.route(context.payload) @when("I route the tool call with provider metadata") def step_route_with_metadata(context: Context) -> None: context.route_result = context.router.route( context.payload, provider_metadata=context.provider_metadata ) @when("I route the batch") def step_route_batch(context: Context) -> None: context.batch_results = context.router.route_batch(context.batch_payloads) @when("I route an empty batch") def step_route_empty_batch(context: Context) -> None: context.batch_results = context.router.route_batch([]) @when("I route the tool call with streaming") def step_route_streaming(context: Context) -> None: context.stream_items = list(context.router.route_streaming(context.payload)) @then("the normalized result should be successful") def step_check_result_success(context: Context) -> None: assert context.route_result.result.success, ( f"Expected success, got error: {context.route_result.result.error}" ) @then("the normalized result should not be successful") def step_check_result_failure(context: Context) -> None: assert not context.route_result.result.success @then('the normalized result tool_name should be "{expected}"') def step_check_result_name(context: Context, expected: str) -> None: assert context.route_result.tool_name == expected @then('the normalized result provider_format should be "{expected}"') def step_check_result_provider(context: Context, expected: str) -> None: assert context.route_result.provider_format.value == expected @then('the normalized result tool_call_id should start with "{prefix}"') def step_check_result_id_prefix(context: Context, prefix: str) -> None: assert context.route_result.tool_call_id.startswith(prefix) @then('the normalized result error_category should be "{expected}"') def step_check_result_error_cat(context: Context, expected: str) -> None: assert context.route_result.error_category is not None assert context.route_result.error_category.value == expected @then('the normalized result provider_metadata should contain key "{key}"') def step_check_result_metadata_key(context: Context, key: str) -> None: assert key in context.route_result.provider_metadata @then("the normalized result is_validation should be True") def step_check_is_validation_true(context: Context) -> None: assert context.route_result.is_validation is True @then("the normalized result validation_passed should be True") def step_check_validation_passed_true(context: Context) -> None: assert context.route_result.validation_passed is True @then("the normalized result validation_passed should be False") def step_check_validation_passed_false(context: Context) -> None: assert context.route_result.validation_passed is False # -- Batch results ----------------------------------------------------------- @then("the batch should return {count:d} results") def step_check_batch_count(context: Context, count: int) -> None: assert len(context.batch_results) == count @then("all batch results should be successful") def step_check_batch_all_success(context: Context) -> None: for r in context.batch_results: assert r.result.success, f"Batch item failed: {r.result.error}" # -- Streaming results ------------------------------------------------------- @then("the stream should emit a pending update") def step_check_stream_pending(context: Context) -> None: pending = [ i for i in context.stream_items if isinstance(i, StreamingToolUpdate) and i.status == StreamingStatus.PENDING ] assert len(pending) >= 1, "No pending update found" @then("the stream should emit a running update") def step_check_stream_running(context: Context) -> None: running = [ i for i in context.stream_items if isinstance(i, StreamingToolUpdate) and i.status == StreamingStatus.RUNNING ] assert len(running) >= 1, "No running update found" @then("the stream should emit a complete update") def step_check_stream_complete(context: Context) -> None: complete = [ i for i in context.stream_items if isinstance(i, StreamingToolUpdate) and i.status == StreamingStatus.COMPLETE ] assert len(complete) >= 1, "No complete update found" @then("the stream should emit a final result") def step_check_stream_final(context: Context) -> None: results = [ i for i in context.stream_items if isinstance(i, NormalizedToolCallResult) ] assert len(results) >= 1, "No final result found" @then("the stream should emit an error update") def step_check_stream_error(context: Context) -> None: errors = [ i for i in context.stream_items if isinstance(i, StreamingToolUpdate) and i.status == StreamingStatus.ERROR ] assert len(errors) >= 1, "No error update found" @then("the stream should emit a final result that is not successful") def step_check_stream_final_failure(context: Context) -> None: results = [ i for i in context.stream_items if isinstance(i, NormalizedToolCallResult) ] assert len(results) >= 1, "No final result found" assert not results[-1].result.success # -- Error Classification --------------------------------------------------- @when('I classify the error "{error_msg}"') def step_classify_error(context: Context, error_msg: str) -> None: context.error_category = classify_tool_error(error_msg) @then('the error category should be "{expected}"') def step_check_error_cat(context: Context, expected: str) -> None: assert context.error_category.value == expected # -- Schema Normalization --------------------------------------------------- @given('a tool spec named "{name}" with description "{desc}"') def step_tool_spec_with_desc(context: Context, name: str, desc: str) -> None: context.tool_spec = ToolSpec( name=name, description=desc, handler=_echo_handler, ) @given('a tool spec named "{name}" with a very long description') def step_tool_spec_long_desc(context: Context, name: str) -> None: context.tool_spec = ToolSpec( name=name, description="A" * 2000, handler=_echo_handler, ) @when('I normalize the schema for "{provider}" provider') def step_normalize_schema(context: Context, provider: str) -> None: context.normalized_schema = normalize_tool_schema_for_provider( context.tool_spec, ProviderFormat(provider) ) @when('I normalize the schema for "{provider}" provider with max length {length:d}') def step_normalize_schema_max(context: Context, provider: str, length: int) -> None: context.normalized_schema = normalize_tool_schema_for_provider( context.tool_spec, ProviderFormat(provider), max_description_length=length ) @when("I try to normalize the schema with max_description_length {length:d}") def step_try_normalize_schema_bad_len(context: Context, length: int) -> None: try: normalize_tool_schema_for_provider( context.tool_spec, ProviderFormat.OPENAI, max_description_length=length ) context.raised_error = None except ValueError as exc: context.raised_error = exc @then('the normalized schema should have key "{key}"') def step_check_schema_key(context: Context, key: str) -> None: assert key in context.normalized_schema @then('the normalized schema should have name "{name}"') def step_check_schema_name(context: Context, name: str) -> None: assert context.normalized_schema["name"] == name @then("the normalized schema description should be at most {length:d} characters") def step_check_schema_desc_len(context: Context, length: int) -> None: assert len(context.normalized_schema["description"]) <= length # -- Schema Export ----------------------------------------------------------- @when('I export schemas for "{provider}" provider') def step_export_schemas(context: Context, provider: str) -> None: context.exported_schemas = context.router.export_schemas(ProviderFormat(provider)) @then("the exported schemas should contain {count:d} schema") def step_check_exported_count(context: Context, count: int) -> None: assert len(context.exported_schemas) == count @then('the first exported schema should have tool_type "{expected}"') def step_check_exported_type(context: Context, expected: str) -> None: assert context.exported_schemas[0]["tool_type"] == expected # -- Sequence counter ------------------------------------------------------- @then("the router sequence should be {expected:d}") def step_check_sequence(context: Context, expected: int) -> None: assert context.router.sequence == expected # -- Router construction validation ----------------------------------------- @when("I try to create a router with empty plan_id") def step_try_create_router_empty(context: Context) -> None: try: ToolCallRouter( registry=context.registry, runner=context.runner, plan_id="", ) context.raised_error = None except ValueError as exc: context.raised_error = exc @when("I try to route a non-dict payload") def step_try_route_non_dict(context: Context) -> None: try: context.router.route("not a dict") # type: ignore[arg-type] context.raised_error = None except ValueError as exc: context.raised_error = exc @when("I try to route_batch a non-list payload") def step_try_route_batch_non_list(context: Context) -> None: try: context.router.route_batch("not a list") # type: ignore[arg-type] context.raised_error = None except ValueError as exc: context.raised_error = exc # -- Detect format non-dict ------------------------------------------------- @when("I detect format for a non-dict value") def step_detect_non_dict(context: Context) -> None: context.detected_format = detect_provider_format("not-a-dict") # type: ignore[arg-type] @when("I classify an empty error message") def step_classify_empty_error(context: Context) -> None: context.error_category = classify_tool_error("") @given("a tool call with invalid JSON arguments") def step_payload_invalid_json(context: Context) -> None: context.payload = {"name": "test/echo", "arguments": "not valid json"} # -- Coverage: provider branches and validation --------------------------------- @given('a tool call payload with name "{name}" and dict arguments') def step_payload_dict_args(context: Context, name: str) -> None: context.payload = {"name": name, "arguments": {"msg": "hello"}} @given("an OpenAI payload with integer arguments") def step_payload_int_args(context: Context) -> None: context.payload = {"name": "test/echo", "arguments": 42} @when("I try to normalize the invalid openai arguments") def step_normalize_invalid_openai(context: Context) -> None: try: normalize_tool_call(context.payload) context.raised_error = None except ValueError as exc: context.raised_error = exc @given("an Anthropic payload with string input") def step_payload_anthropic_str(context: Context) -> None: context.payload = { "name": "test/echo", "id": "call-1", "type": "tool_use", "input": "bad", } @when("I try to normalize the invalid anthropic input") def step_normalize_invalid_anthropic(context: Context) -> None: try: normalize_tool_call(context.payload) context.raised_error = None except ValueError as exc: context.raised_error = exc @given("a LangChain payload with dict args") def step_payload_langchain_dict(context: Context) -> None: context.payload = {"name": "test/echo", "args": {"key": "val"}, "id": "lc-1"} @given("a LangChain payload with list args") def step_payload_langchain_list(context: Context) -> None: context.payload = {"name": "test/echo", "args": [1, 2, 3], "id": "lc-2"} @when("I try to normalize the invalid langchain args") def step_normalize_invalid_langchain(context: Context) -> None: try: normalize_tool_call(context.payload) context.raised_error = None except ValueError as exc: context.raised_error = exc @given("an unknown format payload with parameters key") def step_payload_unknown_params(context: Context) -> None: context.payload = {"name": "test/echo", "parameters": {"query": "hello"}} @given("an unknown format payload with JSON string in args key") def step_payload_unknown_json_str(context: Context) -> None: # Use "arguments" with a non-parseable string first then "input" as JSON string # to trigger the unknown branch with a JSON string in a fallback key. # "arguments" with int makes it not str/dict -> unknown; "input" as JSON string. context.payload = {"name": "test/echo", "input": '{"data": "value"}'} @given("an unknown format payload with bad JSON and valid fallback") def step_payload_unknown_bad_json(context: Context) -> None: # arguments=42 makes detect_provider_format return UNKNOWN. # In normalize unknown branch: "arguments" (int) skipped, # "input" (bad JSON str) triggers JSONDecodeError→continue, # "parameters" (dict) is used successfully. context.payload = { "name": "test/echo", "arguments": 42, "input": "not json", "parameters": {"ok": True}, } @when('I normalize the tool call for "{fmt}" format') def step_normalize_for_format(context: Context, fmt: str) -> None: context.normalized_request = normalize_tool_call(context.payload) @then('the normalized request should have tool name "{name}"') def step_then_request_tool_name(context: Context, name: str) -> None: assert context.normalized_request.tool_name == name, ( f"Expected '{name}', got '{context.normalized_request.tool_name}'" ) @then('the normalized arguments should be a dict with key "{key}"') def step_then_args_dict_key(context: Context, key: str) -> None: assert key in context.normalized_request.arguments @then('the normalized arguments should have key "{key}"') def step_then_args_key(context: Context, key: str) -> None: assert key in context.normalized_request.arguments @given('a tool spec named "{name}"') def step_given_tool_spec(context: Context, name: str) -> None: context.tool_spec = ToolSpec( name=name, description="test tool", input_schema={"type": "object", "properties": {"msg": {"type": "string"}}}, output_schema={}, capabilities=ToolCapability(read_only=False, writes=True), handler=lambda inputs: inputs, ) @then('the schema should have key "{key}"') def step_then_schema_key(context: Context, key: str) -> None: assert key in context.normalized_schema, ( f"Missing key '{key}' in {context.normalized_schema}" ) @then('the router plan_id should be "{expected}"') def step_then_router_plan_id(context: Context, expected: str) -> None: assert context.router.plan_id == expected # -- Tools that raise or fail for coverage tests --------- @given("a tool registry with a tool that raises RuntimeError") def step_registry_runtime_error(context: Context) -> None: registry = ToolRegistry() def _noop(inputs: dict[str, Any]) -> dict[str, Any]: return {} registry.register( ToolSpec( name="test/boom", description="exploding tool", input_schema={}, output_schema={}, capabilities=ToolCapability(read_only=True), handler=_noop, ) ) context.tool_registry = registry runner = ToolRunner(registry) # Override execute to raise RuntimeError, bypassing ToolRunner's catch def _raising_execute( name: str, arguments: dict[str, Any], *, plan_env: str | None = None, project_env: str | None = None, **_kwargs: Any, ) -> ToolResult: raise RuntimeError("boom") runner.execute = _raising_execute # type: ignore[assignment] context.runner = runner @when("I route an OpenAI payload for the error tool") def step_route_error_tool(context: Context) -> None: context.route_result = context.router.route( {"name": "test/boom", "arguments": "{}"} ) @then("the route result should have success false") def step_then_result_fail(context: Context) -> None: assert context.route_result.result.success is False @then('the route result error should contain "{text}"') def step_then_result_error_contains(context: Context, text: str) -> None: assert text in (context.route_result.result.error or "") @given("a tool registry with a tool that returns failed result") def step_registry_fail_result(context: Context) -> None: registry = ToolRegistry() def _fail(inputs: dict[str, Any]) -> dict[str, Any]: return {"error": "tool not found: missing"} spec = ToolSpec( name="test/fail", description="failing tool", input_schema={}, output_schema={}, capabilities=ToolCapability(read_only=True), handler=_fail, ) registry.register(spec) context.tool_registry = registry runner = ToolRunner(registry) # Override execute to return a ToolResult with success=False def _execute_fail( name: str, arguments: dict[str, Any], *, plan_env: str | None = None, project_env: str | None = None, **_kwargs: Any, ) -> ToolResult: return ToolResult( success=False, output={}, error="tool not found: missing", duration_ms=1.0, ) runner.execute = _execute_fail # type: ignore[assignment] context.runner = runner @when("I route an OpenAI payload for the fail tool") def step_route_fail_tool(context: Context) -> None: context.route_result = context.router.route( {"name": "test/fail", "arguments": "{}"} ) @then("the route result should have error_category set") def step_then_error_category_set(context: Context) -> None: assert context.route_result.error_category is not None @given("a tool registry with a validation tool") def step_registry_validation_tool(context: Context) -> None: registry = ToolRegistry() def _validate(inputs: dict[str, Any]) -> dict[str, Any]: return {"passed": True} registry.register( ToolSpec( name="validate/check", description="validate something", input_schema={}, output_schema={"validation_mode": "strict"}, capabilities=ToolCapability(read_only=True, writes=False), handler=_validate, ) ) context.tool_registry = registry context.runner = ToolRunner(registry) @when("I route an OpenAI payload for the validation tool") def step_route_validation_tool(context: Context) -> None: context.route_result = context.router.route( {"name": "validate/check", "arguments": "{}"} ) @then("the route result should have is_validation true") def step_then_is_validation(context: Context) -> None: assert context.route_result.is_validation is True @then("the route result should have validation_passed true") def step_then_validation_passed(context: Context) -> None: assert context.route_result.validation_passed is True # -- Streaming coverage ---- @when("I stream-route an OpenAI payload for the error tool") def step_stream_route_error(context: Context) -> None: results = list( context.router.route_streaming({"name": "test/boom", "arguments": "{}"}) ) context.streaming_results = results @then("the streaming updates should include ERROR status") def step_then_streaming_error_status(context: Context) -> None: statuses = [ r.status for r in context.streaming_results if isinstance(r, StreamingToolUpdate) ] assert StreamingStatus.ERROR in statuses @then("the streaming final result should have success false") def step_then_streaming_fail(context: Context) -> None: final = [ r for r in context.streaming_results if isinstance(r, NormalizedToolCallResult) ] assert len(final) == 1 assert final[0].result.success is False @when("I try to stream-route a non-dict payload") def step_try_stream_non_dict(context: Context) -> None: try: list(context.router.route_streaming("not a dict")) # type: ignore[arg-type] context.raised_error = None except ValueError as exc: context.raised_error = exc @when("I stream-route an OpenAI payload for the validation tool") def step_stream_route_validation(context: Context) -> None: results = list( context.router.route_streaming({"name": "validate/check", "arguments": "{}"}) ) context.streaming_results = results @then("the streaming final result should have is_validation true") def step_then_streaming_validation(context: Context) -> None: final = [ r for r in context.streaming_results if isinstance(r, NormalizedToolCallResult) ] assert len(final) == 1 assert final[0].is_validation is True # -- Schema export coverage ---- @when('I export tool schemas for "{provider}"') def step_export_schemas(context: Context, provider: str) -> None: fmt = ProviderFormat(provider) context.exported_schemas = context.router.export_schemas(fmt) @then('the exported schemas should include a tool with type "{tool_type}"') def step_then_schema_tool_type(context: Context, tool_type: str) -> None: types = [s.get("tool_type") for s in context.exported_schemas] assert tool_type in types, f"Expected type '{tool_type}' in {types}" @given('a tool registry with a validation tool with mode "{mode}"') def step_registry_validation_with_mode(context: Context, mode: str) -> None: registry = ToolRegistry() def _validate(inputs: dict[str, Any]) -> dict[str, Any]: return {"passed": True} registry.register( ToolSpec( name="validate/strict", description="strict validator", input_schema={}, output_schema={"validation_mode": mode}, capabilities=ToolCapability(read_only=True, writes=False), handler=_validate, ) ) context.tool_registry = registry context.runner = ToolRunner(registry) @then('the exported validation schema should have mode "{mode}"') def step_then_schema_mode(context: Context, mode: str) -> None: validation_schemas = [ s for s in context.exported_schemas if s.get("tool_type") == "validation" ] assert len(validation_schemas) >= 1 assert validation_schemas[0].get("validation_mode") == mode @given("a tool registry with a validation tool with numeric mode") def step_registry_validation_numeric_mode(context: Context) -> None: registry = ToolRegistry() def _validate(inputs: dict[str, Any]) -> dict[str, Any]: return {"passed": True} registry.register( ToolSpec( name="validate/numeric", description="numeric mode validator", input_schema={}, output_schema={"validation_mode": 123}, capabilities=ToolCapability(read_only=True, writes=False), handler=_validate, ) ) context.tool_registry = registry context.runner = ToolRunner(registry) @then("the exported validation schema should not have a mode") def step_then_schema_no_mode(context: Context) -> None: validation_schemas = [ s for s in context.exported_schemas if s.get("tool_type") == "validation" ] assert len(validation_schemas) >= 1 assert "validation_mode" not in validation_schemas[0]