Files
goclaw/internal/bootstrap/seed_store_test.go
T
Luan Vu a4f2d02a80 fix(channels): annotate DM messages with sender identity (#120)
* fix(channels): annotate DM messages with sender identity

Telegram and Zalo group messages already include [From: sender] prefix
so the agent knows who is talking, but DM messages were sent without
any sender context — the agent had no way to address the user by name.

- Telegram DM: add [From: @username] (or FirstName if no username)
- Zalo DM: add [From: displayName] when dName is present in payload

* fix(tests): add missing EnsureUserProfile to test stubs

AgentStore interface gained EnsureUserProfile in 4fce731 but the test
stub implementations were not updated, breaking CI on main.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 19:25:55 +07:00

275 lines
10 KiB
Go

package bootstrap
import (
"context"
"maps"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// ---- minimal AgentStore stub for seed tests ----
type seedStubStore struct {
// agent-level files (simulates agent_context_files)
agentFiles map[string]string // fileName → content
// per-user files (simulates user_context_files)
userFiles map[string]string // fileName → content (shared across all users for simplicity)
// captures what was written per-user: fileName → content
seededUserFiles map[string]string
}
func newSeedStub() *seedStubStore {
return &seedStubStore{
agentFiles: make(map[string]string),
userFiles: make(map[string]string),
seededUserFiles: make(map[string]string),
}
}
func (s *seedStubStore) GetAgentContextFiles(_ context.Context, _ uuid.UUID) ([]store.AgentContextFileData, error) {
var out []store.AgentContextFileData
for name, content := range s.agentFiles {
out = append(out, store.AgentContextFileData{FileName: name, Content: content})
}
return out, nil
}
func (s *seedStubStore) SetAgentContextFile(_ context.Context, _ uuid.UUID, name, content string) error {
s.agentFiles[name] = content
return nil
}
func (s *seedStubStore) GetUserContextFiles(_ context.Context, _ uuid.UUID, _ string) ([]store.UserContextFileData, error) {
var out []store.UserContextFileData
for name, content := range s.userFiles {
out = append(out, store.UserContextFileData{FileName: name, Content: content})
}
return out, nil
}
func (s *seedStubStore) SetUserContextFile(_ context.Context, _ uuid.UUID, _, name, content string) error {
s.seededUserFiles[name] = content
return nil
}
func (s *seedStubStore) DeleteUserContextFile(_ context.Context, _ uuid.UUID, _, _ string) error {
return nil
}
// Remaining interface methods — not exercised.
func (s *seedStubStore) Create(_ context.Context, _ *store.AgentData) error { return nil }
func (s *seedStubStore) GetByKey(_ context.Context, _ string) (*store.AgentData, error) {
return nil, nil
}
func (s *seedStubStore) GetByID(_ context.Context, _ uuid.UUID) (*store.AgentData, error) {
return nil, nil
}
func (s *seedStubStore) Update(_ context.Context, _ uuid.UUID, _ map[string]any) error { return nil }
func (s *seedStubStore) Delete(_ context.Context, _ uuid.UUID) error { return nil }
func (s *seedStubStore) List(_ context.Context, _ string) ([]store.AgentData, error) { return nil, nil }
func (s *seedStubStore) GetDefault(_ context.Context) (*store.AgentData, error) { return nil, nil }
func (s *seedStubStore) ShareAgent(_ context.Context, _ uuid.UUID, _, _, _ string) error { return nil }
func (s *seedStubStore) RevokeShare(_ context.Context, _ uuid.UUID, _ string) error { return nil }
func (s *seedStubStore) ListShares(_ context.Context, _ uuid.UUID) ([]store.AgentShareData, error) {
return nil, nil
}
func (s *seedStubStore) CanAccess(_ context.Context, _ uuid.UUID, _ string) (bool, string, error) {
return true, "admin", nil
}
func (s *seedStubStore) ListAccessible(_ context.Context, _ string) ([]store.AgentData, error) {
return nil, nil
}
func (s *seedStubStore) GetUserOverride(_ context.Context, _ uuid.UUID, _ string) (*store.UserAgentOverrideData, error) {
return nil, nil
}
func (s *seedStubStore) SetUserOverride(_ context.Context, _ *store.UserAgentOverrideData) error {
return nil
}
func (s *seedStubStore) GetOrCreateUserProfile(_ context.Context, _ uuid.UUID, _, _, _ string) (bool, string, error) {
return false, "", nil
}
func (s *seedStubStore) IsGroupFileWriter(_ context.Context, _ uuid.UUID, _, _ string) (bool, error) {
return false, nil
}
func (s *seedStubStore) AddGroupFileWriter(_ context.Context, _ uuid.UUID, _, _, _, _ string) error {
return nil
}
func (s *seedStubStore) RemoveGroupFileWriter(_ context.Context, _ uuid.UUID, _, _ string) error {
return nil
}
func (s *seedStubStore) ListGroupFileWriters(_ context.Context, _ uuid.UUID, _ string) ([]store.GroupFileWriterData, error) {
return nil, nil
}
func (s *seedStubStore) ListGroupFileWriterGroups(_ context.Context, _ uuid.UUID) ([]store.GroupWriterGroupInfo, error) {
return nil, nil
}
func (s *seedStubStore) ListUserInstances(_ context.Context, _ uuid.UUID) ([]store.UserInstanceData, error) {
return nil, nil
}
func (s *seedStubStore) UpdateUserProfileMetadata(_ context.Context, _ uuid.UUID, _ string, _ map[string]string) error {
return nil
}
func (s *seedStubStore) EnsureUserProfile(_ context.Context, _ uuid.UUID, _ string) error {
return nil
}
// ---- Tests ----
// TestSeedUserFiles_PredefinedAgent_UsesAgentLevelUserMD is the primary regression test.
// When a predefined agent has wizard-written USER.md in agent_context_files, SeedUserFiles
// must seed that content into user_context_files — NOT the blank embedded template.
func TestSeedUserFiles_PredefinedAgent_UsesAgentLevelUserMD(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
wizardContent := "# User Profile\nOwner: Alice\nLanguage: English\nNotes: Prefers concise answers"
// Simulate wizard writing USER.md at agent level via agents.files.set
as.agentFiles[UserFile] = wizardContent
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-alice", store.AgentTypePredefined)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
// USER.md must be in the seeded list
foundUserMD := false
for _, f := range seeded {
if f == UserFile {
foundUserMD = true
}
}
if !foundUserMD {
t.Errorf("USER.md not in seeded files list: %v", seeded)
}
// The seeded USER.md must contain wizard content, not the blank template
got, ok := as.seededUserFiles[UserFile]
if !ok {
t.Fatal("USER.md was not written to user_context_files")
}
if got != wizardContent {
t.Errorf("seeded USER.md content mismatch:\n want: %q\n got: %q", wizardContent, got)
}
}
// TestSeedUserFiles_PredefinedAgent_FallsBackToTemplateWhenNoAgentLevelUserMD verifies
// that when there is no wizard-written USER.md at agent level, the blank template is used.
func TestSeedUserFiles_PredefinedAgent_FallsBackToTemplateWhenNoAgentLevelUserMD(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
// No agent-level USER.md — wizard did not write one
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-bob", store.AgentTypePredefined)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
// USER.md should still be seeded (from embedded template)
foundUserMD := false
for _, f := range seeded {
if f == UserFile {
foundUserMD = true
}
}
if !foundUserMD {
t.Errorf("USER.md should be seeded from template when no agent-level file exists: %v", seeded)
}
// Content should be the embedded template (non-empty — the template file exists)
got, ok := as.seededUserFiles[UserFile]
if !ok {
t.Fatal("USER.md was not written to user_context_files")
}
if got == "" {
t.Error("seeded USER.md should not be empty (expected embedded template content)")
}
}
// TestSeedUserFiles_PredefinedAgent_DoesNotOverwriteExistingPerUserContent verifies
// that personalized per-user USER.md written via conversation is never overwritten.
func TestSeedUserFiles_PredefinedAgent_DoesNotOverwriteExistingPerUserContent(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
personalContent := "# User Profile\nMy customized personal content"
// Pre-populate per-user USER.md (simulates user who already chatted and personalized)
as.userFiles[UserFile] = personalContent
// Also set wizard content at agent level
as.agentFiles[UserFile] = "wizard content that should NOT override personal content"
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-charlie", store.AgentTypePredefined)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
// USER.md must NOT be in the seeded list (existing content, should skip)
for _, f := range seeded {
if f == UserFile {
t.Error("USER.md should NOT be re-seeded when per-user content already exists")
}
}
// SetUserContextFile must not have been called for USER.md
if _, wrote := as.seededUserFiles[UserFile]; wrote {
t.Error("SetUserContextFile should not be called when per-user USER.md already has content")
}
}
// TestSeedUserFiles_OpenAgent_UsesEmbeddedTemplate verifies that open agents
// are completely unaffected — they still receive embedded templates per-user.
func TestSeedUserFiles_OpenAgent_UsesEmbeddedTemplate(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
// Open agents should never check agent_context_files for USER.md
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-dave", store.AgentTypeOpen)
if err != nil {
t.Fatalf("SeedUserFiles returned error: %v", err)
}
// Open agents seed the full set: AGENTS.md, SOUL.md, IDENTITY.md, USER.md, BOOTSTRAP.md
expectedFiles := map[string]bool{
AgentsFile: true, SoulFile: true, IdentityFile: true, UserFile: true, BootstrapFile: true,
}
for _, f := range seeded {
delete(expectedFiles, f)
}
if len(expectedFiles) > 0 {
t.Errorf("open agent: missing seeded files: %v", expectedFiles)
}
// USER.md must have been written using embedded template (non-empty)
got, ok := as.seededUserFiles[UserFile]
if !ok {
t.Fatal("open agent: USER.md was not written to user_context_files")
}
if got == "" {
t.Error("open agent: seeded USER.md should not be empty")
}
}
// TestSeedUserFiles_IdempotentOnSecondCall verifies that calling SeedUserFiles
// a second time for the same user does not re-seed already-present files.
func TestSeedUserFiles_IdempotentOnSecondCall(t *testing.T) {
as := newSeedStub()
agentID := uuid.New()
// First call — seeds files
SeedUserFiles(context.Background(), as, agentID, "user-frank", store.AgentTypePredefined)
// Simulate what the first call wrote (move seededUserFiles → userFiles)
maps.Copy(as.userFiles, as.seededUserFiles)
as.seededUserFiles = make(map[string]string)
// Second call — must seed nothing (all files already exist)
seeded, err := SeedUserFiles(context.Background(), as, agentID, "user-frank", store.AgentTypePredefined)
if err != nil {
t.Fatalf("second SeedUserFiles returned error: %v", err)
}
if len(seeded) != 0 {
t.Errorf("second call should seed nothing, but seeded: %v", seeded)
}
if len(as.seededUserFiles) != 0 {
t.Errorf("second call should not write any files, but wrote: %v", as.seededUserFiles)
}
}