Ver código fonte

fix: guard structured incomplete fallback

Problem: a structured resolver's complete=False signal could trigger fallback even when resolved arguments also had substantive enum, type, or additional-property validation errors.

Risk: explicit incomplete results now fall back only when original validation has no substantive error; optional-only incomplete and pure required-missing resolution remain compatible.
zhenyu.hu 2 semanas atrás
pai
commit
432eadc60c

+ 4 - 1
src/agent_lab/application/events/kernel.py

@@ -67,6 +67,9 @@ class EventKernel:
         assert resolved is not None
 
         validation = self._validate(definition, resolved.arguments)
+        fallback_eligible = validation.missing_required or (
+            not resolution_complete and validation.status is None
+        )
         if not resolution_complete and validation.status is None:
             validation = _ValidationResult(
                 status=EventStatus.INVALID_ARGUMENTS,
@@ -75,7 +78,7 @@ class EventKernel:
         used_fallback = False
         if (
             validation.status is not EventStatus.DEFINITION_ERROR
-            and (validation.missing_required or not resolution_complete)
+            and fallback_eligible
             and request.source is not EventSource.PROVIDER_RESOLVED
             and definition.fallback_allowed
             and self.argument_fallback is not None

+ 63 - 0
tests/test_event_kernel.py

@@ -322,6 +322,69 @@ async def test_kernel_does_not_fallback_for_required_plus_substantive_error(
     assert fallback_calls == 0
 
 
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    ("arguments", "properties", "expected_error"),
+    [
+        (
+            {"mode": "invalid"},
+            {"mode": {"enum": ["valid"]}},
+            "invalid event arguments: 'invalid' is not one of ['valid']",
+        ),
+        (
+            {"count": "invalid"},
+            {"count": {"type": "integer"}},
+            "invalid argument type for count: expected integer",
+        ),
+        (
+            {"unexpected": True},
+            {},
+            "invalid event arguments: Additional properties are not allowed "
+            "('unexpected' was unexpected)",
+        ),
+    ],
+)
+async def test_structured_incomplete_does_not_fallback_over_substantive_error(
+    arguments: dict[str, Any],
+    properties: dict[str, Any],
+    expected_error: str,
+):
+    fallback_calls = 0
+
+    async def fallback(*args: Any) -> ResolvedEventArguments:
+        nonlocal fallback_calls
+        fallback_calls += 1
+        raise AssertionError("fallback should not run")
+
+    definition = _definition(
+        parameters={
+            "type": "object",
+            "properties": {
+                "query": {"type": "string"},
+                **properties,
+            },
+            "required": ["query"],
+            "additionalProperties": False,
+        },
+        resolver=lambda request, context: EventArgumentResolution(
+            arguments=arguments,
+            complete=False,
+        ),
+    )
+
+    result = await EventKernel(
+        EventRegistry([definition]), argument_fallback=fallback
+    ).execute(
+        EventRequest(id="event-1", name=definition.name),
+        enabled_names=[definition.name],
+    )
+
+    assert result.status is EventStatus.INVALID_ARGUMENTS
+    assert result.error == expected_error
+    assert result.used_fallback is False
+    assert fallback_calls == 0
+
+
 def _discriminated_composed_parameters(composition: str) -> dict[str, Any]:
     return {
         "type": "object",