Harden active user turn dedupe tests
This commit is contained in:
parent
709f133d76
commit
035a2609f1
3 changed files with 86 additions and 35 deletions
|
|
@ -2339,7 +2339,11 @@ function _stripForcedSkillEnvelope(text){
|
|||
}
|
||||
|
||||
function _normalizeUserTranscriptText(text){
|
||||
let value=_stripAttachedFilesMarker(_stripForcedSkillEnvelope(text));
|
||||
const value=_stripAttachedFilesMarker(_stripForcedSkillEnvelope(text));
|
||||
// ui.js is loaded before sessions.js in index.html and owns the canonical
|
||||
// workspace-sentinel parser used by rendering. Keep a small fallback for
|
||||
// static/helper tests and defensive partial loads, but prefer the renderer's
|
||||
// parser whenever it is available.
|
||||
if(typeof _stripWorkspaceDisplayPrefix==='function'){
|
||||
return _stripWorkspaceDisplayPrefix(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text()
|
||||
SESSIONS_SRC = (ROOT / "static" / "sessions.js").read_text()
|
||||
UI_SRC = (ROOT / "static" / "ui.js").read_text()
|
||||
|
||||
|
||||
def _function_body(src: str, signature: str) -> str:
|
||||
|
|
@ -21,6 +24,53 @@ def _function_body(src: str, signature: str) -> str:
|
|||
raise AssertionError(f"could not extract function body for {signature!r}")
|
||||
|
||||
|
||||
def _run_session_identity_probe() -> dict:
|
||||
prompt = "same submitted prompt\nwith a second line"
|
||||
workspace_prompt = f"[Workspace::v1: /tmp/hermes-webui]\n{prompt}"
|
||||
legacy_workspace_prompt = f"[Workspace: /tmp/hermes-webui]\n{prompt}"
|
||||
attached_prompt = f"{prompt}\n\n[Attached files: /tmp/a.txt]"
|
||||
forced_prompt = (
|
||||
"[USER OVERRIDE] You MUST follow the skill 'hermes-webui-coordinator' "
|
||||
"content provided below before responding to the next message.\n\n"
|
||||
"[FORCED SKILL CONTEXT: hermes-webui-coordinator]\n"
|
||||
"skill body that should not make a second user bubble\n"
|
||||
"[/FORCED SKILL CONTEXT]\n\n"
|
||||
f"{prompt}"
|
||||
)
|
||||
helpers = "\n".join(
|
||||
[
|
||||
_function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"),
|
||||
_function_body(SESSIONS_SRC, "function _messageComparableText"),
|
||||
_function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"),
|
||||
_function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"),
|
||||
_function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"),
|
||||
_function_body(SESSIONS_SRC, "function _sameTranscriptMessage"),
|
||||
_function_body(SESSIONS_SRC, "function _inflightHasVisibleLiveState"),
|
||||
]
|
||||
)
|
||||
script = f"""
|
||||
{helpers}
|
||||
const plain = {{role:'user', content:{json.dumps(prompt)}}};
|
||||
const workspace = {{role:'user', content:{json.dumps(workspace_prompt)}}};
|
||||
const legacyWorkspace = {{role:'user', content:{json.dumps(legacy_workspace_prompt)}}};
|
||||
const attached = {{role:'user', content:{json.dumps(attached_prompt)}}};
|
||||
const forced = {{role:'user', content:{json.dumps(forced_prompt)}}};
|
||||
const different = {{role:'user', content:'a different submitted prompt'}};
|
||||
process.stdout.write(JSON.stringify({{
|
||||
workspaceDedupe: _sameTranscriptMessage(plain, workspace),
|
||||
legacyWorkspaceDedupe: _sameTranscriptMessage(plain, legacyWorkspace),
|
||||
attachedDedupe: _sameTranscriptMessage(plain, attached),
|
||||
forcedSkillDedupe: _sameTranscriptMessage(plain, forced),
|
||||
differentUserNotDedupe: !_sameTranscriptMessage(plain, different),
|
||||
roleMismatchNotDedupe: !_sameTranscriptMessage(plain, {{role:'assistant', content:{json.dumps(prompt)}}}),
|
||||
userOnlyInflightVisible: _inflightHasVisibleLiveState({{messages:[plain]}}),
|
||||
emptyUserOnlyInflightNotVisible: !_inflightHasVisibleLiveState({{messages:[{{role:'user', content:' '}}]}}),
|
||||
}}));
|
||||
"""
|
||||
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_reattach_path_uses_replay_when_status_reports_journal():
|
||||
reattach_pos = MESSAGES_SRC.index("let replayOnly=false;")
|
||||
block = MESSAGES_SRC[reattach_pos : reattach_pos + 1200]
|
||||
|
|
@ -150,31 +200,26 @@ def test_active_reload_keeps_user_only_inflight_visible_until_pending_dedupe():
|
|||
The browser must not discard the user-only optimistic INFLIGHT entry as a
|
||||
cursor-only snapshot before pending/live replay reconciliation runs.
|
||||
"""
|
||||
helper = _function_body(SESSIONS_SRC, "function _inflightHasVisibleLiveState")
|
||||
result = _run_session_identity_probe()
|
||||
|
||||
assert "msg.role === 'user'" in helper or "msg.role==='user'" in helper
|
||||
assert "_messageComparableText(msg)" in helper or "msg.content" in helper
|
||||
assert result["userOnlyInflightVisible"] is True
|
||||
assert result["emptyUserOnlyInflightNotVisible"] is True
|
||||
|
||||
|
||||
def test_pending_user_merge_dedupes_workspace_prefixed_replay_rows():
|
||||
def test_pending_user_merge_dedupes_user_turn_variants_by_behavior():
|
||||
"""Pending user rows and replayed/checkpointed user rows share one turn.
|
||||
|
||||
The model-facing current user message may carry the WebUI workspace sentinel,
|
||||
while pending_user_message stores only the human prompt. Frontend transcript
|
||||
equality must normalize the same way the renderer and backend context
|
||||
identity do, otherwise active reload/reconnect can show two user bubbles for
|
||||
one submitted turn.
|
||||
Execute the same JavaScript helpers the browser uses so the regression test
|
||||
catches regex/order/trim mistakes, not just identifier wiring.
|
||||
"""
|
||||
same = _function_body(SESSIONS_SRC, "function _sameTranscriptMessage")
|
||||
normalizer = _function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText")
|
||||
forced = _function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope")
|
||||
result = _run_session_identity_probe()
|
||||
|
||||
assert "_normalizeUserTranscriptText" in same
|
||||
assert "_stripWorkspaceDisplayPrefix" in normalizer
|
||||
assert "_stripAttachedFilesMarker" in normalizer
|
||||
assert "Workspace::v1" in normalizer
|
||||
assert "FORCED SKILL CONTEXT" in forced
|
||||
assert "USER OVERRIDE" in forced
|
||||
assert result["workspaceDedupe"] is True
|
||||
assert result["legacyWorkspaceDedupe"] is True
|
||||
assert result["attachedDedupe"] is True
|
||||
assert result["forcedSkillDedupe"] is True
|
||||
assert result["differentUserNotDedupe"] is True
|
||||
assert result["roleMismatchNotDedupe"] is True
|
||||
|
||||
|
||||
def test_live_tool_matching_uses_the_same_aliases_as_live_card_dedup():
|
||||
|
|
|
|||
|
|
@ -119,32 +119,34 @@ def test_eager_checkpointed_user_is_removed_from_model_context():
|
|||
assert [m["content"] for m in context] == ["older", "prior"]
|
||||
|
||||
|
||||
def test_submitted_turn_journal_does_not_duplicate_current_model_context(tmp_path):
|
||||
from api.turn_journal import append_turn_journal_event
|
||||
def test_active_pending_current_user_is_removed_from_model_context():
|
||||
"""Current pending user text must not appear in conversation_history twice.
|
||||
|
||||
The provider receives the active turn as `user_message`; if an eager
|
||||
checkpoint/recovery path has already put the same current user text at the
|
||||
end of `context_messages`, `_context_messages_for_new_turn` must strip it
|
||||
before building the model-facing history. Turn-journal `submitted` records
|
||||
are intentionally not a context source for this helper.
|
||||
"""
|
||||
session = Session(
|
||||
session_id="journal_context_current_turn",
|
||||
title="journal context",
|
||||
messages=[{"role": "user", "content": "older"}],
|
||||
context_messages=[{"role": "user", "content": "older"}],
|
||||
messages=[
|
||||
{"role": "user", "content": "older"},
|
||||
{"role": "assistant", "content": "prior"},
|
||||
],
|
||||
context_messages=[
|
||||
{"role": "user", "content": "older"},
|
||||
{"role": "assistant", "content": "prior"},
|
||||
{"role": "user", "content": "[Workspace::v1: /tmp/hermes]\ncurrent prompt"},
|
||||
],
|
||||
active_stream_id="stream-current",
|
||||
pending_user_message="current prompt",
|
||||
)
|
||||
append_turn_journal_event(
|
||||
session.session_id,
|
||||
{
|
||||
"event": "submitted",
|
||||
"stream_id": "stream-current",
|
||||
"role": "user",
|
||||
"content": "current prompt",
|
||||
"created_at": 123.0,
|
||||
},
|
||||
session_dir=tmp_path,
|
||||
)
|
||||
|
||||
context = streaming._context_messages_for_new_turn(session, "current prompt")
|
||||
|
||||
assert [m["content"] for m in context] == ["older"]
|
||||
assert [m["content"] for m in context] == ["older", "prior"]
|
||||
|
||||
|
||||
def test_eager_checkpointed_user_is_not_duplicated_after_agent_result():
|
||||
|
|
|
|||
Loading…
Reference in a new issue