honcho/tests/crud
Eugene Eisenstein 03253d7a08
fix(deriver): strip NUL bytes from model-generated observations (#1095)
* fix(deriver): strip NUL bytes from model-generated observations

Postgres rejects NUL (0x00) in text columns and in jsonb strings. API
ingress has always stripped it from user-supplied content, but the
deriver's own output did not go through any equivalent: a model can emit
a \u0000 escape in its tool-call arguments, which the JSON parser decodes
into a real NUL byte. Seen in production when models transcribe shell
output (`tr '\x00' '\n'`) or Windows paths (`c:\<NUL>users\amal`).

The NUL reached the exact-content dedup pre-fetch in create_documents as
a bind parameter, so the query raised DataError before any row was
written and the whole batch for that observer was dropped.

Strip in _normalized_observation and _normalized_observation_input --
the points that already normalize text for persistence and embedding --
so the embedded text matches the stored text. premises and sources are
covered too, since they ride along in internal_metadata. The emptiness
check now runs after normalization, because str.strip() does not remove
NUL and all-NUL content would otherwise be stored as an empty string.

DocumentCreate.content gets a mode="before" validator as a backstop for
callers that bypass those paths; running before the length constraint
makes all-NUL content fail min_length rather than silently empty out.

The NUL helpers move out of schemas/api.py into utils/sanitization.py as
a single recursive strip_nul, so ingress and internal paths share one
implementation. It is overloaded to keep str -> str for the callers that
chain .strip(), and passes None through so optional fields need no guard.

Fixes HONCHO-4XZ

* fix: broaden nul strip check

* chore: code simplification

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-31 13:32:43 -04:00
..
test_document.py fix(embedding): truncate in batch embed and return results breakdown (#1019) 2026-08-20 11:42:38 -04:00
test_get_or_create_retry_invalidation.py Scopes Phase 2a: scope-kind peers, guardrails, and scopes CRUD routes (#884) 2026-08-12 16:21:40 -04:00
test_peer_card.py sdks: add set peer card function (#371) 2026-02-09 15:35:57 -05:00
test_representation_manager.py fix(deriver): strip NUL bytes from model-generated observations (#1095) 2026-08-31 13:32:43 -04:00
test_session.py fix(crud): preserve joined_at for active session peers (#1059) 2026-08-25 10:51:59 -04:00
test_session_scope_clauses.py fix(scopes): scope observer sessions in SQL instead of a fetched name list (#1065) 2026-08-25 13:24:31 -04:00
test_workspace.py feat: implement async workspace deletion with active session checks (#378) 2026-02-12 18:48:00 -05:00