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) } }