readysite / website / models / models_test.go
21.3 KB
models_test.go
package models

import (
	"testing"
	"time"
)

// ---------- User CRUD ----------

func TestUserCRUD(t *testing.T) {
	// Create
	user := &User{
		Email:        "alice@example.com",
		PasswordHash: "hashed_pw",
		Name:         "Alice",
		Role:         "admin",
		Verified:     true,
	}
	id, err := Users.Insert(user)
	if err != nil {
		t.Fatalf("Insert user: %v", err)
	}
	if id == "" {
		t.Fatal("Insert should return a non-empty ID")
	}
	if user.ID != id {
		t.Fatalf("user.ID = %q, want %q", user.ID, id)
	}

	// Get
	got, err := Users.Get(id)
	if err != nil {
		t.Fatalf("Get user: %v", err)
	}
	if got.Email != "alice@example.com" {
		t.Errorf("Email = %q, want %q", got.Email, "alice@example.com")
	}
	if got.Name != "Alice" {
		t.Errorf("Name = %q, want %q", got.Name, "Alice")
	}
	if got.Role != "admin" {
		t.Errorf("Role = %q, want %q", got.Role, "admin")
	}
	if !got.Verified {
		t.Error("Verified should be true")
	}
	if got.CreatedAt.IsZero() {
		t.Error("CreatedAt should be set")
	}

	// Update
	got.Name = "Alice Updated"
	got.Role = "user"
	if err := Users.Update(got); err != nil {
		t.Fatalf("Update user: %v", err)
	}
	got2, err := Users.Get(id)
	if err != nil {
		t.Fatalf("Get after update: %v", err)
	}
	if got2.Name != "Alice Updated" {
		t.Errorf("Name after update = %q, want %q", got2.Name, "Alice Updated")
	}
	if got2.Role != "user" {
		t.Errorf("Role after update = %q, want %q", got2.Role, "user")
	}

	// Delete
	if err := Users.Delete(got2); err != nil {
		t.Fatalf("Delete user: %v", err)
	}
	deleted, err := Users.Get(id)
	if err == nil && deleted != nil {
		t.Error("Get after delete should return error or nil")
	}
}

// ---------- Page CRUD ----------

func TestPageCRUD(t *testing.T) {
	// Create a page with a custom slug ID
	page := &Page{
		Position: 0,
	}
	page.ID = "about"
	id, err := Pages.Insert(page)
	if err != nil {
		t.Fatalf("Insert page: %v", err)
	}
	if id != "about" {
		t.Fatalf("ID = %q, want %q", id, "about")
	}

	// Create page content
	content := &PageContent{
		PageID:      id,
		Title:       "About Us",
		Description: "Learn more about us",
		HTML:        "<h1>About</h1><p>Hello world</p>",
		Status:      StatusPublished,
	}
	contentID, err := PageContents.Insert(content)
	if err != nil {
		t.Fatalf("Insert page content: %v", err)
	}
	if contentID == "" {
		t.Fatal("PageContent insert should return a non-empty ID")
	}

	// Get page and verify content accessors
	got, err := Pages.Get(id)
	if err != nil {
		t.Fatalf("Get page: %v", err)
	}

	if got.Title() != "About Us" {
		t.Errorf("Title() = %q, want %q", got.Title(), "About Us")
	}
	if got.Description() != "Learn more about us" {
		t.Errorf("Description() = %q, want %q", got.Description(), "Learn more about us")
	}
	if got.HTML() != "<h1>About</h1><p>Hello world</p>" {
		t.Errorf("HTML() = %q, want expected value", got.HTML())
	}
	if got.Path() != "/about" {
		t.Errorf("Path() = %q, want %q", got.Path(), "/about")
	}

	// IsPublished / Status
	if !got.IsPublished() {
		t.Error("Page with published content should report IsPublished() = true")
	}
	if got.Status() != StatusPublished {
		t.Errorf("Status() = %q, want %q", got.Status(), StatusPublished)
	}

	// LatestContent / PublishedContent
	latest := got.LatestContent()
	if latest == nil {
		t.Fatal("LatestContent() should not be nil")
	}
	if latest.Title != "About Us" {
		t.Errorf("LatestContent().Title = %q, want %q", latest.Title, "About Us")
	}
	pub := got.PublishedContent()
	if pub == nil {
		t.Fatal("PublishedContent() should not be nil")
	}
	if !pub.IsPublished() {
		t.Error("PublishedContent().IsPublished() should be true")
	}
	if pub.IsDraft() {
		t.Error("PublishedContent().IsDraft() should be false")
	}

	// Add a draft version
	draftContent := &PageContent{
		PageID:      id,
		Title:       "About Us - Draft",
		Description: "Draft description",
		HTML:        "<h1>About Draft</h1>",
		Status:      StatusDraft,
	}
	_, err = PageContents.Insert(draftContent)
	if err != nil {
		t.Fatalf("Insert draft content: %v", err)
	}

	// Latest should now be the draft
	latest2 := got.LatestContent()
	if latest2 == nil {
		t.Fatal("LatestContent() after draft insert should not be nil")
	}
	if latest2.Title != "About Us - Draft" {
		t.Errorf("LatestContent().Title = %q, want %q", latest2.Title, "About Us - Draft")
	}
	if latest2.IsPublished() {
		t.Error("Draft content should not be published")
	}
	if !latest2.IsDraft() {
		t.Error("Draft content IsDraft() should be true")
	}

	// Contents should return multiple versions
	contents, err := got.Contents()
	if err != nil {
		t.Fatalf("Contents(): %v", err)
	}
	if len(contents) != 2 {
		t.Errorf("len(Contents()) = %d, want 2", len(contents))
	}

	// PageContent.Page() back-reference
	parentPage, err := latest.Page()
	if err != nil {
		t.Fatalf("PageContent.Page(): %v", err)
	}
	if parentPage.ID != id {
		t.Errorf("PageContent.Page().ID = %q, want %q", parentPage.ID, id)
	}

	// Update page
	got.Position = 5
	if err := Pages.Update(got); err != nil {
		t.Fatalf("Update page: %v", err)
	}
	got2, err := Pages.Get(id)
	if err != nil {
		t.Fatalf("Get after update: %v", err)
	}
	if got2.Position != 5 {
		t.Errorf("Position after update = %d, want 5", got2.Position)
	}

	// Delete page content, then page
	for _, c := range contents {
		if err := PageContents.Delete(c); err != nil {
			t.Fatalf("Delete page content: %v", err)
		}
	}
	if err := Pages.Delete(got2); err != nil {
		t.Fatalf("Delete page: %v", err)
	}
	deleted, err := Pages.Get(id)
	if err == nil && deleted != nil {
		t.Error("Get after delete should return error or nil")
	}
}

