HERMES-GIELIE/tests/test_pr1318_context_length_fallback.py

110 lines
4.9 KiB
Python
Raw Normal View History

fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
"""Regression test for #1318 fallback (#1344 follow-up).
PR #1318 / #1341 / a5c10d5 (in v0.50.246) persisted context_length to the
session when agent.context_compressor was present. But for fresh agents or
interrupted streams, context_compressor may be absent or report 0 — leaving
the context-ring indicator showing 0% even with the writer in place.
This follow-up adds a fallback to agent.model_metadata.get_model_context_length()
that resolves the model's static context window when the compressor didn't.
Sourced from @jasonjcwu's PR #1344, extracted into a focused follow-up.
Tests:
1. Writer block contains the fallback after the compressor block
2. Fallback gates on s.context_length being 0/falsy
3. Fallback uses agent.model + agent.base_url
4. Fallback exception is silently swallowed (older agent builds)
5. Fallback runs before s.save() so the value is persisted
"""
import re
from pathlib import Path
STREAMING = Path(__file__).resolve().parent.parent / "api" / "streaming.py"
def _persistence_block():
"""Return the source range covering the post-merge per-turn save block."""
src = STREAMING.read_text(encoding="utf-8")
Release v0.51.230 — Release GX (stage-p14 — extract <think> to m.reasoning #3455 + LLM Wiki last-writer #1257) (#3466) * Release v0.51.230 (stage-p14): extract <think> to m.reasoning #3455 + LLM Wiki last-writer (#1257) Salvage of #3455 (@gsurenull): dropped the stale api/config.py bits (MiniMax-M3 + SCHEMA_VERSION 3->4 — both already on master via #3374). Kept the two genuine fixes: (1) _splitThinkFromContent persist-path extraction of inline <think> blocks into m.reasoning (fixes 30-50% session bloat for reasoning-only providers like MiniMax-M3); (2) LLM Wiki status Last-writer 3-tier fallback (was always 'Not available' since #1257). Added 9 Node-driven think-split regression tests (data-loss guards: content-before/after preserved, unclosed blocks intact, lookalike tags not extracted). * fix(#3455): renderer-matching think extraction + wiki symlink/bounded-read guards (Codex review) Codex review of stage-p14 found 3 SILENT bugs, all fixed: (1) DATA-LOSS: _splitThinkFromContent's Pass-2 whole-body scan extracted a CLOSED literal <think>...</think> from visible prose/code (e.g. inside a fenced code block) into m.reasoning, emptying it — more aggressive than the renderer (which only strips LEADING blocks). Removed Pass 2; extraction now matches _streamDisplay semantics (leading-only, loop captures consecutive leading blocks). +fenced-code regression test. (2) PRIVACY: _llm_wiki_last_writer followed symlinked .md pages resolving OUTSIDE the wiki (is_file follows symlinks), leaking external frontmatter. Now requires resolved path under wiki_root. +symlink-containment regression test. (3) CONTRACT/PERF: replaced full read_text() with bounded line-by-line reads (frontmatter block only / capped log-heading scan), never page bodies. * fix(#3455): think-split is leading-single (renderer-matching) + fix 2 stale source-match tests Codex re-review finding #2: looping consecutive leading blocks diverged from the renderer (_streamDisplay/_parseStreamState strip ONE leading block). Now extracts exactly one leading block. Also updated 2 tests that asserted pre-split implementation strings: test_live_stream_tokens_persist (content:assistantText -> content:split.content, invariant preserved) and the consecutive-blocks test. NOTE: Codex finding #1 (client-only split doesn't persist server-side) is a separate architectural decision pending Nathan. * feat(#3455): split inline <think> server-side before s.save() so persisted file is compacted (Codex #1) Codex finding #1: the think-split was client-only, so the SAVED session file still carried inline <think> blocks (bloat) — the fix only compacted the browser copy. Added _split_thinking_from_content (api/streaming.py), a server-side twin of the JS helper with identical leading-only/single-block semantics, applied to the final assistant message before s.save() (extended the existing reasoning-persist block). Merges with on_reasoning-stream reasoning. +8 backend-parity regression tests covering the mid-body-code-block data-loss guard, unclosed-intact, single-leading, none-content. * test: update 3 save-path source-assertion tests for #3455 server-side think-split The backend think-split (api/streaming.py reasoning-persist block) changed the literal code shape + grew the pre-save block, breaking 8 source-assertion tests that anchor on it: - test_sprint42: assert _rm['reasoning']=_reasoning_text -> now _merged_reasoning/_existing_reasoning + _split_thinking_from_content present (intent preserved: reasoning persisted before save). - test_pr1318 (6) + test_pr1341: re-anchored the locator from the changed 'if _reasoning_text and s.messages:' line to the stable 'Persist reasoning trace in the session' comment marker; bumped the 1341 byte-distance limit 15000->16000 (the test self-documents bumping on legit pre-save growth). All behavioral invariants (reasoning persisted + context fields before save) unchanged. --------- Co-authored-by: nesquena-hermes <[email protected]>
2026-06-03 05:48:35 +00:00
start = src.find("Persist reasoning trace in the session")
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
assert start != -1, "Reasoning trace marker not found in streaming.py"
2026-06-25 14:36:23 +00:00
save_match = re.search(r"\n[ \t]+s\.save\(\)", src[start:])
assert save_match is not None, "s.save() not found after the reasoning trace marker"
end = start + save_match.start()
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
# Include the s.save() line so we can verify ordering
end = src.find("\n", end + 1)
return src[start:end]
def test_fallback_uses_model_metadata():
"""Block must import and call get_model_context_length on missing compressor data."""
block = _persistence_block()
assert "from agent.model_metadata import get_model_context_length" in block, (
"Fallback must import get_model_context_length from agent.model_metadata"
)
assert "get_model_context_length(" in block, (
"Fallback must call get_model_context_length()"
)
def test_fallback_gates_on_falsy_context_length():
"""Fallback runs when the compressor didn't populate s.context_length.
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
The gate must still check s.context_length being falsy — if the compressor
set context_length but it was 0, we want the fallback to fire. (#3256/#3263
widened the gate to ALSO fire on `_skip_cc_cl` — a non-default model whose
compressor carried the stale global cap — so the gate is now
`(not getattr(s, 'context_length', 0)) or _skip_cc_cl`. The falsy-check
remains; this test asserts that invariant is intact, not the exact spelling.)
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
"""
block = _persistence_block()
# The conditional must still reference s.context_length being falsy.
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
assert (
"not getattr(s, 'context_length'" in block
or "not s.context_length" in block
), "Fallback must still gate on s.context_length being falsy"
fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348) * fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted context_length on the session so the context-ring indicator survives page reloads. But the writer only fired when agent.context_compressor was present and reported a non-zero value. Fresh agents, interrupted streams, or compressors without the attribute would still leave s.context_length=0 — and the indicator would still show 0% on reload. This follow-up adds a fallback that calls agent.model_metadata.get_model_context_length(model, base_url) when the compressor didn't populate the value. The function returns a sensible static context window for any known model (with a 256K default for unknown models). Wrapped in a broad try/except because older hermes-agent builds may not expose the helper. Sourced from PR #1344 (@jasonjcwu) — extracted into this focused follow-up after #1344 was closed as superseded by #1341. Adds 6 structural tests covering: import + call presence, falsy-gate, agent.model/base_url passing, exception swallowing, save() ordering, result assignment. Closes the data-flow gap in #1318 for the compressor-missing case. * test: relax pr1341 block-size assertion to accommodate the new fallback --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 17:27:56 +00:00
def test_fallback_passes_model_and_base_url():
"""Fallback must source the model and base_url from the agent itself."""
block = _persistence_block()
# Must reference both agent.model and agent.base_url in the call
assert "agent, 'model'" in block, "Fallback must read agent.model"
assert "agent, 'base_url'" in block, "Fallback must read agent.base_url"
def test_fallback_exception_is_swallowed():
"""If get_model_context_length raises (older agent build, network error,
bad provider config), the fallback must not break s.save()."""
block = _persistence_block()
# Must wrap the import + call in try/except
fallback_section = block[block.find("Fallback"):]
assert "try:" in fallback_section, "Fallback must use try/except"
# except Exception: pass-style — old agent builds may not have this helper at all
assert "except Exception:" in fallback_section, (
"Fallback must catch broad Exception (older agent builds may not have the helper)"
)
def test_fallback_runs_before_save():
"""The fallback must mutate s.context_length BEFORE s.save() so the value lands on disk."""
block = _persistence_block()
fallback_idx = block.find("get_model_context_length")
save_idx = block.rfind("s.save()")
assert fallback_idx != -1 and save_idx != -1
assert fallback_idx < save_idx, (
"Fallback must run BEFORE s.save() — otherwise the resolved context_length "
"is not persisted to the session JSON."
)
def test_fallback_assigns_context_length_when_resolved():
"""The fallback must assign s.context_length when get_model_context_length returns a non-zero value."""
block = _persistence_block()
fallback_section = block[block.find("Fallback"):]
# Must have an `if _resolved_cl:` guard followed by `s.context_length = _resolved_cl`
assert "_resolved_cl" in fallback_section, "Fallback must capture the result"
assert "s.context_length = _resolved_cl" in fallback_section, (
"Fallback must assign the resolved value to s.context_length"
)