diff --git a/pkg/dispatch/service/dispatch_engine.go b/pkg/dispatch/service/dispatch_engine.go index 125f212..eb3a8bc 100644 --- a/pkg/dispatch/service/dispatch_engine.go +++ b/pkg/dispatch/service/dispatch_engine.go @@ -17,8 +17,10 @@ import ( "github.com/EvolutionAPI/evo-bot-runtime/pkg/pipeline/model" ) -// DispatchEngine segments the AI response, appends the message signature, -// and sends each part sequentially via HTTP postback. +// DispatchEngine segments the AI response and sends each part sequentially +// via HTTP postback, unmodified — it does not apply BotConfig.MessageSignature +// (the CRM applies the agent-bot display-name prefix centrally; see +// dispatch_engine_test.go for the contract this must not regress). // Swap the dispatch backend by providing a different implementation at main.go wiring. type DispatchEngine interface { Dispatch( @@ -81,10 +83,10 @@ func (d *dispatchEngineImpl) Dispatch( parts := segmentContent(residual, cfg) - // Prepend signature to the first part (FR-21) - if cfg.MessageSignature != "" && len(parts) > 0 { - parts[0] = cfg.MessageSignature + parts[0] - } + // FR-21's agent-bot display-name prefix is no longer applied here: the CRM + // now applies it once, centrally, for every outgoing AgentBot message + // (including this postback), so doing it here too would double-prefix it. + // cfg.MessageSignature is kept on BotConfig for callers that still read it. start := time.Now() diff --git a/pkg/dispatch/service/dispatch_engine_test.go b/pkg/dispatch/service/dispatch_engine_test.go index 08f799f..e16ceaf 100644 --- a/pkg/dispatch/service/dispatch_engine_test.go +++ b/pkg/dispatch/service/dispatch_engine_test.go @@ -6,7 +6,6 @@ import ( "errors" "net/http" "net/http/httptest" - "strings" "sync" "testing" "time" @@ -39,7 +38,13 @@ func collectParts(t *testing.T) (*httptest.Server, *[]string, *sync.Mutex) { return server, &parts, &mu } -func TestDispatch_MultiPart_SignatureOnFirstOnly(t *testing.T) { +// MessageSignature is no longer applied here: the CRM now prefixes the +// agent-bot display name once, centrally, for every outgoing AgentBot +// message (see Message#apply_agent_bot_signature on the CRM side) — doing +// it here too would double-prefix it. BotConfig still carries the field +// for callers that read it for other purposes, but Dispatch must leave +// content untouched regardless of its value. +func TestDispatch_MultiPart_SignatureNotAppliedByBotRuntime(t *testing.T) { server, partsPtr, mu := collectParts(t) eng := service.NewDispatchEngine("") @@ -61,17 +66,17 @@ func TestDispatch_MultiPart_SignatureOnFirstOnly(t *testing.T) { copy(parts, *partsPtr) mu.Unlock() - if len(parts) <= 1 { - t.Fatalf("segmentation must produce multiple parts, got %d", len(parts)) + // "hello world this is test" (limit 15) → ["hello world", "this is test"] + // (greedy word-packing; see segmentContent). Asserting the full parts, + // not just the signature's absence, so a future regression that drops or + // mangles segmented content can't hide behind a passing "no [bot]" check. + want := []string{"hello world", "this is test"} + if len(parts) != len(want) { + t.Fatalf("segmentation must produce %d parts, got %d: %q", len(want), len(parts), parts) } - - // Signature only on first part - if !strings.HasPrefix(parts[0], "[bot] ") { - t.Errorf("signature must be prepended to first part: %q", parts[0]) - } - for i, p := range parts[1:] { - if strings.Contains(p, "[bot]") { - t.Errorf("signature must NOT be on part %d: %q", i+1, p) + for i, p := range parts { + if p != want[i] { + t.Errorf("part %d = %q, want %q (MessageSignature must not be applied by bot_runtime)", i, p, want[i]) } } } @@ -98,9 +103,9 @@ func TestDispatch_NoSegmentation_SinglePart(t *testing.T) { if len(parts) != 1 { t.Fatalf("disabled segmentation must produce exactly one part, got %d", len(parts)) } - want := "—signature full response here" + want := "full response here" if parts[0] != want { - t.Errorf("parts[0] = %q, want %q", parts[0], want) + t.Errorf("parts[0] = %q, want %q (MessageSignature must not be applied by bot_runtime)", parts[0], want) } } @@ -146,32 +151,6 @@ func TestDispatch_Cancellation_ReturnsInterrupted(t *testing.T) { } } -func TestDispatch_EmptySignature_NoSuffix(t *testing.T) { - server, partsPtr, mu := collectParts(t) - - eng := service.NewDispatchEngine("") - cfg := model.BotConfig{ - TextSegmentationEnabled: false, - MessageSignature: "", // empty — no suffix - } - - if err := eng.Dispatch(context.Background(), 4, 4, "no signature here", cfg, server.URL); err != nil { - t.Fatalf("Dispatch returned unexpected error: %v", err) - } - - mu.Lock() - parts := make([]string, len(*partsPtr)) - copy(parts, *partsPtr) - mu.Unlock() - - if len(parts) != 1 { - t.Fatalf("expected 1 part, got %d", len(parts)) - } - if parts[0] != "no signature here" { - t.Errorf("parts[0] = %q, want %q (empty signature must not append anything)", parts[0], "no signature here") - } -} - func TestDispatch_NonOKResponse_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 0fb2e1c..cdbb904 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -612,8 +612,11 @@ func TestE2E_PipelineIsolation(t *testing.T) { // TestE2E_DispatchSegmentation verifies that BotConfig segmentation fields are // threaded correctly from the event through the pipeline to the dispatch engine, -// and that the AI response is split into the expected parts with the signature -// appended only to the last one. +// and that the AI response is split into the expected parts. MessageSignature +// is set on the event to prove bot_runtime does NOT act on it — the CRM applies +// the agent-bot display-name prefix once, centrally, on every postback it +// receives (see Message#apply_agent_bot_signature on the CRM side), so +// bot_runtime must leave segment content untouched regardless of this field. // // dispatch_engine_test.go covers segmentation in isolation. This is the first // test to exercise the full chain: BotConfig in the HTTP event → pipelineEntry → @@ -621,7 +624,6 @@ func TestE2E_PipelineIsolation(t *testing.T) { // // Content: "hello world foo bar", limit=10 chars per segment. // segmentContent produces ["hello", "world foo", "bar"]. -// With signature " [sig]" appended to the last part: "bar [sig]". func TestE2E_DispatchSegmentation(t *testing.T) { h := newHarness(t) contactID, convID := nextPair() @@ -650,12 +652,11 @@ func TestE2E_DispatchSegmentation(t *testing.T) { t.Fatalf("postback called %d times, want 3 (one per segment)", n) } - // EVO-558: the signature is prepended to the first segment only. - want := []string{" [sig]hello", "world foo", "bar"} + want := []string{"hello", "world foo", "bar"} for i, body := range h.pbServer.allBodies() { got := decodePostbackContent(body) if got != want[i] { - t.Errorf("segment[%d] = %q, want %q", i, got, want[i]) + t.Errorf("segment[%d] = %q, want %q (MessageSignature must not be applied by bot_runtime)", i, got, want[i]) } } }