// ---------- Page Tree (Parent/Children) ----------

func TestPageTree(t *testing.T) {
	// Create parent page
	parent := &Page{Position: 0}
	parent.ID = "services"
	_, err := Pages.Insert(parent)
	if err != nil {
		t.Fatalf("Insert parent: %v", err)
	}

	// Create child pages
	child1 := &Page{ParentID: "services", Position: 0}
	child1.ID = "consulting"
	_, err = Pages.Insert(child1)
	if err != nil {
		t.Fatalf("Insert child1: %v", err)
	}

	child2 := &Page{ParentID: "services", Position: 1}
	child2.ID = "training"
	_, err = Pages.Insert(child2)
	if err != nil {
		t.Fatalf("Insert child2: %v", err)
	}

	// Verify Children()
	children, err := parent.Children()
	if err != nil {
		t.Fatalf("Children(): %v", err)
	}
	if len(children) != 2 {
		t.Fatalf("len(Children()) = %d, want 2", len(children))
	}
	if children[0].ID != "consulting" {
		t.Errorf("children[0].ID = %q, want %q", children[0].ID, "consulting")
	}
	if children[1].ID != "training" {
		t.Errorf("children[1].ID = %q, want %q", children[1].ID, "training")
	}

	// Verify Parent()
	p, err := child1.Parent()
	if err != nil {
		t.Fatalf("child1.Parent(): %v", err)
	}
	if p == nil {
		t.Fatal("child1.Parent() should not be nil")
	}
	if p.ID != "services" {
		t.Errorf("Parent().ID = %q, want %q", p.ID, "services")
	}

	// Root page has no parent
	rootParent, err := parent.Parent()
	if err != nil {
		t.Fatalf("parent.Parent(): %v", err)
	}
	if rootParent != nil {
		t.Error("Root page Parent() should be nil")
	}

	// Verify Siblings()
	siblings, err := child1.Siblings()
	if err != nil {
		t.Fatalf("Siblings(): %v", err)
	}
	if len(siblings) != 1 {
		t.Fatalf("len(Siblings()) = %d, want 1", len(siblings))
	}
	if siblings[0].ID != "training" {
		t.Errorf("siblings[0].ID = %q, want %q", siblings[0].ID, "training")
	}

	// Verify Path() for nested pages
	if child1.Path() != "/services/consulting" {
		t.Errorf("child1.Path() = %q, want %q", child1.Path(), "/services/consulting")
	}
	if parent.Path() != "/services" {
		t.Errorf("parent.Path() = %q, want %q", parent.Path(), "/services")
	}

	// Home page path
	home := &Page{Position: 0}
	home.ID = "home"
	_, err = Pages.Insert(home)
	if err != nil {
		t.Fatalf("Insert home: %v", err)
	}
	if home.Path() != "/" {
		t.Errorf("home.Path() = %q, want %q", home.Path(), "/")
	}

	// Cleanup
	for _, p := range []*Page{child1, child2, parent, home} {
		Pages.Delete(p)
	}
}

