Two more helpers needed for end-to-end multi-user attribution: - IsSystemSenderID filters synthetic trigger sources (cron, heartbeat, async:* tool callbacks, system channel) so internal events do not surface as distinct human users in the conversation. Without this, cron-triggered messages would carry attribution like cron, producing a confusing [cron] [System: cron] ... double prefix on adapters that fall back to in-content rendering. - ApplyUserNamePrefix renders msg.Name as a [name] prefix on user content for adapters whose APIs do not support a per-message author identity (Anthropic, Bedrock, OpenAI Responses). The persisted message is never mutated; only the wire payload carries the prefix. Tool result messages and non-user roles pass through unchanged. These helpers are pure and independently testable; they will be wired into the agent and provider adapters in subsequent commits. Refs #2702.
158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package messageutil
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
|
)
|
|
|
|
func TestSanitizeMessageName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"empty", "", ""},
|
|
{"whitespace only", " ", ""},
|
|
{"plain ascii", "alice", "alice"},
|
|
{"discord-style id", "alice#1234", "alice_1234"},
|
|
{"telegram numeric id", "141455495", "141455495"},
|
|
{"slack-style id", "U07AB12C3DEF", "U07AB12C3DEF"},
|
|
{"already valid mixed case", "Alice_Bob-99", "Alice_Bob-99"},
|
|
{"strips leading/trailing underscores", "@alice@", "alice"},
|
|
{"collapses runs", "a@@@b", "a_b"},
|
|
{"non-ascii becomes underscores", "李华", ""},
|
|
{"non-ascii mixed", "alice 李", "alice"},
|
|
{"all special chars collapses to empty", "@@@!!!", ""},
|
|
{"truncates to 64", strings.Repeat("a", 100), strings.Repeat("a", 64)},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := SanitizeMessageName(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("SanitizeMessageName(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeMessageName_OutputAlwaysWireSafe(t *testing.T) {
|
|
inputs := []string{
|
|
"alice#1234", "U07AB12C3DEF", "141455495",
|
|
"@@@", "李华", "alice 李", "Alice_Bob-99",
|
|
strings.Repeat("a", 200),
|
|
}
|
|
for _, in := range inputs {
|
|
got := SanitizeMessageName(in)
|
|
if got == "" {
|
|
continue
|
|
}
|
|
if len(got) > maxMessageNameLen {
|
|
t.Errorf("SanitizeMessageName(%q) length %d exceeds max %d", in, len(got), maxMessageNameLen)
|
|
}
|
|
for _, r := range got {
|
|
ok := (r >= 'a' && r <= 'z') ||
|
|
(r >= 'A' && r <= 'Z') ||
|
|
(r >= '0' && r <= '9') ||
|
|
r == '_' || r == '-'
|
|
if !ok {
|
|
t.Errorf("SanitizeMessageName(%q) = %q contains invalid rune %q", in, got, r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsSystemSenderID(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{" ", true},
|
|
{"cron", true},
|
|
{"CRON", true},
|
|
{"heartbeat", true},
|
|
{"system", true},
|
|
{"async:tool_call", true},
|
|
{"ASYNC:Foo", true},
|
|
{"alice", false},
|
|
{"U07AB12C3DEF", false},
|
|
{"141455495", false},
|
|
{"alice#1234", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.in, func(t *testing.T) {
|
|
if got := IsSystemSenderID(tt.in); got != tt.want {
|
|
t.Errorf("IsSystemSenderID(%q) = %v, want %v", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyUserNamePrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
msg protocoltypes.Message
|
|
want string
|
|
}{
|
|
{
|
|
name: "user with name",
|
|
msg: protocoltypes.Message{Role: "user", Name: "alice", Content: "hello"},
|
|
want: "[alice] hello",
|
|
},
|
|
{
|
|
name: "user without name",
|
|
msg: protocoltypes.Message{Role: "user", Content: "hello"},
|
|
want: "hello",
|
|
},
|
|
{
|
|
name: "user with name but empty content",
|
|
msg: protocoltypes.Message{Role: "user", Name: "alice", Content: ""},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "user tool result is not prefixed",
|
|
msg: protocoltypes.Message{Role: "user", Name: "alice", Content: `{"ok":true}`, ToolCallID: "call_1"},
|
|
want: `{"ok":true}`,
|
|
},
|
|
{
|
|
name: "assistant with name not prefixed",
|
|
msg: protocoltypes.Message{Role: "assistant", Name: "alice", Content: "I am the assistant"},
|
|
want: "I am the assistant",
|
|
},
|
|
{
|
|
name: "tool role with name not prefixed",
|
|
msg: protocoltypes.Message{Role: "tool", Name: "alice", Content: `{"x":1}`},
|
|
want: `{"x":1}`,
|
|
},
|
|
{
|
|
name: "system role with name not prefixed",
|
|
msg: protocoltypes.Message{Role: "system", Name: "alice", Content: "instructions"},
|
|
want: "instructions",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ApplyUserNamePrefix(tt.msg)
|
|
if got != tt.want {
|
|
t.Errorf("ApplyUserNamePrefix(%+v) = %q, want %q", tt.msg, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyUserNamePrefix_DoesNotMutateInput(t *testing.T) {
|
|
msg := protocoltypes.Message{Role: "user", Name: "alice", Content: "hello"}
|
|
original := msg
|
|
_ = ApplyUserNamePrefix(msg)
|
|
if !reflect.DeepEqual(msg, original) {
|
|
t.Errorf("ApplyUserNamePrefix mutated the input message: got %+v, want %+v", msg, original)
|
|
}
|
|
}
|