session.resume already unwraps steer rows; the REST projection the desktop hydrates from after a turn did not, so a correction sent during a tool swapped its bubble for the model-facing OUT-OF-BAND marker text.
517 lines
18 KiB
Python
517 lines
18 KiB
Python
"""Public Codex commentary is a display projection, never a replay mutation."""
|
|
|
|
from hermes_cli.web_routers.sessions import _project_for_display
|
|
from tui_gateway.server import _history_to_messages
|
|
from agent.history_commentary import visible_commentary
|
|
from hermes_constants import get_hermes_home_override
|
|
|
|
import asyncio
|
|
import pytest
|
|
|
|
|
|
def _row(text="I will inspect the files."):
|
|
return {
|
|
"role": "assistant",
|
|
"content": "",
|
|
"reasoning": f"Private summary.\n\n{text}",
|
|
"reasoning_content": f"Private summary.\n\n{text}",
|
|
"codex_message_items": [
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"phase": "commentary",
|
|
"content": [{"type": "output_text", "text": text}],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"phase": "analysis",
|
|
"content": [{"type": "output_text", "text": "private analysis"}],
|
|
},
|
|
],
|
|
"tool_calls": [
|
|
{
|
|
"id": "call-1",
|
|
"type": "function",
|
|
"function": {"name": "terminal", "arguments": "{}"},
|
|
}
|
|
],
|
|
"timestamp": 1,
|
|
}
|
|
|
|
|
|
def test_rest_history_separates_public_commentary_without_changing_source():
|
|
row = _row()
|
|
projected = _project_for_display([row])[0]
|
|
|
|
assert projected["display_commentary"] == ["I will inspect the files."]
|
|
assert projected["display_reasoning"] == "Private summary."
|
|
assert projected["codex_message_items"] == row["codex_message_items"]
|
|
assert projected["tool_calls"] == row["tool_calls"]
|
|
assert row["reasoning"] == "Private summary.\n\nI will inspect the files."
|
|
|
|
|
|
def test_gateway_history_uses_same_display_projection():
|
|
row = _row()
|
|
projected = _history_to_messages([row])[0]
|
|
|
|
assert projected["display_commentary"] == ["I will inspect the files."]
|
|
assert projected["display_reasoning"] == "Private summary."
|
|
assert projected["codex_message_items"] == row["codex_message_items"]
|
|
assert row["reasoning"].endswith("I will inspect the files.")
|
|
|
|
|
|
def test_rest_history_shows_a_steer_as_the_users_own_words():
|
|
from agent.prompt_builder import steer_user_row
|
|
|
|
row = steer_user_row("fix the header too")
|
|
projected = _project_for_display([row])[0]
|
|
|
|
# Same words session.resume shows; the model-facing marker stays in the source.
|
|
assert projected["display_content"] == "fix the header too"
|
|
assert _history_to_messages([row])[0]["text"] == "fix the header too"
|
|
assert row["content"].startswith("[OUT-OF-BAND USER MESSAGE")
|
|
|
|
|
|
def test_history_applies_live_stripping_and_redaction_without_changing_raw_items(
|
|
monkeypatch,
|
|
):
|
|
monkeypatch.setattr("agent.redact._redact_enabled", lambda: True)
|
|
raw = (
|
|
"Start. <think>private scratchpad</think> Key=" + "sk-" + "demo0123456789abcdef"
|
|
)
|
|
row = _row(raw)
|
|
|
|
for projected in (_project_for_display([row])[0], _history_to_messages([row])[0]):
|
|
assert projected["display_commentary"] == [visible_commentary(raw)]
|
|
assert "<think>" not in projected["display_commentary"][0]
|
|
assert "sk-demo0123456789abcdef" not in projected["display_commentary"][0]
|
|
assert projected["display_reasoning"] == "Private summary."
|
|
assert projected["codex_message_items"] == row["codex_message_items"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"disabled",
|
|
[
|
|
{"show_commentary": False},
|
|
{"show_commentary": "false"},
|
|
{"interim_assistant_messages": False},
|
|
],
|
|
)
|
|
def test_owner_profile_settings_apply_to_rest_and_rpc(monkeypatch, tmp_path, disabled):
|
|
from hermes_cli import config as config_mod
|
|
|
|
enabled_home, disabled_home = tmp_path / "enabled", tmp_path / "disabled"
|
|
observed = []
|
|
|
|
def config():
|
|
home = get_hermes_home_override()
|
|
observed.append(str(home))
|
|
return {"display": {} if str(home) == str(enabled_home) else disabled}
|
|
|
|
monkeypatch.setattr(config_mod, "load_config", config)
|
|
row = _row()
|
|
for adapter in (
|
|
lambda home: _project_for_display([row], home=home)[0],
|
|
lambda home: _history_to_messages([row], profile_home=home)[0],
|
|
):
|
|
assert adapter(enabled_home)["display_commentary"] == [
|
|
"I will inspect the files."
|
|
]
|
|
hidden = adapter(disabled_home)
|
|
assert hidden["display_commentary"] == []
|
|
assert hidden["display_reasoning"] == "Private summary."
|
|
assert hidden["reasoning"] == row["reasoning"]
|
|
assert adapter(enabled_home)["display_reasoning"] == "Private summary."
|
|
assert observed == [str(enabled_home), str(disabled_home), str(enabled_home)] * 2
|
|
|
|
|
|
def test_rest_pages_bind_the_history_owner_for_messages_and_around(
|
|
monkeypatch, tmp_path
|
|
):
|
|
from hermes_cli import config as config_mod
|
|
from hermes_cli.web_routers import sessions
|
|
import hermes_state_timeline
|
|
|
|
row = _row()
|
|
homes = {name: tmp_path / name for name in ("visible", "hidden")}
|
|
monkeypatch.setattr(
|
|
sessions, "_cron_profile_home", lambda profile: (profile, homes[profile])
|
|
)
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda: {
|
|
"display": {
|
|
"show_commentary": str(get_hermes_home_override())
|
|
== str(homes["visible"])
|
|
}
|
|
},
|
|
)
|
|
|
|
class FakeDB:
|
|
def resolve_session_id(self, sid):
|
|
return sid
|
|
|
|
def resolve_resume_session_id(self, sid):
|
|
return sid
|
|
|
|
def get_messages(self, *args, **kwargs):
|
|
return [row]
|
|
|
|
monkeypatch.setattr(
|
|
sessions, "_with_db", lambda profile, fn, read_only: fn(FakeDB())
|
|
)
|
|
monkeypatch.setattr(sessions, "_timeline_session_id", lambda db, sid, owner: sid)
|
|
monkeypatch.setattr(
|
|
hermes_state_timeline,
|
|
"get_session_messages_around",
|
|
lambda *args, **kwargs: {"messages": [row], "pagination": {}},
|
|
)
|
|
|
|
async def exercise():
|
|
for profile, expected in (
|
|
("visible", ["I will inspect the files."]),
|
|
("hidden", []),
|
|
):
|
|
page = await sessions.get_session_messages(
|
|
"sid",
|
|
profile=profile,
|
|
limit=120,
|
|
offset=0,
|
|
order="latest",
|
|
include_compacted=True,
|
|
)
|
|
around = await sessions.get_session_messages_around(
|
|
"sid", row_id=1, profile=profile, limit=120
|
|
)
|
|
assert page["messages"][0]["display_commentary"] == expected
|
|
assert around["messages"][0]["display_commentary"] == expected
|
|
assert page["profile"] == around["profile"] == profile
|
|
|
|
asyncio.run(exercise())
|
|
|
|
|
|
def test_unscoped_rest_history_uses_custom_home_of_its_database(monkeypatch, tmp_path):
|
|
from hermes_cli import config as config_mod
|
|
from hermes_cli.web_routers import sessions
|
|
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
|
import hermes_state_timeline
|
|
|
|
custom, default = tmp_path / "custom", tmp_path / "default"
|
|
monkeypatch.setattr(
|
|
sessions, "_cron_profile_home", lambda profile: ("default", default)
|
|
)
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda: {
|
|
"display": {
|
|
"show_commentary": str(get_hermes_home_override()) != str(custom)
|
|
}
|
|
},
|
|
)
|
|
row = _row()
|
|
|
|
class FakeDB:
|
|
def resolve_session_id(self, sid):
|
|
return sid
|
|
|
|
def resolve_resume_session_id(self, sid):
|
|
return sid
|
|
|
|
def get_messages(self, *args, **kwargs):
|
|
return [row]
|
|
|
|
monkeypatch.setattr(
|
|
sessions, "_with_db", lambda profile, fn, read_only: fn(FakeDB())
|
|
)
|
|
monkeypatch.setattr(sessions, "_timeline_session_id", lambda db, sid, owner: sid)
|
|
monkeypatch.setattr(
|
|
hermes_state_timeline,
|
|
"get_session_messages_around",
|
|
lambda *args, **kwargs: {"messages": [row], "pagination": {}},
|
|
)
|
|
|
|
async def exercise():
|
|
page = await sessions.get_session_messages(
|
|
"sid",
|
|
profile=None,
|
|
limit=120,
|
|
offset=0,
|
|
order="latest",
|
|
include_compacted=True,
|
|
)
|
|
around = await sessions.get_session_messages_around(
|
|
"sid",
|
|
row_id=1,
|
|
profile=None,
|
|
limit=120,
|
|
)
|
|
assert page["messages"][0]["display_commentary"] == []
|
|
assert around["messages"][0]["display_commentary"] == []
|
|
|
|
token = set_hermes_home_override(str(custom))
|
|
try:
|
|
asyncio.run(exercise())
|
|
finally:
|
|
reset_hermes_home_override(token)
|
|
|
|
|
|
def test_cold_resume_uses_the_resumed_session_home(monkeypatch, tmp_path):
|
|
from hermes_cli import config as config_mod
|
|
from tui_gateway.server import _Resume
|
|
|
|
home = tmp_path / "disabled"
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda: {
|
|
"display": {"show_commentary": str(get_hermes_home_override()) != str(home)}
|
|
},
|
|
)
|
|
resume = _Resume.__new__(_Resume)
|
|
resume.omit_messages = False
|
|
resume.profile_home = home
|
|
|
|
assert resume.messages([_row()])[0]["display_commentary"] == []
|
|
|
|
|
|
def test_joined_canonical_commentary_uses_sanitized_display_copy(monkeypatch):
|
|
monkeypatch.setattr("agent.redact._redact_enabled", lambda: True)
|
|
raw = "Checking. Key=" + "sk-" + "demo0123456789abcdef"
|
|
row = _row(raw)
|
|
row["content"] = raw
|
|
|
|
visible = _project_for_display([row])[0]
|
|
assert visible["display_content"] == visible_commentary(raw)
|
|
assert visible["content"] == raw
|
|
assert visible["display_reasoning"] == "Private summary."
|
|
|
|
from agent.history_commentary import project_history_commentary
|
|
from hermes_cli import config as config_mod
|
|
|
|
monkeypatch.setattr(
|
|
config_mod, "load_config", lambda: {"display": {"show_commentary": False}}
|
|
)
|
|
hidden = project_history_commentary([row])[0]
|
|
assert hidden["display_content"] == visible_commentary(raw)
|
|
assert hidden["display_commentary"] == []
|
|
|
|
|
|
def test_only_exact_delimiter_bounded_copy_is_removed():
|
|
row = _row("Same phrase")
|
|
row["reasoning"] = "Summary mentioning Same phrase as an incidental substring."
|
|
projected = _project_for_display([row])[0]
|
|
assert projected["display_reasoning"] == row["reasoning"]
|
|
|
|
row["reasoning"] = "Before.\n\nSame phrase\n\nAfter."
|
|
assert _project_for_display([row])[0]["display_reasoning"] == "Before.\n\nAfter."
|
|
|
|
|
|
def test_foreign_and_malformed_items_are_not_public():
|
|
row = _row()
|
|
row["codex_message_items"][0]["role"] = "user"
|
|
assert "display_commentary" not in _project_for_display([row])[0]
|
|
row["codex_message_items"] = "{invalid JSON"
|
|
assert "display_commentary" not in _project_for_display([row])[0]
|
|
|
|
|
|
def test_mixed_canonical_content_is_sanitized_without_truncating_possible_final(
|
|
monkeypatch,
|
|
):
|
|
raw = "Checking. <think>hidden</think> key=" + "sk-" + "demo0123456789abcdef"
|
|
row = _row(raw)
|
|
row["content"] = raw + "\n\nFinal result."
|
|
monkeypatch.setattr("agent.redact._redact_enabled", lambda: True)
|
|
|
|
for projected in (_project_for_display([row])[0], _history_to_messages([row])[0]):
|
|
assert projected["display_commentary"] == []
|
|
assert projected["display_content"] == visible_commentary(row["content"])
|
|
assert raw not in projected["display_reasoning"]
|
|
|
|
from hermes_cli import config as config_mod
|
|
|
|
monkeypatch.setattr(
|
|
config_mod, "load_config", lambda: {"display": {"show_commentary": False}}
|
|
)
|
|
for projected in (_project_for_display([row])[0], _history_to_messages([row])[0]):
|
|
assert projected["display_commentary"] == []
|
|
assert projected["display_content"] == visible_commentary(row["content"])
|
|
assert row["content"].startswith(raw)
|
|
|
|
|
|
@pytest.mark.parametrize("final", ["Checking. The answer is 42.", "Checking."])
|
|
def test_stream_recovered_final_without_final_sidecar_is_authoritative(
|
|
monkeypatch, final
|
|
):
|
|
from types import SimpleNamespace
|
|
from agent.turn_finalizer import _close_transcript_tail
|
|
from hermes_cli import config as config_mod
|
|
|
|
row = _row("Checking.")
|
|
agent = SimpleNamespace(_db_flush_scan_prefix=None)
|
|
_close_transcript_tail(agent, [row], final, False, True)
|
|
assert row["content"] == final
|
|
for visible in (True, False):
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda visible=visible: {"display": {"show_commentary": visible}},
|
|
)
|
|
for projected in (
|
|
_project_for_display([row])[0],
|
|
_history_to_messages([row])[0],
|
|
):
|
|
assert (
|
|
projected.get(
|
|
"display_content", projected.get("content", projected.get("text"))
|
|
)
|
|
== final
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("final", ["Checking. The answer is 42.", "Checking."])
|
|
@pytest.mark.parametrize("phase", ["final", "final_answer", None])
|
|
def test_canonical_content_matching_final_item_is_never_stripped(
|
|
monkeypatch, final, phase
|
|
):
|
|
from hermes_cli import config as config_mod
|
|
|
|
row = _row("Checking.")
|
|
row["content"] = final
|
|
item = {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": final}],
|
|
}
|
|
if phase is not None:
|
|
item["phase"] = phase
|
|
row["codex_message_items"].append(item)
|
|
for visible in (True, False):
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda visible=visible: {"display": {"show_commentary": visible}},
|
|
)
|
|
for projected in (
|
|
_project_for_display([row])[0],
|
|
_history_to_messages([row])[0],
|
|
):
|
|
assert (
|
|
projected.get(
|
|
"display_content", projected.get("content", projected.get("text"))
|
|
)
|
|
== final
|
|
)
|
|
assert projected["display_commentary"] == (["Checking."] if visible else [])
|
|
|
|
|
|
def test_mixed_canonical_preserves_final_repetition_of_a_commentary_phrase(monkeypatch):
|
|
from hermes_cli import config as config_mod
|
|
|
|
row = _row("Checking")
|
|
row["content"] = "Checking\n\nFinal: Checking completed."
|
|
for visible in (True, False):
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda visible=visible: {"display": {"show_commentary": visible}},
|
|
)
|
|
for projected in (
|
|
_project_for_display([row])[0],
|
|
_history_to_messages([row])[0],
|
|
):
|
|
assert projected["display_content"] == row["content"]
|
|
assert projected["display_commentary"] == []
|
|
|
|
row["codex_message_items"].append({
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"phase": "commentary",
|
|
"content": [{"type": "output_text", "text": "Second update"}],
|
|
})
|
|
row["content"] = "Checking\n\nSecond update\n\nFinal: Checking completed."
|
|
monkeypatch.setattr(
|
|
config_mod, "load_config", lambda: {"display": {"show_commentary": True}}
|
|
)
|
|
for projected in (_project_for_display([row])[0], _history_to_messages([row])[0]):
|
|
assert projected["display_content"] == row["content"]
|
|
assert projected["display_commentary"] == []
|
|
|
|
|
|
def test_legacy_newline_and_ambiguous_repeats_do_not_leak_public_text_into_thinking():
|
|
row = _row("Public update.")
|
|
row["reasoning"] = "Private before.\nPublic update.\nPrivate after."
|
|
assert (
|
|
_project_for_display([row])[0]["display_reasoning"]
|
|
== "Private before.\nPrivate after."
|
|
)
|
|
|
|
row["reasoning"] = (
|
|
"Private before.\n\nPublic update.\n\nPrivate after.\n\nPublic update."
|
|
)
|
|
assert (
|
|
_project_for_display([row])[0]["display_reasoning"]
|
|
== "Private before.\n\nPrivate after."
|
|
)
|
|
assert row["reasoning"].endswith("Public update.")
|
|
|
|
|
|
def test_overlapping_public_items_do_not_leave_partial_text_in_final_or_thinking(
|
|
monkeypatch,
|
|
):
|
|
from hermes_cli import config as config_mod
|
|
|
|
row = _row("Start")
|
|
row["codex_message_items"].insert(
|
|
1,
|
|
{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"phase": "commentary",
|
|
"content": [{"type": "output_text", "text": "Start\nTail public"}],
|
|
},
|
|
)
|
|
row["reasoning"] = "Private.\n\nStart\nTail public"
|
|
row["content"] = "Start\nTail public\n\nFinal answer."
|
|
|
|
for visible in (True, False):
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda visible=visible: {"display": {"show_commentary": visible}},
|
|
)
|
|
for projected in (
|
|
_project_for_display([row])[0],
|
|
_history_to_messages([row])[0],
|
|
):
|
|
assert projected["display_content"] == row["content"]
|
|
assert projected["display_reasoning"] == "Private."
|
|
assert projected["display_commentary"] == []
|
|
|
|
# Without an attributable leading commentary span, this belongs to the final.
|
|
row["content"] = "Leading Start\nTail public trailing"
|
|
for visible in (True, False):
|
|
monkeypatch.setattr(
|
|
config_mod,
|
|
"load_config",
|
|
lambda visible=visible: {"display": {"show_commentary": visible}},
|
|
)
|
|
for projected in (
|
|
_project_for_display([row])[0],
|
|
_history_to_messages([row])[0],
|
|
):
|
|
assert projected["display_content"] == "Leading Start\nTail public trailing"
|
|
assert projected["display_commentary"] == (
|
|
["Start", "Start\nTail public"] if visible else []
|
|
)
|
|
|
|
row["content"] = "Start\nTail publicFinal answer."
|
|
monkeypatch.setattr(
|
|
config_mod, "load_config", lambda: {"display": {"show_commentary": True}}
|
|
)
|
|
for projected in (_project_for_display([row])[0], _history_to_messages([row])[0]):
|
|
assert projected["display_content"] == row["content"]
|
|
assert projected["display_commentary"] == []
|