diff --git a/src/conductor/providers/_schema.py b/src/conductor/providers/_schema.py index dc85e2a1..98171321 100644 --- a/src/conductor/providers/_schema.py +++ b/src/conductor/providers/_schema.py @@ -107,26 +107,18 @@ def build_json_schema_properties( def build_prompt_schema_field( field: OutputField, *, - field_name: str | None, depth: int = 0, max_depth: int = 10, - description_fallback: bool = False, ) -> dict[str, Any]: """Build a prompt-facing schema fragment for a single ``OutputField``. - When ``description_fallback`` is true and the field has no explicit - description and ``field_name`` is provided, the description is filled - with ``"The {field_name} field"``. Array items are built with - ``field_name=None`` so they do not gain a fallback description. + The fragment contains ``type`` and optionally ``description``, + ``properties`` + ``required`` (for objects), or ``items`` (for arrays). Args: field: The output field definition. - field_name: The field name used for the fallback description, or - ``None`` to suppress the fallback. depth: Current nesting depth. max_depth: Maximum allowed nesting depth. - description_fallback: Whether to synthesize a description when one - is not explicitly set. Returns: A prompt-facing schema fragment dictionary. @@ -136,30 +128,19 @@ def build_prompt_schema_field( """ _check_depth(depth, max_depth) - description = field.description - if description_fallback and description is None and field_name is not None: - description = f"The {field_name} field" - schema: dict[str, Any] = {"type": field.type} - if description: - schema["description"] = description + if field.description: + schema["description"] = field.description if field.type == "object" and field.properties: schema["properties"] = build_prompt_schema_properties( - field.properties, - depth=depth + 1, - max_depth=max_depth, - description_fallback=description_fallback, + field.properties, depth=depth + 1, max_depth=max_depth ) schema["required"] = list(field.properties.keys()) if field.type == "array" and field.items: schema["items"] = build_prompt_schema_field( - field.items, - field_name=None, - depth=depth + 1, - max_depth=max_depth, - description_fallback=description_fallback, + field.items, depth=depth + 1, max_depth=max_depth ) return schema @@ -170,7 +151,6 @@ def build_prompt_schema_properties( *, depth: int = 0, max_depth: int = 10, - description_fallback: bool = False, ) -> dict[str, Any]: """Build a prompt-facing schema mapping from named ``OutputField`` definitions. @@ -178,8 +158,6 @@ def build_prompt_schema_properties( fields: Mapping from field name to output field definition. depth: Current nesting depth. max_depth: Maximum allowed nesting depth. - description_fallback: Whether to synthesize a description when one - is not explicitly set. Returns: A prompt-facing schema mapping. @@ -190,99 +168,6 @@ def build_prompt_schema_properties( _check_depth(depth, max_depth) return { - name: build_prompt_schema_field( - field, - field_name=name, - depth=depth, - max_depth=max_depth, - description_fallback=description_fallback, - ) + name: build_prompt_schema_field(field, depth=depth, max_depth=max_depth) for name, field in fields.items() } - - -def build_hermes_legacy_prompt_schema( - fields: dict[str, OutputField], *, depth: int = 0, max_depth: int = 10 -) -> dict[str, Any]: - """Build the Hermes legacy prompt-facing schema mapping. - - This matches the legacy Hermes provider behavior: descriptions fall back - to ``"The {field_name} field"`` at the top level, but array items do not - receive a fallback description. Unlike the generic prompt builder, object - items inside arrays are emitted with ``properties`` but no ``required`` - key, and array-of-array items collapse to ``{"type": "array"}`` without - further recursion (an explicit item description is still kept). - - Args: - fields: Mapping from field name to output field definition. - depth: Current nesting depth. - max_depth: Maximum allowed nesting depth. - - Returns: - The Hermes legacy prompt-facing schema mapping. - - Raises: - SchemaDepthError: When the depth limit is exceeded. - """ - _check_depth(depth, max_depth) - - result: dict[str, Any] = {} - for field_name, field_def in fields.items(): - field_schema: dict[str, Any] = {"type": field_def.type} - - if field_def.description: - field_schema["description"] = field_def.description - else: - field_schema["description"] = f"The {field_name} field" - - if field_def.type == "object" and field_def.properties: - field_schema["properties"] = build_hermes_legacy_prompt_schema( - field_def.properties, depth=depth + 1, max_depth=max_depth - ) - field_schema["required"] = list(field_def.properties.keys()) - - if field_def.type == "array" and field_def.items: - field_schema["items"] = _build_hermes_legacy_item_schema( - field_def.items, depth=depth + 1, max_depth=max_depth - ) - - result[field_name] = field_schema - - return result - - -def _build_hermes_legacy_item_schema( - field: OutputField, *, depth: int, max_depth: int -) -> dict[str, Any]: - """Build the Hermes legacy schema fragment for an array item. - - Object items include ``properties`` but no ``required``. Array items of - any kind collapse to the bare ``{"type": "array"}`` shape with no - inner recursion (an explicit item description is still kept). - - Args: - field: The array item output field definition. - depth: Current nesting depth. - max_depth: Maximum allowed nesting depth. - - Returns: - The Hermes legacy item schema fragment. - - Raises: - SchemaDepthError: When the depth limit is exceeded. - """ - item_schema: dict[str, Any] = {"type": field.type} - - if field.description: - item_schema["description"] = field.description - - if field.type == "object" and field.properties: - _check_depth(depth, max_depth) - # Pinned legacy counting: the array itself advanced depth by one when - # calling this helper, so the item's properties must recurse at the - # same depth (not depth + 1) to match pre-refactor Hermes behavior. - item_schema["properties"] = build_hermes_legacy_prompt_schema( - field.properties, depth=depth, max_depth=max_depth - ) - - return item_schema diff --git a/src/conductor/providers/copilot.py b/src/conductor/providers/copilot.py index b80393ef..8002dd3b 100644 --- a/src/conductor/providers/copilot.py +++ b/src/conductor/providers/copilot.py @@ -1659,7 +1659,6 @@ def _build_prompt_schema( schema, depth=depth, max_depth=self._max_schema_depth, - description_fallback=True, ) except SchemaDepthError as exc: raise ValidationError( diff --git a/src/conductor/providers/hermes.py b/src/conductor/providers/hermes.py index b219f65f..9476aab1 100644 --- a/src/conductor/providers/hermes.py +++ b/src/conductor/providers/hermes.py @@ -25,14 +25,14 @@ from conductor.executor.output import parse_json_output, validate_output from conductor.providers._schema import ( SchemaDepthError, - build_hermes_legacy_prompt_schema, + build_prompt_schema_properties, ) from conductor.providers.base import AgentOutput, AgentProvider, EventCallback from conductor.providers.capabilities import ProviderCapabilities from conductor.providers.reasoning import ReasoningEffort, resolve_reasoning_effort if TYPE_CHECKING: - from conductor.config.schema import AgentDef + from conductor.config.schema import AgentDef, OutputField # The hermes-agent package ships its public API under the top-level module # name "run_agent" (not "hermes_agent"). Catch only ModuleNotFoundError so @@ -642,14 +642,14 @@ async def _wait_for_event(event: asyncio.Event) -> None: await event.wait() -def _build_prompt_schema(schema: dict[str, Any], depth: int = 0) -> dict[str, Any]: +def _build_prompt_schema(schema: dict[str, OutputField], depth: int = 0) -> dict[str, Any]: """Build a prompt-facing schema description from OutputField definitions. - Wraps the shared Hermes legacy builder, converting core depth errors into - the provider's ValidationError with the exact legacy message and suggestion. + Wraps the shared recursive prompt builder, converting core depth errors into + the provider's ValidationError with the exact message and suggestion. """ try: - return build_hermes_legacy_prompt_schema(schema, depth=depth, max_depth=_MAX_SCHEMA_DEPTH) + return build_prompt_schema_properties(schema, depth=depth, max_depth=_MAX_SCHEMA_DEPTH) except SchemaDepthError as exc: raise ValidationError( f"Schema nesting depth exceeds maximum of {_MAX_SCHEMA_DEPTH} levels", diff --git a/tests/test_providers/test_copilot.py b/tests/test_providers/test_copilot.py index 19298330..827ac61b 100644 --- a/tests/test_providers/test_copilot.py +++ b/tests/test_providers/test_copilot.py @@ -456,7 +456,24 @@ def test_build_prompt_schema_recurses_through_nested_fields(self) -> None: == "string" ) assert schema["plan"]["required"] == ["questions", "areas", "sources"] - assert schema["summary"]["description"] == "The summary field" + assert "description" not in schema["summary"] + + def test_no_fallback_description(self) -> None: + """A field without an explicit description must not have a synthesized description key.""" + provider = CopilotProvider(mock_handler=stub_handler) + agent = AgentDef( + name="summarizer", + model="gpt-4", + prompt="Summarize", + output={ + "summary": {"type": "string"}, + }, + ) + + schema = provider._build_prompt_schema(agent.output or {}) + + assert schema["summary"]["type"] == "string" + assert "description" not in schema["summary"] def test_build_prompt_schema_depth_limit_enforced(self) -> None: """Excessively nested schemas raise ValidationError with the pinned message. @@ -601,6 +618,33 @@ def test_build_parse_recovery_prompt_basic(self) -> None: assert '"value"' in prompt assert "ONLY a valid JSON object" in prompt + def test_build_parse_recovery_prompt_has_no_fallback_descriptions(self) -> None: + """The schema embedded in a parse recovery prompt must be built from OutputField + definitions so it contains no synthesized fallback descriptions.""" + provider = CopilotProvider(mock_handler=stub_handler) + agent = AgentDef( + name="parser", + model="gpt-4", + prompt="Parse", + output={ + "name": {"type": "string"}, + "value": {"type": "number"}, + }, + ) + + schema = provider._build_prompt_schema(agent.output or {}) + prompt = provider._build_parse_recovery_prompt( + parse_error="missing quotes", + original_response='{"name": "x}', + schema=schema, + ) + + assert '"name"' in prompt + assert '"value"' in prompt + assert '"The name field"' not in prompt + assert '"The value field"' not in prompt + assert "```json" in prompt + def test_build_parse_recovery_prompt_truncates_long_response(self) -> None: """Test that long responses are truncated in recovery prompt.""" provider = CopilotProvider(mock_handler=stub_handler) diff --git a/tests/test_providers/test_hermes.py b/tests/test_providers/test_hermes.py index a74360fa..61ca9a96 100644 --- a/tests/test_providers/test_hermes.py +++ b/tests/test_providers/test_hermes.py @@ -839,10 +839,11 @@ def test_real_capabilities_tuple_excludes_max(self) -> None: class TestHermesBuildPromptSchema: """Tests for the Hermes prompt-schema builder wrapper. - The wrapper delegates to conductor.providers._schema.build_hermes_legacy_prompt_schema - and converts SchemaDepthError into ValidationError with the exact legacy message and - suggestion. These tests pin the legacy behavior (description fallback, no required - inside array-item objects, collapsed array-of-array items) and the depth boundary. + The wrapper delegates to the shared recursive builder in + conductor.providers._schema and converts SchemaDepthError into ValidationError + with the exact message and suggestion. These tests pin the shared + semantics (no description fallback, required inside array-item objects, + recursive array-of-array items) and the depth boundary. """ def _chain_schema(self, levels: int) -> dict[str, OutputField]: @@ -852,14 +853,14 @@ def _chain_schema(self, levels: int) -> dict[str, OutputField]: schema = {"nested": OutputField(type="object", properties=schema)} return schema - def test_build_prompt_schema_top_level_description_fallback(self) -> None: - """Top-level fields without explicit descriptions get the legacy fallback.""" + def test_build_prompt_schema_omits_missing_description(self) -> None: + """Top-level fields without explicit descriptions must not synthesize a description.""" schema = {"answer": OutputField(type="string")} result = _build_prompt_schema(schema) - assert result == {"answer": {"type": "string", "description": "The answer field"}} + assert result == {"answer": {"type": "string"}} - def test_build_prompt_schema_array_item_object_has_no_required(self) -> None: - """Legacy Hermes: array object items include properties but no required.""" + def test_build_prompt_schema_array_item_object_has_required(self) -> None: + """Array item schemas must include the required property names.""" schema = { "items": OutputField( type="array", @@ -873,11 +874,11 @@ def test_build_prompt_schema_array_item_object_has_no_required(self) -> None: ) } result = _build_prompt_schema(schema) - assert "required" not in result["items"]["items"] + assert result["items"]["items"]["required"] == ["key", "value"] assert "properties" in result["items"]["items"] - def test_build_prompt_schema_array_of_arrays_collapsed(self) -> None: - """Legacy Hermes behavior: array-of-array items collapse to bare {type: array}.""" + def test_build_prompt_schema_array_of_arrays_recurses(self) -> None: + """Array item schemas must recurse and expose the inner item type.""" schema = { "matrix": OutputField( type="array", @@ -885,11 +886,12 @@ def test_build_prompt_schema_array_of_arrays_collapsed(self) -> None: ) } result = _build_prompt_schema(schema) - assert result["matrix"]["items"] == {"type": "array"} + assert result["matrix"]["items"]["type"] == "array" + assert result["matrix"]["items"]["items"]["type"] == "number" def test_build_prompt_schema_exceeds_max_depth(self) -> None: """Depths above _MAX_SCHEMA_DEPTH raise ValidationError with the exact - legacy message and suggestion.""" + shared message and suggestion.""" # A 12-level object chain reaches depth 11, which exceeds the default max depth of 10. overly_nested = self._chain_schema(_MAX_SCHEMA_DEPTH + 2) with pytest.raises(ValidationError) as exc_info: @@ -900,12 +902,51 @@ def test_build_prompt_schema_exceeds_max_depth(self) -> None: assert error.args[0] == expected assert error.suggestion == "Simplify your output schema to reduce nesting depth" + def test_nested_array_object_descriptions_preserved(self) -> None: + """Explicit descriptions must survive at every level of nested array schemas.""" + schema = { + "outer": OutputField( + type="array", + description="Outer array", + items=OutputField( + type="object", + description="Object item", + properties={ + "inner": OutputField( + type="array", + description="Inner array", + items=OutputField( + type="object", + description="Inner object", + properties={ + "name": OutputField(type="string", description="Name field"), + }, + ), + ), + }, + ), + ) + } + result = _build_prompt_schema(schema) + assert result["outer"]["description"] == "Outer array" + assert result["outer"]["items"]["description"] == "Object item" + assert result["outer"]["items"]["properties"]["inner"]["description"] == "Inner array" + assert ( + result["outer"]["items"]["properties"]["inner"]["items"]["description"] + == "Inner object" + ) + assert ( + result["outer"]["items"]["properties"]["inner"]["items"]["properties"]["name"][ + "description" + ] + == "Name field" + ) + def test_build_prompt_schema_array_item_depth_parity(self) -> None: - """Array object items must not consume an extra depth level. + """Shared depth counting: for array, properties inside the item start at depth 2. - Legacy Hermes passed the item's properties recursion the same depth as the - array field itself (depth+1 total). This pins that a chain reaching depth 10 - inside an array item is accepted, while a chain reaching depth 11 raises. + A chain of 8 object levels inside an array item (leaf at depth 10) is + accepted; a chain of 9 (leaf at depth 11) raises ValidationError. """ def chain_in_array_item(levels: int) -> dict[str, OutputField]: @@ -919,34 +960,41 @@ def chain_in_array_item(levels: int) -> dict[str, OutputField]: ) } - # _chain_schema(9) reaches depth 10 inside the array item: accepted. - _build_prompt_schema(chain_in_array_item(9)) + # _chain_schema(8) reaches depth 10 inside the array item: accepted. + _build_prompt_schema(chain_in_array_item(8)) - # _chain_schema(10) reaches depth 11: one level too deep. + # _chain_schema(9) reaches depth 11: one level too deep. with pytest.raises(ValidationError, match="exceeds maximum"): - _build_prompt_schema(chain_in_array_item(10)) + _build_prompt_schema(chain_in_array_item(9)) def test_build_prompt_schema_non_object_array_item_at_boundary_accepted(self) -> None: - """Non-object array items must not consume a depth level. + """Shared depth counting: every array item, including scalar items, consumes one level. - Requirement: legacy Hermes checked depth only when recursing into object - properties, so a chain reaching exactly _MAX_SCHEMA_DEPTH whose leaf is an - array of scalars (or an array of arrays) was accepted pre-refactor. The - shared builder must preserve that boundary: no error at depth 10, while a - chain one object level deeper still raises. + An object chain of 9 nested objects ending in scalar array fields has array items + at depth 10 (accepted); 10 nested objects pushes them to depth 11 (raises). A + pure chain of 10 nested arrays has its scalar leaf at depth 10 (accepted); 11 + nested arrays raises. """ - # 10 nested objects (depths 0..9) ending in non-object array fields at depth 10. + # 9 nested objects ending in scalar array fields: array items sit at depth 10. inner: dict[str, OutputField] = { "tags": OutputField(type="array", items=OutputField(type="string")), - "matrix": OutputField( - type="array", items=OutputField(type="array", items=OutputField(type="number")) - ), } - for _ in range(_MAX_SCHEMA_DEPTH): + for _ in range(_MAX_SCHEMA_DEPTH - 1): inner = {"nested": OutputField(type="object", properties=inner)} _build_prompt_schema(inner) - # One more object level pushes the object chain to depth 11: must raise. + # 10 nested objects push scalar array items to depth 11: raises. too_deep = {"nested": OutputField(type="object", properties=inner)} with pytest.raises(ValidationError, match="exceeds maximum"): _build_prompt_schema(too_deep) + + # Pure array chain: 10 nested arrays accepted, 11 raises. + def nested_array_chain(levels: int) -> OutputField: + leaf: OutputField = OutputField(type="string") + for _ in range(levels): + leaf = OutputField(type="array", items=leaf) + return leaf + + _build_prompt_schema({"matrix": nested_array_chain(_MAX_SCHEMA_DEPTH)}) + with pytest.raises(ValidationError, match="exceeds maximum"): + _build_prompt_schema({"matrix": nested_array_chain(_MAX_SCHEMA_DEPTH + 1)}) diff --git a/tests/test_providers/test_output_schema.py b/tests/test_providers/test_output_schema.py index ae061fc3..aaa05642 100644 --- a/tests/test_providers/test_output_schema.py +++ b/tests/test_providers/test_output_schema.py @@ -1,9 +1,9 @@ """Golden regression tests for provider output-schema wrappers. These tests pin the exact full-wrapper output of each provider's schema builder -against pre-refactor literals. Any behavioral change in the -shared schema builder or a provider wrapper that alters the serialized JSON will -cause these tests to fail. +against golden literals capturing the current expected behavior. Any behavioral +change in the shared schema builder or a provider wrapper that alters the +serialized JSON will cause these tests to fail. """ from __future__ import annotations @@ -294,8 +294,7 @@ def _serialize(actual: Any) -> str: "description": "A string scalar field with a description" }, "number_scalar": { - "type": "number", - "description": "The number_scalar field" + "type": "number" }, "nested_object": { "type": "object", @@ -306,8 +305,7 @@ def _serialize(actual: Any) -> str: "description": "Nested string field" }, "nested_number": { - "type": "number", - "description": "The nested_number field" + "type": "number" } }, "required": [ @@ -360,20 +358,16 @@ def _serialize(actual: Any) -> str: # Expected output for CopilotProvider._build_prompt_schema(missing_descriptions_schema). EXPECTED_COPILOT_MISSING_SCHEMA = """{ "string_scalar": { - "type": "string", - "description": "The string_scalar field" + "type": "string" }, "nested_object": { "type": "object", - "description": "The nested_object field", "properties": { "nested_string": { - "type": "string", - "description": "The nested_string field" + "type": "string" }, "nested_number": { - "type": "number", - "description": "The nested_number field" + "type": "number" } }, "required": [ @@ -383,24 +377,20 @@ def _serialize(actual: Any) -> str: }, "array_of_scalars": { "type": "array", - "description": "The array_of_scalars field", "items": { "type": "string" } }, "array_of_objects": { "type": "array", - "description": "The array_of_objects field", "items": { "type": "object", "properties": { "obj_key": { - "type": "string", - "description": "The obj_key field" + "type": "string" }, "obj_val": { - "type": "number", - "description": "The obj_val field" + "type": "number" } }, "required": [ @@ -411,7 +401,6 @@ def _serialize(actual: Any) -> str: }, "array_of_arrays": { "type": "array", - "description": "The array_of_arrays field", "items": { "type": "array", "items": { @@ -428,8 +417,7 @@ def _serialize(actual: Any) -> str: "description": "A string scalar field with a description" }, "number_scalar": { - "type": "number", - "description": "The number_scalar field" + "type": "number" }, "nested_object": { "type": "object", @@ -440,8 +428,7 @@ def _serialize(actual: Any) -> str: "description": "Nested string field" }, "nested_number": { - "type": "number", - "description": "The nested_number field" + "type": "number" } }, "required": [ @@ -471,14 +458,22 @@ def _serialize(actual: Any) -> str: "type": "number", "description": "The value" } - } + }, + "required": [ + "obj_key", + "obj_val" + ] } }, "array_of_arrays": { "type": "array", "description": "An array of arrays", "items": { - "type": "array" + "type": "array", + "items": { + "type": "number", + "description": "A number in nested array" + } } } }""" @@ -486,20 +481,16 @@ def _serialize(actual: Any) -> str: # Expected output for Hermes _build_prompt_schema(missing_descriptions_schema). EXPECTED_HERMES_MISSING_SCHEMA = """{ "string_scalar": { - "type": "string", - "description": "The string_scalar field" + "type": "string" }, "nested_object": { "type": "object", - "description": "The nested_object field", "properties": { "nested_string": { - "type": "string", - "description": "The nested_string field" + "type": "string" }, "nested_number": { - "type": "number", - "description": "The nested_number field" + "type": "number" } }, "required": [ @@ -509,33 +500,35 @@ def _serialize(actual: Any) -> str: }, "array_of_scalars": { "type": "array", - "description": "The array_of_scalars field", "items": { "type": "string" } }, "array_of_objects": { "type": "array", - "description": "The array_of_objects field", "items": { "type": "object", "properties": { "obj_key": { - "type": "string", - "description": "The obj_key field" + "type": "string" }, "obj_val": { - "type": "number", - "description": "The obj_val field" + "type": "number" } - } + }, + "required": [ + "obj_key", + "obj_val" + ] } }, "array_of_arrays": { "type": "array", - "description": "The array_of_arrays field", "items": { - "type": "array" + "type": "array", + "items": { + "type": "number" + } } } }""" @@ -692,12 +685,10 @@ def _serialize(actual: Any) -> str: def _make_copilot_provider() -> CopilotProvider: - """Return a CopilotProvider instance wired to a no-op stub handler.""" - - def stub_handler(agent: Any, prompt: str, context: dict[str, Any]) -> dict[str, Any]: - return {"result": "stub"} - - return CopilotProvider(mock_handler=stub_handler) + """Create a CopilotProvider with a no-op mock handler for schema tests.""" + return CopilotProvider( + mock_handler=AsyncMock(), + ) class TestClaudeOutputSchemaGolden: @@ -744,15 +735,16 @@ class TestCopilotOutputSchemaGolden: """Golden tests for CopilotProvider's prompt schema wrapper.""" def test_build_prompt_schema_rich_matches_baseline(self) -> None: - """Copilot prompt schema wrapper must be byte-for-byte identical to the pre-refactor - baseline for a schema with descriptions and nested structure.""" + """Copilot prompt schema wrapper must match the shared recursive builder output for + a schema with descriptions and nested structure; explicit descriptions are preserved.""" provider = _make_copilot_provider() actual = _serialize(provider._build_prompt_schema(rich_schema)) assert actual == EXPECTED_COPILOT_RICH_SCHEMA def test_build_prompt_schema_missing_descriptions_matches_baseline(self) -> None: - """Copilot prompt schema wrapper must be byte-for-byte identical to the pre-refactor - baseline for a schema without descriptions, including description fallbacks.""" + """Copilot prompt schema wrapper must match the current prompt schema output for a + schema without descriptions - fields without an explicit description emit no + description key.""" provider = _make_copilot_provider() actual = _serialize(provider._build_prompt_schema(missing_descriptions_schema)) assert actual == EXPECTED_COPILOT_MISSING_SCHEMA @@ -762,14 +754,16 @@ class TestHermesOutputSchemaGolden: """Golden tests for Hermes' module-level prompt schema wrapper.""" def test_build_prompt_schema_rich_matches_baseline(self) -> None: - """Hermes prompt schema wrapper must be byte-for-byte identical to the pre-refactor - baseline for a schema with descriptions, including legacy collapsed array-of-arrays.""" + """Hermes prompt schema wrapper must match the shared recursive builder output for + a schema with descriptions and fully recursive array items; explicit descriptions + survive at every nesting level.""" actual = _serialize(_build_prompt_schema(rich_schema)) assert actual == EXPECTED_HERMES_RICH_SCHEMA def test_build_prompt_schema_missing_descriptions_matches_baseline(self) -> None: - """Hermes prompt schema wrapper must be byte-for-byte identical to the pre-refactor - baseline for a schema without descriptions, including description fallbacks.""" + """Hermes prompt schema wrapper must match the current prompt schema output for a + schema without descriptions - fields without an explicit description emit no + description key.""" actual = _serialize(_build_prompt_schema(missing_descriptions_schema)) assert actual == EXPECTED_HERMES_MISSING_SCHEMA @@ -793,13 +787,13 @@ def test_build_output_format_missing_descriptions_matches_baseline(self) -> None class TestGoldenMutationGuard: """Negative guard proving the golden tests are sensitive to behavioral changes.""" - def test_mutated_copilot_literal_does_not_match(self) -> None: - """If the builder output is mutated (e.g., a description fallback is changed), the - golden assertion must fail so regressions are caught.""" + def test_golden_assertion_detects_missing_description(self) -> None: + """A schema whose only difference from `rich_schema` is a dropped description + must not satisfy the rich-schema golden assertion.""" provider = _make_copilot_provider() - actual = _serialize(provider._build_prompt_schema(rich_schema)) - mutated = EXPECTED_COPILOT_RICH_SCHEMA.replace( - '"description": "The number_scalar field"', - '"description": "The number_scalar field (mutated)"', - ) - assert actual != mutated + schema_without_description = { + **rich_schema, + "string_scalar": OutputField(type="string"), + } + actual = _serialize(provider._build_prompt_schema(schema_without_description)) + assert actual != EXPECTED_COPILOT_RICH_SCHEMA