// ---------- Collection CRUD ----------

func TestCollectionCRUD(t *testing.T) {
	// Create
	coll := &Collection{
		Name:        "Blog Posts",
		Description: "All blog posts",
		Type:        CollectionTypeBase,
		Schema:      `[{"name":"title","type":"text","required":true}]`,
	}
	coll.ID = "blog_posts"
	id, err := Collections.Insert(coll)
	if err != nil {
		t.Fatalf("Insert collection: %v", err)
	}
	if id != "blog_posts" {
		t.Fatalf("ID = %q, want %q", id, "blog_posts")
	}

	// Get
	got, err := Collections.Get(id)
	if err != nil {
		t.Fatalf("Get collection: %v", err)
	}
	if got.Name != "Blog Posts" {
		t.Errorf("Name = %q, want %q", got.Name, "Blog Posts")
	}
	if got.Description != "All blog posts" {
		t.Errorf("Description = %q, want %q", got.Description, "All blog posts")
	}
	if !got.IsBase() {
		t.Error("IsBase() should be true for base collection")
	}
	if got.IsView() {
		t.Error("IsView() should be false for base collection")
	}

	// Empty collection should have 0 documents
	if got.DocumentCount() != 0 {
		t.Errorf("DocumentCount() = %d, want 0", got.DocumentCount())
	}
	docs, err := got.Documents()
	if err != nil {
		t.Fatalf("Documents(): %v", err)
	}
	if len(docs) != 0 {
		t.Errorf("len(Documents()) = %d, want 0", len(docs))
	}

	// Delete
	if err := Collections.Delete(got); err != nil {
		t.Fatalf("Delete collection: %v", err)
	}
	deleted, err := Collections.Get(id)
	if err == nil && deleted != nil {
		t.Error("Get after delete should return error or nil")
	}
}

// ---------- Document CRUD ----------

func TestDocumentCRUD(t *testing.T) {
	// Create parent collection
	coll := &Collection{
		Name:   "Products",
		Type:   CollectionTypeBase,
		Schema: `[{"name":"name","type":"text"},{"name":"price","type":"number"}]`,
	}
	coll.ID = "products"
	_, err := Collections.Insert(coll)
	if err != nil {
		t.Fatalf("Insert collection: %v", err)
	}

	// Create document with JSON data
	doc := &Document{
		CollectionID: "products",
	}
	err = doc.SetAll(map[string]any{
		"name":  "Widget",
		"price": 19.99,
	})
	if err != nil {
		t.Fatalf("SetAll: %v", err)
	}
	id, err := Documents.Insert(doc)
	if err != nil {
		t.Fatalf("Insert document: %v", err)
	}
	if id == "" {
		t.Fatal("Insert should return a non-empty ID")
	}

	// Get
	got, err := Documents.Get(id)
	if err != nil {
		t.Fatalf("Get document: %v", err)
	}
	if got.CollectionID != "products" {
		t.Errorf("CollectionID = %q, want %q", got.CollectionID, "products")
	}

	// Back-reference to collection
	parent, err := got.Collection()
	if err != nil {
		t.Fatalf("Document.Collection(): %v", err)
	}
	if parent.ID != "products" {
		t.Errorf("Collection().ID = %q, want %q", parent.ID, "products")
	}

	// Collection.DocumentCount should now be 1
	if coll.DocumentCount() != 1 {
		t.Errorf("DocumentCount() = %d, want 1", coll.DocumentCount())
	}

	// Update document data using Set
	if err := got.Set("name", "Super Widget"); err != nil {
		t.Fatalf("Set: %v", err)
	}
	if err := Documents.Update(got); err != nil {
		t.Fatalf("Update document: %v", err)
	}
	got2, err := Documents.Get(id)
	if err != nil {
		t.Fatalf("Get after update: %v", err)
	}
	if got2.GetString("name") != "Super Widget" {
		t.Errorf("GetString(name) = %q, want %q", got2.GetString("name"), "Super Widget")
	}

	// Delete document
	if err := Documents.Delete(got2); err != nil {
		t.Fatalf("Delete document: %v", err)
	}
	deleted, err := Documents.Get(id)
	if err == nil && deleted != nil {
		t.Error("Get after delete should return error or nil")
	}

	// Cleanup collection
	Collections.Delete(coll)
}

