hermes-agent/tests/agent/test_gemini_schema.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

169 lines
6.4 KiB
Python

"""Tests for agent.gemini_schema — OpenAI→Gemini tool parameter translation."""
from agent.gemini_schema import (
sanitize_gemini_schema,
sanitize_gemini_tool_parameters,
)
class TestSanitizeGeminiSchema:
def test_strips_unknown_top_level_keys(self):
"""$schema / additionalProperties etc. must not reach Gemini."""
schema = {
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": False,
"properties": {"foo": {"type": "string"}},
}
cleaned = sanitize_gemini_schema(schema)
assert "$schema" not in cleaned
assert "additionalProperties" not in cleaned
assert cleaned["type"] == "object"
assert cleaned["properties"] == {"foo": {"type": "string"}}
def test_stringifies_integer_enum_to_satisfy_gemini(self):
"""Gemini rejects numeric enum metadata unless values are strings.
Regression for the Discord tool's ``auto_archive_duration``:
``{type: integer, enum: [60, 1440, 4320, 10080]}`` caused
Gemini HTTP 400 INVALID_ARGUMENT
"Invalid value ... (TYPE_STRING), 60" on every request that
shipped the full tool catalog to generativelanguage.googleapis.com.
"""
schema = {
"type": "integer",
"enum": [60, 1440, 4320, 10080],
"description": "Minutes (60, 1440, 4320, 10080).",
}
cleaned = sanitize_gemini_schema(schema)
assert cleaned["type"] == "integer"
assert cleaned["enum"] == ["60", "1440", "4320", "10080"]
# Description remains useful model guidance.
assert cleaned["description"].startswith("Minutes")
def test_stringifies_nested_integer_enum_inside_properties(self):
"""The fix must apply recursively — the Discord case is nested."""
schema = {
"type": "object",
"properties": {
"auto_archive_duration": {
"type": "integer",
"enum": [60, 1440, 4320, 10080],
"description": "Thread archive duration in minutes.",
},
"status": {
"type": "string",
"enum": ["active", "archived"],
},
},
}
cleaned = sanitize_gemini_schema(schema)
props = cleaned["properties"]
# Integer enum is retained as Gemini-compatible string metadata...
assert props["auto_archive_duration"]["type"] == "integer"
assert props["auto_archive_duration"]["enum"] == ["60", "1440", "4320", "10080"]
# ...but the sibling string enum is preserved.
assert props["status"]["enum"] == ["active", "archived"]
def test_non_dict_input_returns_empty(self):
assert sanitize_gemini_schema(None) == {}
assert sanitize_gemini_schema("not a schema") == {}
assert sanitize_gemini_schema([1, 2, 3]) == {}
class TestRequiredPropertyPruning:
"""Gemini rejects ``required`` names missing from the node's ``properties``.
Regression for the Kilo-Org/kilocode#11955 bug class: MCP servers (e.g.
the GitHub remote MCP) emit array item schemas whose ``required`` lists
reference properties that don't exist in the same node — Google fails the
entire GenerateContentRequest with HTTP 400 "property is not defined".
"""
def test_prunes_inside_array_items(self):
"""The exact shape from the GitHub MCP report — nested in items."""
schema = {
"type": "object",
"properties": {
"issue_fields": {
"type": "array",
"items": {
"type": "object",
"required": ["field_id", "value"],
},
},
},
"required": ["issue_fields"],
}
cleaned = sanitize_gemini_schema(schema)
items = cleaned["properties"]["issue_fields"]["items"]
assert "required" not in items
# Top-level required is valid and survives.
assert cleaned["required"] == ["issue_fields"]
def test_valid_required_untouched(self):
schema = {
"type": "object",
"properties": {"a": {"type": "string"}, "b": {"type": "integer"}},
"required": ["a", "b"],
}
cleaned = sanitize_gemini_schema(schema)
assert cleaned["required"] == ["a", "b"]
def test_prunes_inside_anyof_branches(self):
schema = {
"anyOf": [
{
"type": "object",
"properties": {"x": {"type": "string"}},
"required": ["x", "ghost"],
},
{"type": "object", "required": ["orphan"]},
]
}
cleaned = sanitize_gemini_schema(schema)
assert cleaned["anyOf"][0]["required"] == ["x"]
assert "required" not in cleaned["anyOf"][1]
class TestSanitizeGeminiToolParameters:
def test_empty_parameters_return_valid_object_schema(self):
"""Gemini requires ``parameters`` to be a valid object schema."""
cleaned = sanitize_gemini_tool_parameters({})
assert cleaned == {"type": "object", "properties": {}}
def test_discord_create_thread_parameters_no_longer_trip_gemini(self):
"""End-to-end regression: the exact shape that was rejected in prod."""
params = {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["create_thread"]},
"auto_archive_duration": {
"type": "integer",
"enum": [60, 1440, 4320, 10080],
"description": "Thread archive duration in minutes "
"(create_thread, default 1440).",
},
},
"required": ["action"],
}
cleaned = sanitize_gemini_tool_parameters(params)
aad = cleaned["properties"]["auto_archive_duration"]
# The field that triggered the Gemini 400 is now string metadata.
assert aad["enum"] == ["60", "1440", "4320", "10080"]
# Type + description survive so the model still knows what to send.
assert aad["type"] == "integer"
assert "1440" in aad["description"]
# And the string-enum sibling is untouched.
assert cleaned["properties"]["action"]["enum"] == ["create_thread"]