test(profiles): two invariants for the truncated-JSON refusal, comment trimmed to the why

Parametrized refusal test (bare/fenced/uppercase-fenced truncated objects) asserts
profile.yaml is byte-identical afterwards; one prose test keeps the lenient
fallback contract. Replaces five near-duplicate tests from #104075.

Campaign tracker: https://github.com/NousResearch/hermes-agent/issues/104154
This commit is contained in:
Teknium
2026-09-06 02:25:27 -07:00
parent 1f16bbf108
commit 0edab1ac4d
2 changed files with 25 additions and 91 deletions
+2 -6
View File
@@ -166,12 +166,8 @@ def describe_profile(profile_name: str, *, overwrite: bool = False, timeout: Opt
raw = ""
parsed = _extract_json_blob(raw)
if parsed is None:
# A response that is JSON-SHAPED (starts with `{`, once code fences are stripped) but
# failed to parse is a malformed/truncated structured reply -- e.g. the aux model or
# transport cut it off mid-object -- not free-form prose. Persisting it verbatim writes
# a raw JSON fragment (literal `{`, `\n` escapes, a sentence chopped mid-word) into
# profile.yaml's description field (#104067). Only fall back to "whole reply is prose"
# when the reply never looked like JSON in the first place.
# JSON-shaped but unparseable = the requested object got cut off (#104067); the prose
# fallback below is only for models that never attempted JSON.
stripped = _FENCE_RE.sub("", raw.strip())
if stripped.startswith("{"):
logger.info(
+23 -85
View File
@@ -76,101 +76,39 @@ def test_describer_writes_description_with_auto_true(profile_env, monkeypatch):
assert meta["description_auto"] is True
def test_describer_rejects_truncated_json_response(profile_env, monkeypatch):
# Real-world shape (#104067): the aux model's response is cut off mid-object -- no
# closing brace/quote -- so `_extract_json_blob` can't parse it. The old fallback
# persisted this raw fragment verbatim as the description; it must now be refused.
@pytest.fixture
def registered_profile(profile_env, monkeypatch):
monkeypatch.setattr(profiles_mod, "profile_exists", lambda n: n == "myprof")
monkeypatch.setattr(profiles_mod, "normalize_profile_name", lambda n: n)
monkeypatch.setattr(profiles_mod, "get_profile_dir", lambda n: profile_env)
return profile_env
truncated = '{\n "description": "Generalist agent that writes and debugs code, orchestrates autonomous sub-agents, and automates macOS/App'
with _patch_aux_client(truncated), patch(
"agent.auxiliary_client.get_auxiliary_extra_body", return_value={}
):
outcome = describer.describe_profile("myprof")
@pytest.mark.parametrize("raw", [
'{\n "description": "Generalist agent that writes and debugs code, orchestrates autonomous sub-agents, and automates macOS/App',
'{\n "desc',
'```json\n{\n "description": "Generalist agent that writes and debugs cod',
'```JSON\n{\n "description": "Generalist agent that writes and debugs cod',
])
def test_describer_refuses_json_shaped_reply_that_does_not_parse(registered_profile, raw):
"""A reply that started as the requested JSON object but was cut off (#104067) is not prose:
it must be refused and leave profile.yaml untouched -- including behind an uppercase fence."""
profiles_mod.write_profile_meta(registered_profile, description="previous", description_auto=True)
before = (registered_profile / "profile.yaml").read_bytes()
with _patch_aux_client(raw), patch("agent.auxiliary_client.get_auxiliary_extra_body", return_value={}):
outcome = describer.describe_profile("myprof", overwrite=True)
assert outcome.ok is False
assert "malformed" in outcome.reason.lower() or "truncated" in outcome.reason.lower()
# Nothing was written: no profile.yaml, or if one exists it has no description key.
meta = profiles_mod.read_profile_meta(profile_env)
assert not meta.get("description")
assert (registered_profile / "profile.yaml").read_bytes() == before
def test_describer_rejects_json_shaped_response_with_no_closing_brace_at_all(profile_env, monkeypatch):
# Even more truncated: cut off right after the opening brace, before any key.
monkeypatch.setattr(profiles_mod, "profile_exists", lambda n: n == "myprof")
monkeypatch.setattr(profiles_mod, "normalize_profile_name", lambda n: n)
monkeypatch.setattr(profiles_mod, "get_profile_dir", lambda n: profile_env)
with _patch_aux_client("{\n \"desc"), patch(
"agent.auxiliary_client.get_auxiliary_extra_body", return_value={}
):
def test_describer_still_accepts_plain_prose_fallback(registered_profile):
"""A reply that never looked like JSON keeps the lenient one-paragraph prose fallback."""
with _patch_aux_client("Writes and debugs Python codebases.\n\nSecond paragraph is dropped."), \
patch("agent.auxiliary_client.get_auxiliary_extra_body", return_value={}):
outcome = describer.describe_profile("myprof")
assert outcome.ok is False
meta = profiles_mod.read_profile_meta(profile_env)
assert not meta.get("description")
def test_describer_rejects_fenced_json_shaped_truncated_response(profile_env, monkeypatch):
# A ```json fence around a truncated object must still be recognized as JSON-shaped
# after fence stripping, not fall through to the raw-text fallback.
monkeypatch.setattr(profiles_mod, "profile_exists", lambda n: n == "myprof")
monkeypatch.setattr(profiles_mod, "normalize_profile_name", lambda n: n)
monkeypatch.setattr(profiles_mod, "get_profile_dir", lambda n: profile_env)
fenced_truncated = '```json\n{\n "description": "Generalist agent that writes and debugs cod'
with _patch_aux_client(fenced_truncated), patch(
"agent.auxiliary_client.get_auxiliary_extra_body", return_value={}
):
outcome = describer.describe_profile("myprof")
assert outcome.ok is False
meta = profiles_mod.read_profile_meta(profile_env)
assert not meta.get("description")
def test_describer_rejects_uppercase_fenced_json_shaped_truncated_response(profile_env, monkeypatch):
# Regression for review feedback on #104075: an uppercase ```JSON fence must be
# stripped the same as a lowercase one. Before, _FENCE_RE was case-sensitive, so
# stripping this left "JSON\n{...}" -- which doesn't start with "{" -- and the
# truncated fragment fell through to the raw-text-as-prose fallback and got persisted
# anyway, defeating the fix for this fence variant.
monkeypatch.setattr(profiles_mod, "profile_exists", lambda n: n == "myprof")
monkeypatch.setattr(profiles_mod, "normalize_profile_name", lambda n: n)
monkeypatch.setattr(profiles_mod, "get_profile_dir", lambda n: profile_env)
fenced_truncated = '```JSON\n{\n "description": "Generalist agent that writes and debugs cod'
with _patch_aux_client(fenced_truncated), patch(
"agent.auxiliary_client.get_auxiliary_extra_body", return_value={}
):
outcome = describer.describe_profile("myprof")
assert outcome.ok is False
meta = profiles_mod.read_profile_meta(profile_env)
assert not meta.get("description")
def test_describer_still_accepts_plain_prose_fallback(profile_env, monkeypatch):
# Regression guard: a model that ignores the JSON instruction and just replies in
# plain prose (never looked JSON-shaped) must still hit the existing lenient
# raw-text fallback -- this fix must not make the describer stricter than before
# for genuinely non-JSON replies.
monkeypatch.setattr(profiles_mod, "profile_exists", lambda n: n == "myprof")
monkeypatch.setattr(profiles_mod, "normalize_profile_name", lambda n: n)
monkeypatch.setattr(profiles_mod, "get_profile_dir", lambda n: profile_env)
with _patch_aux_client("Writes and debugs Python codebases end to end."), patch(
"agent.auxiliary_client.get_auxiliary_extra_body", return_value={}
):
outcome = describer.describe_profile("myprof")
assert outcome.ok, outcome.reason
assert outcome.description == "Writes and debugs Python codebases end to end."
meta = profiles_mod.read_profile_meta(profile_env)
assert meta["description"] == "Writes and debugs Python codebases end to end."
assert meta["description_auto"] is True
assert outcome.description == "Writes and debugs Python codebases."
assert profiles_mod.read_profile_meta(registered_profile)["description"] == outcome.description
def test_describer_refuses_to_overwrite_user_authored(profile_env, monkeypatch):