// ---------- Document Data Access ----------

func TestDocumentDataAccess(t *testing.T) {
	doc := &Document{}
	err := doc.SetAll(map[string]any{
		"title":     "My Post",
		"count":     42.0, // JSON numbers are float64
		"published": true,
		"created":   "2025-01-15T10:30:00Z",
		"tags":      []any{"go", "web", "cms"},
		"rating":    4.5,
	})
	if err != nil {
		t.Fatalf("SetAll: %v", err)
	}

	// GetString
	if got := doc.GetString("title"); got != "My Post" {
		t.Errorf("GetString(title) = %q, want %q", got, "My Post")
	}
	if got := doc.GetString("nonexistent"); got != "" {
		t.Errorf("GetString(nonexistent) = %q, want empty", got)
	}

	// GetInt
	if got := doc.GetInt("count"); got != 42 {
		t.Errorf("GetInt(count) = %d, want 42", got)
	}
	if got := doc.GetInt("nonexistent"); got != 0 {
		t.Errorf("GetInt(nonexistent) = %d, want 0", got)
	}

	// GetFloat
	if got := doc.GetFloat("rating"); got != 4.5 {
		t.Errorf("GetFloat(rating) = %f, want 4.5", got)
	}

	// GetBool
	if got := doc.GetBool("published"); !got {
		t.Error("GetBool(published) = false, want true")
	}
	if got := doc.GetBool("nonexistent"); got {
		t.Error("GetBool(nonexistent) = true, want false")
	}

	// GetTime
	expectedTime, _ := time.Parse(time.RFC3339, "2025-01-15T10:30:00Z")
	if got := doc.GetTime("created"); !got.Equal(expectedTime) {
		t.Errorf("GetTime(created) = %v, want %v", got, expectedTime)
	}
	if got := doc.GetTime("nonexistent"); !got.IsZero() {
		t.Errorf("GetTime(nonexistent) = %v, want zero", got)
	}

	// GetStrings
	tags := doc.GetStrings("tags")
	if len(tags) != 3 {
		t.Fatalf("len(GetStrings(tags)) = %d, want 3", len(tags))
	}
	if tags[0] != "go" || tags[1] != "web" || tags[2] != "cms" {
		t.Errorf("GetStrings(tags) = %v, want [go web cms]", tags)
	}
	if got := doc.GetStrings("nonexistent"); got != nil {
		t.Errorf("GetStrings(nonexistent) = %v, want nil", got)
	}

	// Set single field
	if err := doc.Set("title", "Updated Post"); err != nil {
		t.Fatalf("Set: %v", err)
	}
	if got := doc.GetString("title"); got != "Updated Post" {
		t.Errorf("GetString after Set = %q, want %q", got, "Updated Post")
	}
	// Verify other fields are preserved
	if got := doc.GetInt("count"); got != 42 {
		t.Errorf("GetInt(count) after Set(title) = %d, want 42 (should be preserved)", got)
	}

	// Nil document returns zero values
	var nilDoc *Document
	if got := nilDoc.GetString("any"); got != "" {
		t.Errorf("nil.GetString = %q, want empty", got)
	}
	if got := nilDoc.GetInt("any"); got != 0 {
		t.Errorf("nil.GetInt = %d, want 0", got)
	}
	if got := nilDoc.GetBool("any"); got {
		t.Error("nil.GetBool = true, want false")
	}
	if got := nilDoc.GetTime("any"); !got.IsZero() {
		t.Errorf("nil.GetTime = %v, want zero", got)
	}
	if got := nilDoc.GetFloat("any"); got != 0 {
		t.Errorf("nil.GetFloat = %f, want 0", got)
	}
	if got := nilDoc.GetStrings("any"); got != nil {
		t.Errorf("nil.GetStrings = %v, want nil", got)
	}
}

// ---------- Conversation / Messages ----------

