fix(chat): reconcile live assistant reply carrying verifier footer with REST echo (#1241) (#1256)

This commit is contained in:
m57 authored and GitHub committed 2026-09-24 22:17:10 +03:00
1 parent 0d4c0b40ad
commit f3921957b5
5 files changed
+310 -3

No files matched your search

@@ -655,6 +655,50 @@ class ChatPagingMergeTest {
assertTrue(sameLogicalMessage(canonical, live))
}
@Test
fun verifierFooterReplyMatchesCanonicalCounterpartWithoutFooter() {
val footer =
"""
⚠️ File-mutation verifier: 1 file edit(s) FAILED this turn despite any wording above that may suggest otherwise. Run git status or read_file to confirm what actually landed.
• /tmp/skill_lang_audit.py — [write_file] Write denied: '/tmp/skill_lang_audit.py' is outside HERMES_WRITE_SAFE_ROOT (/opt/data). Unset the variable or add this path's directory prefix.
""".trimIndent()
val body = "I completed the audit. Here are the findings."
val liveAssistant =
ChatMessage(
id = "uuid-123",
role = MessageRole.ASSISTANT,
content = "$body\n\n$footer",
completionId = "comp-1",
)
val serverRow =
SessionMessage(
id = 42,
role = "assistant",
content = JsonPrimitive(body),
)
// 1. mapServerMessages correctly matches liveAssistant and acquires comp-1 and rich footer content
val mapped = mapServerMessages("session", listOf(serverRow), 0, true, listOf(liveAssistant))
assertEquals(1, mapped.size)
val canonical = mapped.single()
assertEquals("rest-session-42", canonical.id)
assertEquals("comp-1", canonical.completionId)
assertTrue("Canonical mapped row should retain verifier footer", canonical.content.contains(footer))
// 2. mergeTranscriptWithLive merges into a single message preserving footer and acquiring restId
val merged =
mergeTranscriptWithLive(
restMessages = mapped,
currentMessages = listOf(liveAssistant),
preserveLiveIds = true,
)
assertEquals(1, merged.size)
val single = merged.single()
assertEquals("uuid-123", single.id)
assertEquals("rest-session-42", single.restId)
assertTrue("Merged message must keep the verifier warning footer", single.content.contains(footer))
}
@Test
fun permanentlyLocalRowsDoNotMatchCanonicalRowsByContent() {
val localFeedback =
@@ -0,0 +1,90 @@
package com.m57.hermescontrol.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatVerifierFooterTest {
private val sampleFooter =
"""
⚠️ File-mutation verifier: 1 file edit(s) FAILED this turn despite any wording above that may suggest otherwise. Run git status or read_file to confirm what actually landed.
• /tmp/skill_lang_audit.py — [write_file] Write denied: '/tmp/skill_lang_audit.py' is outside HERMES_WRITE_SAFE_ROOT (/opt/data). Unset the variable or add this path's directory prefix.
""".trimIndent()
private val backtickedFooter =
"""
⚠️ File-mutation verifier: 1 file edit(s) FAILED this turn despite any wording above that may suggest otherwise. Run `git status` or `read_file` to confirm what actually landed.
• `/tmp/skill_lang_audit.py` — [write_file] Write denied: '/tmp/skill_lang_audit.py' is outside HERMES_WRITE_SAFE_ROOT (/opt/data). Unset the variable or add this path's directory prefix.
""".trimIndent()
@Test
fun `split extracts body and footer for exact reporter format`() {
val body = "I completed the audit. Here are the findings."
val full = "$body\n\n$sampleFooter"
val result = ChatVerifierFooter.split(full)
assertNotNull(result)
assertEquals(body, result?.body)
assertEquals(sampleFooter, result?.footer?.trim())
}
@Test
fun `split extracts body and footer for backend backticked format`() {
val body = "Audit summary:\n- item 1\n- item 2"
val full = "$body\n\n$backtickedFooter"
val result = ChatVerifierFooter.split(full)
assertNotNull(result)
assertEquals(body, result?.body)
assertEquals(backtickedFooter, result?.footer?.trim())
}
@Test
fun `split handles multiple bullets and overflow note`() {
val footer =
"""
⚠️ File-mutation verifier: 3 file edit(s) FAILED this turn despite any wording above that may suggest otherwise. Run `git status` or `read_file` to confirm what actually landed.
• `/a.py` — [patch] failed
• `/b.py` — [patch] failed
• … and 1 more
""".trimIndent()
val body = "Done"
val result = ChatVerifierFooter.split("$body\n\n$footer")
assertNotNull(result)
assertEquals("Done", result?.body)
}
@Test
fun `split rejects footer when body is empty`() {
val result = ChatVerifierFooter.split(sampleFooter)
assertNull(result)
}
@Test
fun `split rejects footer when inside a line without newline before it`() {
val text = "Look at this: ⚠️ File-mutation verifier: 1 file edit(s) FAILED this turn despite any wording..."
val result = ChatVerifierFooter.split(text)
assertNull(result)
}
@Test
fun `split rejects text with unrelated trailing prose`() {
val full = "Body text\n\n$sampleFooter\nAnd here is more stuff after the footer."
val result = ChatVerifierFooter.split(full)
assertNull(result)
}
@Test
fun `matchesBase compares body ignoring verifier footer`() {
val body = "Here is the response without errors."
val withFooter = "$body\n\n$sampleFooter"
assertTrue(ChatVerifierFooter.matchesBase(body, withFooter))
assertTrue(ChatVerifierFooter.matchesBase(withFooter, body))
assertTrue(ChatVerifierFooter.matchesBase(withFooter, withFooter))
assertFalse(ChatVerifierFooter.matchesBase("Different body", withFooter))
}
}