picoclaw/pkg/agent/prompt_turn_test.go
maxiaoyang 7129c97dd2 feat(agent): plumb sender ID through user message persistence
Connects the inbound SenderID, which was already carried end-to-end via
InboundContext to PromptBuildRequest, into the per-turn user message so
historical messages remember who said what.

- userPromptMessage gains a senderID parameter. Real human IDs are
  sanitized via SanitizeMessageName; synthetic triggers (cron,
  heartbeat, async:*) are filtered out via IsSystemSenderID so they
  do not surface as distinct users.

- pipeline_setup.go now passes ts.opts.Dispatch.SenderID() and uses
  AddFullMessage unconditionally. The previous AddMessage(role,
  content) shortcut for media-less turns silently dropped the new
  Name field, breaking history disambiguation.

- context.go (BuildMessagesFromPrompt) propagates req.SenderID onto
  the current-turn user message that goes to the LLM.

After this commit, sessions persist sender attribution and provider
adapters receive it on each user message. Adapters that consume Name
(OpenAI native field, Anthropic-style content prefix) land in
follow-up commits.

The existing "## Current Sender" block in buildDynamicContext is
unchanged; it continues to describe the latest speaker in the system
prompt while Name covers the per-message channel for history.

Refs #2702.
2026-04-29 21:02:59 +08:00

83 lines
2.8 KiB
Go

// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package agent
import (
"testing"
)
// TestUserPromptMessage_AttachesSanitizedSenderName verifies that
// userPromptMessage stores a sanitized sender attribution in
// providers.Message.Name when given a real human sender. This is the
// core wiring change for issue #2702.
func TestUserPromptMessage_AttachesSanitizedSenderName(t *testing.T) {
tests := []struct {
name string
senderID string
wantName string
}{
{"slack-style id", "U07AB12C3DEF", "U07AB12C3DEF"},
{"discord-style id sanitized", "alice#1234", "alice_1234"},
{"telegram numeric", "141455495", "141455495"},
{"unicode display becomes empty", "李华", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := userPromptMessage("hello", nil, tt.senderID)
if msg.Role != "user" {
t.Errorf("Role = %q, want user", msg.Role)
}
if msg.Content != "hello" {
t.Errorf("Content = %q, want hello", msg.Content)
}
if msg.Name != tt.wantName {
t.Errorf("Name = %q, want %q", msg.Name, tt.wantName)
}
})
}
}
// TestUserPromptMessage_SkipsAttributionForSystemSenders verifies that
// synthetic trigger sources (cron, heartbeat, async callbacks) do not
// produce per-message attribution. These are not distinct human users,
// so leaking them into Name would create spurious "user identities" in
// the OpenAI wire format and a confusing `[cron] [System: ...]` double
// prefix on Anthropic-style adapters.
func TestUserPromptMessage_SkipsAttributionForSystemSenders(t *testing.T) {
systemIDs := []string{"", "cron", "heartbeat", "system", "async:read_file"}
for _, id := range systemIDs {
t.Run(id, func(t *testing.T) {
msg := userPromptMessage("triggered work", nil, id)
if msg.Name != "" {
t.Errorf("Name = %q, want empty for synthetic sender %q", msg.Name, id)
}
})
}
}
// TestUserPromptMessage_NoNameMeansNoAttribution covers the empty-sender
// path explicitly so future refactors don't accidentally start tagging
// anonymous direct-channel turns.
func TestUserPromptMessage_NoNameMeansNoAttribution(t *testing.T) {
msg := userPromptMessage("anonymous message", nil, "")
if msg.Name != "" {
t.Errorf("Name = %q, want empty for anonymous sender", msg.Name)
}
}
// TestUserPromptMessage_PreservesMedia is a regression guard: the
// existing media-passthrough behavior must continue to work after the
// signature change.
func TestUserPromptMessage_PreservesMedia(t *testing.T) {
media := []string{"data:image/png;base64,abc"}
msg := userPromptMessage("look", media, "alice")
if len(msg.Media) != 1 || msg.Media[0] != media[0] {
t.Errorf("Media = %v, want %v", msg.Media, media)
}
if msg.Name != "alice" {
t.Errorf("Name = %q, want alice", msg.Name)
}
}