func TestConversationMessages(t *testing.T) {
	// Create a user for the conversation
	user := &User{
		Email: "bob@example.com",
		Name:  "Bob",
		Role:  "user",
	}
	userID, err := Users.Insert(user)
	if err != nil {
		t.Fatalf("Insert user: %v", err)
	}

	// Create conversation
	conv := &Conversation{
		UserID:  userID,
		Title:   "Help with my site",
		Context: `{"currentPageID":"home"}`,
	}
	convID, err := Conversations.Insert(conv)
	if err != nil {
		t.Fatalf("Insert conversation: %v", err)
	}
	if convID == "" {
		t.Fatal("Conversation ID should not be empty")
	}

	// Verify conversation.User() back-reference
	convUser, err := conv.User()
	if err != nil {
		t.Fatalf("Conversation.User(): %v", err)
	}
	if convUser.ID != userID {
		t.Errorf("User().ID = %q, want %q", convUser.ID, userID)
	}

	// User.Conversations() should list this conversation
	convs, err := user.Conversations()
	if err != nil {
		t.Fatalf("User.Conversations(): %v", err)
	}
	if len(convs) != 1 {
		t.Fatalf("len(Conversations()) = %d, want 1", len(convs))
	}
	if convs[0].ID != convID {
		t.Errorf("Conversations()[0].ID = %q, want %q", convs[0].ID, convID)
	}

	// Empty conversation should have 0 messages
	if conv.MessageCount() != 0 {
		t.Errorf("MessageCount() = %d, want 0", conv.MessageCount())
	}

	// Add messages
	msg1 := &Message{
		ConversationID: convID,
		Role:           "user",
		Content:        "Can you create an about page?",
		Status:         "complete",
	}
	msg1ID, err := Messages.Insert(msg1)
	if err != nil {
		t.Fatalf("Insert message 1: %v", err)
	}

	msg2 := &Message{
		ConversationID: convID,
		Role:           "assistant",
		Content:        "Sure, I will create an about page for you.",
		ToolCalls:      `[{"id":"call_1","name":"create_page","arguments":"{}","result":"done","error":""}]`,
		Status:         "complete",
	}
	msg2ID, err := Messages.Insert(msg2)
	if err != nil {
		t.Fatalf("Insert message 2: %v", err)
	}

	// Verify MessageCount
	if conv.MessageCount() != 2 {
		t.Errorf("MessageCount() = %d, want 2", conv.MessageCount())
	}

	// Verify Messages()
	msgs, err := conv.Messages()
	if err != nil {
		t.Fatalf("Messages(): %v", err)
	}
	if len(msgs) != 2 {
		t.Fatalf("len(Messages()) = %d, want 2", len(msgs))
	}
	if msgs[0].ID != msg1ID {
		t.Errorf("msgs[0].ID = %q, want %q", msgs[0].ID, msg1ID)
	}
	if msgs[1].ID != msg2ID {
		t.Errorf("msgs[1].ID = %q, want %q", msgs[1].ID, msg2ID)
	}

	// Verify LastMessage()
	last, err := conv.LastMessage()
	if err != nil {
		t.Fatalf("LastMessage(): %v", err)
	}
	if last.ID != msg2ID {
		t.Errorf("LastMessage().ID = %q, want %q", last.ID, msg2ID)
	}

	// Verify Message.Conversation() back-reference
	msgConv, err := msg1.Conversation()
	if err != nil {
		t.Fatalf("Message.Conversation(): %v", err)
	}
	if msgConv.ID != convID {
		t.Errorf("Conversation().ID = %q, want %q", msgConv.ID, convID)
	}

	// Verify ToolCalls on msg2
	if !msg2.HasToolCalls() {
		t.Error("msg2 should have tool calls")
	}
	if msg2.ToolCallCount() != 1 {
		t.Errorf("ToolCallCount() = %d, want 1", msg2.ToolCallCount())
	}
	tcList := msg2.ToolCallsList()
	if len(tcList) != 1 {
		t.Fatalf("len(ToolCallsList()) = %d, want 1", len(tcList))
	}
	if tcList[0].Name != "create_page" {
		t.Errorf("ToolCallsList()[0].Name = %q, want %q", tcList[0].Name, "create_page")
	}

	// Message without tool calls
	if msg1.HasToolCalls() {
		t.Error("msg1 should not have tool calls")
	}
	if msg1.ToolCallCount() != 0 {
		t.Errorf("msg1.ToolCallCount() = %d, want 0", msg1.ToolCallCount())
	}

	// Verify status helpers
	pendingMsg := &Message{Status: "pending"}
	if !pendingMsg.IsPending() {
		t.Error("IsPending() should be true for pending message")
	}
	streamingMsg := &Message{Status: "streaming"}
	if !streamingMsg.IsStreaming() {
		t.Error("IsStreaming() should be true for streaming message")
	}

	// Cleanup
	Messages.Delete(msg1)
	Messages.Delete(msg2)
	Conversations.Delete(conv)
	Users.Delete(user)
}

// ---------- Mutation Tracking ----------

func TestMutationTracking(t *testing.T) {
	// Create conversation and message for the mutation
	conv := &Conversation{
		UserID: "test-user-mut",
		Title:  "Mutation test",
	}
	convID, err := Conversations.Insert(conv)
	if err != nil {
		t.Fatalf("Insert conversation: %v", err)
	}

	msg := &Message{
		ConversationID: convID,
		Role:           "assistant",
		Content:        "Creating your about page now.",
		Status:         "complete",
	}
	msgID, err := Messages.Insert(msg)
	if err != nil {
		t.Fatalf("Insert message: %v", err)
	}

	// Create a mutation
	mut := &Mutation{
		MessageID:   msgID,
		Action:      "create",
		EntityType:  "page",
		EntityID:    "about",
		BeforeState: "",
		AfterState:  `{"id":"about","title":"About Us"}`,
		Undone:      false,
	}
	mutID, err := Mutations.Insert(mut)
	if err != nil {
		t.Fatalf("Insert mutation: %v", err)
	}
	if mutID == "" {
		t.Fatal("Mutation ID should not be empty")
	}

	// Get mutation
	got, err := Mutations.Get(mutID)
	if err != nil {
		t.Fatalf("Get mutation: %v", err)
	}
	if got.MessageID != msgID {
		t.Errorf("MessageID = %q, want %q", got.MessageID, msgID)
	}
	if got.Action != "create" {
		t.Errorf("Action = %q, want %q", got.Action, "create")
	}
	if got.EntityType != "page" {
		t.Errorf("EntityType = %q, want %q", got.EntityType, "page")
	}
	if got.EntityID != "about" {
		t.Errorf("EntityID = %q, want %q", got.EntityID, "about")
	}
	if got.Undone {
		t.Error("Undone should be false initially")
	}
	if got.AfterState != `{"id":"about","title":"About Us"}` {
		t.Errorf("AfterState = %q, unexpected", got.AfterState)
	}

	// Verify Mutation.Message() back-reference
	mutMsg, err := got.Message()
	if err != nil {
		t.Fatalf("Mutation.Message(): %v", err)
	}
	if mutMsg.ID != msgID {
		t.Errorf("Message().ID = %q, want %q", mutMsg.ID, msgID)
	}

	// Verify Message.Mutations()
	mutations, err := msg.Mutations()
	if err != nil {
		t.Fatalf("Message.Mutations(): %v", err)
	}
	if len(mutations) != 1 {
		t.Fatalf("len(Mutations()) = %d, want 1", len(mutations))
	}
	if mutations[0].ID != mutID {
		t.Errorf("Mutations()[0].ID = %q, want %q", mutations[0].ID, mutID)
	}

	// Add a second mutation (update) to the same message
	mut2 := &Mutation{
		MessageID:   msgID,
		Action:      "update",
		EntityType:  "page",
		EntityID:    "about",
		BeforeState: `{"id":"about","title":"About Us"}`,
		AfterState:  `{"id":"about","title":"About Our Team"}`,
		Undone:      false,
	}
	mut2ID, err := Mutations.Insert(mut2)
	if err != nil {
		t.Fatalf("Insert mutation 2: %v", err)
	}

	mutations2, err := msg.Mutations()
	if err != nil {
		t.Fatalf("Message.Mutations() after second insert: %v", err)
	}
	if len(mutations2) != 2 {
		t.Fatalf("len(Mutations()) = %d, want 2", len(mutations2))
	}

	// Mark mutation as undone
	got.Undone = true
	if err := Mutations.Update(got); err != nil {
		t.Fatalf("Update mutation: %v", err)
	}
	updated, err := Mutations.Get(mutID)
	if err != nil {
		t.Fatalf("Get after update: %v", err)
	}
	if !updated.Undone {
		t.Error("Undone should be true after update")
	}

	// Cleanup
	Mutations.Delete(mut)
	Mutations.Delete(mut2)
	_ = mut2ID // suppress unused
	Messages.Delete(msg)
	Conversations.Delete(conv)
}
← Back