tools_test.go
package assist_test
import (
"encoding/json"
"testing"
"github.com/readysite/readysite/pkg/assistant"
"github.com/readysite/readysite/website/internal/assist"
"github.com/readysite/readysite/website/models"
)
// =============================================================================
// Tool Definitions
// =============================================================================
func TestToolDefinitions(t *testing.T) {
t.Run("PageTools", func(t *testing.T) {
expectedNames := []string{
"create_page", "update_page", "delete_page", "delete_pages", "get_page", "list_pages",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
continue
}
if tool.Name != name {
t.Errorf("expected tool name %q, got %q", name, tool.Name)
}
}
// Verify required parameters
createPage := assist.GetTool("create_page")
assertRequiredParams(t, createPage, "title", "html")
updatePage := assist.GetTool("update_page")
assertRequiredParams(t, updatePage, "id")
deletePage := assist.GetTool("delete_page")
assertRequiredParams(t, deletePage, "id")
deletePages := assist.GetTool("delete_pages")
assertRequiredParams(t, deletePages, "ids")
getPage := assist.GetTool("get_page")
assertRequiredParams(t, getPage, "id")
})
t.Run("CollectionTools", func(t *testing.T) {
expectedNames := []string{
"create_collection", "update_collection", "delete_collection",
"delete_collections", "get_collection", "list_collections",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
createCol := assist.GetTool("create_collection")
assertRequiredParams(t, createCol, "name")
updateCol := assist.GetTool("update_collection")
assertRequiredParams(t, updateCol, "id")
})
t.Run("DocumentTools", func(t *testing.T) {
expectedNames := []string{
"create_document", "create_documents", "update_document",
"delete_document", "get_document", "query_documents",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
createDoc := assist.GetTool("create_document")
assertRequiredParams(t, createDoc, "collection_id", "data")
queryDocs := assist.GetTool("query_documents")
assertRequiredParams(t, queryDocs, "collection_id")
})
t.Run("PartialTools", func(t *testing.T) {
expectedNames := []string{
"create_partial", "update_partial", "delete_partial", "get_partial", "list_partials",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
createPartial := assist.GetTool("create_partial")
assertRequiredParams(t, createPartial, "name", "html")
updatePartial := assist.GetTool("update_partial")
assertRequiredParams(t, updatePartial, "id")
})
t.Run("FileTools", func(t *testing.T) {
expectedNames := []string{
"update_file", "get_file", "list_files", "read_file",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
updateFile := assist.GetTool("update_file")
assertRequiredParams(t, updateFile, "id")
readFile := assist.GetTool("read_file")
assertRequiredParams(t, readFile, "id")
})
t.Run("NoteTools", func(t *testing.T) {
expectedNames := []string{
"create_note", "list_notes", "get_note", "update_note", "delete_note",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
createNote := assist.GetTool("create_note")
assertRequiredParams(t, createNote, "type", "category", "title", "content")
getNote := assist.GetTool("get_note")
assertRequiredParams(t, getNote, "id")
updateNote := assist.GetTool("update_note")
assertRequiredParams(t, updateNote, "id")
deleteNote := assist.GetTool("delete_note")
assertRequiredParams(t, deleteNote, "id")
})
t.Run("UserTools", func(t *testing.T) {
expectedNames := []string{
"create_user", "update_user", "delete_user", "list_users",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
createUser := assist.GetTool("create_user")
assertRequiredParams(t, createUser, "email", "password")
updateUser := assist.GetTool("update_user")
assertRequiredParams(t, updateUser, "id")
deleteUser := assist.GetTool("delete_user")
assertRequiredParams(t, deleteUser, "id")
})
t.Run("UtilityTools", func(t *testing.T) {
expectedNames := []string{
"validate_template", "navigate_user",
}
for _, name := range expectedNames {
tool := assist.GetTool(name)
if tool == nil {
t.Errorf("expected tool %q to exist", name)
}
}
validateTemplate := assist.GetTool("validate_template")
assertRequiredParams(t, validateTemplate, "html")
navigateUser := assist.GetTool("navigate_user")
assertRequiredParams(t, navigateUser, "url")
})
t.Run("GetToolReturnsNilForUnknown", func(t *testing.T) {
tool := assist.GetTool("nonexistent_tool")
if tool != nil {
t.Error("expected nil for unknown tool")
}
})
}
// =============================================================================
// User Tool Execution
// =============================================================================
func TestCreateUserTool(t *testing.T) {
exec := assist.NewExecutor(nil)
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"test@example.com","password":"securepass123","name":"Test User","role":"user"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
id, ok := resp["id"].(string)
if !ok || id == "" {
t.Fatal("expected non-empty user ID in response")
}
if resp["email"] != "test@example.com" {
t.Errorf("expected email 'test@example.com', got %v", resp["email"])
}
if resp["name"] != "Test User" {
t.Errorf("expected name 'Test User', got %v", resp["name"])
}
if resp["role"] != "user" {
t.Errorf("expected role 'user', got %v", resp["role"])
}
// Verify user was created in DB
user, err := models.Users.Get(id)
if err != nil {
t.Fatalf("failed to get created user: %v", err)
}
if user.Email != "test@example.com" {
t.Errorf("expected email 'test@example.com', got %q", user.Email)
}
if user.Name != "Test User" {
t.Errorf("expected name 'Test User', got %q", user.Name)
}
if user.Role != "user" {
t.Errorf("expected role 'user', got %q", user.Role)
}
if !user.Verified {
t.Error("expected admin-created user to be auto-verified")
}
// Cleanup
models.Users.Delete(user)
}
func TestCreateUserTool_DefaultRole(t *testing.T) {
exec := assist.NewExecutor(nil)
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"default-role@example.com","password":"securepass123"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["role"] != "user" {
t.Errorf("expected default role 'user', got %v", resp["role"])
}
// Cleanup
id := resp["id"].(string)
user, _ := models.Users.Get(id)
if user != nil {
models.Users.Delete(user)
}
}
func TestCreateUserTool_DuplicateEmail(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create first user
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"duplicate@example.com","password":"securepass123"}`,
})
if err != nil {
t.Fatalf("unexpected error creating first user: %v", err)
}
var resp map[string]any
json.Unmarshal([]byte(result), &resp)
firstID := resp["id"].(string)
// Try to create duplicate
_, err = exec.Execute(assistant.ToolCall{
ID: "call_2",
Name: "create_user",
Arguments: `{"email":"duplicate@example.com","password":"securepass456"}`,
})
if err == nil {
t.Error("expected error for duplicate email")
}
// Cleanup
user, _ := models.Users.Get(firstID)
if user != nil {
models.Users.Delete(user)
}
}
func TestCreateUserTool_Validation(t *testing.T) {
exec := assist.NewExecutor(nil)
// Missing email
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"","password":"securepass123"}`,
})
if err == nil {
t.Error("expected error for empty email")
}
// Missing password
_, err = exec.Execute(assistant.ToolCall{
ID: "call_2",
Name: "create_user",
Arguments: `{"email":"valid@example.com","password":""}`,
})
if err == nil {
t.Error("expected error for empty password")
}
// Short password
_, err = exec.Execute(assistant.ToolCall{
ID: "call_3",
Name: "create_user",
Arguments: `{"email":"valid@example.com","password":"short"}`,
})
if err == nil {
t.Error("expected error for short password")
}
}
func TestUpdateUserTool(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create user first
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"update-me@example.com","password":"securepass123","name":"Original Name","role":"user"}`,
})
if err != nil {
t.Fatalf("unexpected error creating user: %v", err)
}
var createResp map[string]any
json.Unmarshal([]byte(result), &createResp)
userID := createResp["id"].(string)
// Update the user
result, err = exec.Execute(assistant.ToolCall{
ID: "call_2",
Name: "update_user",
Arguments: `{"id":"` + userID + `","name":"Updated Name","role":"admin"}`,
})
if err != nil {
t.Fatalf("unexpected error updating user: %v", err)
}
var updateResp map[string]any
if err := json.Unmarshal([]byte(result), &updateResp); err != nil {
t.Fatalf("failed to parse update result: %v", err)
}
if updateResp["name"] != "Updated Name" {
t.Errorf("expected name 'Updated Name', got %v", updateResp["name"])
}
if updateResp["role"] != "admin" {
t.Errorf("expected role 'admin', got %v", updateResp["role"])
}
// Verify in DB
user, err := models.Users.Get(userID)
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if user.Name != "Updated Name" {
t.Errorf("expected name 'Updated Name' in DB, got %q", user.Name)
}
if user.Role != "admin" {
t.Errorf("expected role 'admin' in DB, got %q", user.Role)
}
// Cleanup
models.Users.Delete(user)
}
func TestUpdateUserTool_NotFound(t *testing.T) {
exec := assist.NewExecutor(nil)
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "update_user",
Arguments: `{"id":"nonexistent-user-id","name":"No One"}`,
})
if err == nil {
t.Error("expected error for nonexistent user")
}
}
func TestDeleteUserTool(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create user first
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_user",
Arguments: `{"email":"delete-me@example.com","password":"securepass123"}`,
})
if err != nil {
t.Fatalf("unexpected error creating user: %v", err)
}
var createResp map[string]any
json.Unmarshal([]byte(result), &createResp)
userID := createResp["id"].(string)
// Delete the user
_, err = exec.Execute(assistant.ToolCall{
ID: "call_2",
Name: "delete_user",
Arguments: `{"id":"` + userID + `"}`,
})
if err != nil {
t.Fatalf("unexpected error deleting user: %v", err)
}
// Verify user was deleted
_, err = models.Users.Get(userID)
if err == nil {
t.Error("expected user to be deleted from DB")
}
}
func TestDeleteUserTool_NotFound(t *testing.T) {
exec := assist.NewExecutor(nil)
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "delete_user",
Arguments: `{"id":"nonexistent-user-id"}`,
})
if err == nil {
t.Error("expected error for nonexistent user")
}
}
func TestListUsersTool(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create multiple users
var userIDs []string
for _, email := range []string{"list-user-1@example.com", "list-user-2@example.com", "list-user-3@example.com"} {
result, err := exec.Execute(assistant.ToolCall{
ID: "call_create",
Name: "create_user",
Arguments: `{"email":"` + email + `","password":"securepass123","name":"` + email + `"}`,
})
if err != nil {
t.Fatalf("unexpected error creating user %s: %v", email, err)
}
var resp map[string]any
json.Unmarshal([]byte(result), &resp)
userIDs = append(userIDs, resp["id"].(string))
}
// List all users
result, err := exec.Execute(assistant.ToolCall{
ID: "call_list",
Name: "list_users",
Arguments: `{}`,
})
if err != nil {
t.Fatalf("unexpected error listing users: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse list result: %v", err)
}
count, ok := resp["count"].(float64)
if !ok {
t.Fatal("expected count in response")
}
if int(count) < 3 {
t.Errorf("expected at least 3 users, got %d", int(count))
}
users, ok := resp["users"].([]any)
if !ok {
t.Fatal("expected users array in response")
}
if len(users) < 3 {
t.Errorf("expected at least 3 users in array, got %d", len(users))
}
// Cleanup
for _, id := range userIDs {
user, _ := models.Users.Get(id)
if user != nil {
models.Users.Delete(user)
}
}
}
// =============================================================================
// Note Tool Execution
// =============================================================================
func TestNoteTools(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create a note
t.Run("CreateNote", func(t *testing.T) {
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_note",
Arguments: `{"type":"preference","category":"style","title":"Dark Theme","content":"User prefers dark theme for all pages"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
id, ok := resp["id"].(string)
if !ok || id == "" {
t.Fatal("expected non-empty note ID")
}
if resp["title"] != "Dark Theme" {
t.Errorf("expected title 'Dark Theme', got %v", resp["title"])
}
// Verify in DB
note, err := models.Notes.Get(id)
if err != nil {
t.Fatalf("failed to get note: %v", err)
}
if note.Type != "preference" {
t.Errorf("expected type 'preference', got %q", note.Type)
}
if note.Category != "style" {
t.Errorf("expected category 'style', got %q", note.Category)
}
if note.Content != "User prefers dark theme for all pages" {
t.Errorf("unexpected content: %q", note.Content)
}
if note.Source != "ai" {
t.Errorf("expected source 'ai', got %q", note.Source)
}
if !note.Active {
t.Error("expected note to be active by default")
}
// Cleanup
models.Notes.Delete(note)
})
t.Run("CreateNote_InvalidType", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_note",
Arguments: `{"type":"invalid","category":"style","title":"Test","content":"Test"}`,
})
if err == nil {
t.Error("expected error for invalid note type")
}
})
t.Run("CreateNote_InvalidCategory", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_note",
Arguments: `{"type":"preference","category":"invalid","title":"Test","content":"Test"}`,
})
if err == nil {
t.Error("expected error for invalid note category")
}
})
// Get a note
t.Run("GetNote", func(t *testing.T) {
// Create note for retrieval
note := &models.Note{
Type: "convention",
Category: "content",
Title: "Get Test Note",
Content: "This note is for get testing",
Source: "ai",
Active: true,
}
id, _ := models.Notes.Insert(note)
defer func() {
n, _ := models.Notes.Get(id)
if n != nil {
models.Notes.Delete(n)
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "get_note",
Arguments: `{"id":"` + id + `"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["id"] != id {
t.Errorf("expected id %q, got %v", id, resp["id"])
}
if resp["title"] != "Get Test Note" {
t.Errorf("expected title 'Get Test Note', got %v", resp["title"])
}
if resp["type"] != "convention" {
t.Errorf("expected type 'convention', got %v", resp["type"])
}
})
t.Run("GetNote_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "get_note",
Arguments: `{"id":"nonexistent-note"}`,
})
if err == nil {
t.Error("expected error for nonexistent note")
}
})
// Update a note
t.Run("UpdateNote", func(t *testing.T) {
// Create note to update
note := &models.Note{
Type: "learned",
Category: "behavior",
Title: "Original Title",
Content: "Original content",
Source: "ai",
Active: true,
}
id, _ := models.Notes.Insert(note)
defer func() {
n, _ := models.Notes.Get(id)
if n != nil {
models.Notes.Delete(n)
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "update_note",
Arguments: `{"id":"` + id + `","title":"Updated Title","content":"Updated content","active":false}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["title"] != "Updated Title" {
t.Errorf("expected title 'Updated Title', got %v", resp["title"])
}
// Verify in DB
updated, err := models.Notes.Get(id)
if err != nil {
t.Fatalf("failed to get updated note: %v", err)
}
if updated.Title != "Updated Title" {
t.Errorf("expected title 'Updated Title' in DB, got %q", updated.Title)
}
if updated.Content != "Updated content" {
t.Errorf("expected content 'Updated content' in DB, got %q", updated.Content)
}
if updated.Active {
t.Error("expected note to be inactive after update")
}
})
t.Run("UpdateNote_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "update_note",
Arguments: `{"id":"nonexistent-note","title":"No"}`,
})
if err == nil {
t.Error("expected error for nonexistent note")
}
})
// Delete a note
t.Run("DeleteNote", func(t *testing.T) {
// Create note to delete
note := &models.Note{
Type: "preference",
Category: "structure",
Title: "Delete Me",
Content: "This note will be deleted",
Source: "ai",
Active: true,
}
id, _ := models.Notes.Insert(note)
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "delete_note",
Arguments: `{"id":"` + id + `"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify deletion
_, err = models.Notes.Get(id)
if err == nil {
t.Error("expected note to be deleted")
}
})
t.Run("DeleteNote_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "delete_note",
Arguments: `{"id":"nonexistent-note"}`,
})
if err == nil {
t.Error("expected error for nonexistent note")
}
})
// List notes
t.Run("ListNotes", func(t *testing.T) {
// Create several notes
var noteIDs []string
for i, noteData := range []struct {
typ, category, title string
}{
{"preference", "style", "List Note 1"},
{"convention", "content", "List Note 2"},
{"learned", "behavior", "List Note 3"},
} {
note := &models.Note{
Type: noteData.typ,
Category: noteData.category,
Title: noteData.title,
Content: "Content for listing",
Source: "ai",
Active: true,
}
id, err := models.Notes.Insert(note)
if err != nil {
t.Fatalf("failed to create note %d: %v", i, err)
}
noteIDs = append(noteIDs, id)
}
defer func() {
for _, id := range noteIDs {
n, _ := models.Notes.Get(id)
if n != nil {
models.Notes.Delete(n)
}
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "list_notes",
Arguments: `{}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
count, ok := resp["count"].(float64)
if !ok {
t.Fatal("expected count in response")
}
if int(count) < 3 {
t.Errorf("expected at least 3 notes, got %d", int(count))
}
notes, ok := resp["notes"].([]any)
if !ok {
t.Fatal("expected notes array in response")
}
if len(notes) < 3 {
t.Errorf("expected at least 3 notes in array, got %d", len(notes))
}
})
t.Run("ListNotes_FilterByType", func(t *testing.T) {
// Create notes of specific type
note := &models.Note{
Type: "preference",
Category: "style",
Title: "Filter Type Note",
Content: "For type filtering",
Source: "ai",
Active: true,
}
id, _ := models.Notes.Insert(note)
defer func() {
n, _ := models.Notes.Get(id)
if n != nil {
models.Notes.Delete(n)
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "list_notes",
Arguments: `{"type":"preference"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
notes := resp["notes"].([]any)
for _, n := range notes {
noteMap := n.(map[string]any)
if noteMap["type"] != "preference" {
t.Errorf("expected all notes to be type 'preference', got %v", noteMap["type"])
}
}
})
}
// =============================================================================
// Partial Tool Execution
// =============================================================================
func TestPartialTools(t *testing.T) {
exec := assist.NewExecutor(nil)
// Create a partial
t.Run("CreatePartial", func(t *testing.T) {
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "create_partial",
Arguments: `{"name":"Test Header","html":"<header><h1>My Site</h1></header>","id":"test-header","description":"Site header","published":true}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["id"] != "test-header" {
t.Errorf("expected id 'test-header', got %v", resp["id"])
}
if resp["name"] != "Test Header" {
t.Errorf("expected name 'Test Header', got %v", resp["name"])
}
// Verify in DB
partial, err := models.Partials.Get("test-header")
if err != nil {
t.Fatalf("failed to get partial: %v", err)
}
if partial.Name != "Test Header" {
t.Errorf("expected name 'Test Header', got %q", partial.Name)
}
if partial.HTML != "<header><h1>My Site</h1></header>" {
t.Errorf("expected HTML '<header><h1>My Site</h1></header>', got %q", partial.HTML)
}
if partial.Description != "Site header" {
t.Errorf("expected description 'Site header', got %q", partial.Description)
}
if !partial.Published {
t.Error("expected partial to be published")
}
// Cleanup
models.Partials.Delete(partial)
})
// Get a partial
t.Run("GetPartial", func(t *testing.T) {
// Create partial for retrieval
partial := &models.Partial{
Name: "Get Test Partial",
Description: "For get testing",
HTML: "<nav>Navigation</nav>",
Published: true,
}
partial.ID = "get-test-partial"
models.Partials.Insert(partial)
defer func() {
p, _ := models.Partials.Get("get-test-partial")
if p != nil {
models.Partials.Delete(p)
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "get_partial",
Arguments: `{"id":"get-test-partial"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["id"] != "get-test-partial" {
t.Errorf("expected id 'get-test-partial', got %v", resp["id"])
}
if resp["name"] != "Get Test Partial" {
t.Errorf("expected name 'Get Test Partial', got %v", resp["name"])
}
if resp["html"] != "<nav>Navigation</nav>" {
t.Errorf("expected html '<nav>Navigation</nav>', got %v", resp["html"])
}
if resp["published"] != true {
t.Errorf("expected published true, got %v", resp["published"])
}
})
t.Run("GetPartial_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "get_partial",
Arguments: `{"id":"nonexistent-partial"}`,
})
if err == nil {
t.Error("expected error for nonexistent partial")
}
})
// Update a partial
t.Run("UpdatePartial", func(t *testing.T) {
// Create partial to update
partial := &models.Partial{
Name: "Original Partial",
Description: "Original description",
HTML: "<footer>Old Footer</footer>",
Published: false,
}
partial.ID = "update-test-partial"
models.Partials.Insert(partial)
defer func() {
p, _ := models.Partials.Get("update-test-partial")
if p != nil {
models.Partials.Delete(p)
}
}()
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "update_partial",
Arguments: `{"id":"update-test-partial","name":"Updated Partial","html":"<footer>New Footer</footer>","published":true}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
if resp["name"] != "Updated Partial" {
t.Errorf("expected name 'Updated Partial', got %v", resp["name"])
}
// Verify in DB
updated, err := models.Partials.Get("update-test-partial")
if err != nil {
t.Fatalf("failed to get updated partial: %v", err)
}
if updated.Name != "Updated Partial" {
t.Errorf("expected name 'Updated Partial' in DB, got %q", updated.Name)
}
if updated.HTML != "<footer>New Footer</footer>" {
t.Errorf("expected updated HTML, got %q", updated.HTML)
}
if !updated.Published {
t.Error("expected partial to be published after update")
}
})
t.Run("UpdatePartial_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "update_partial",
Arguments: `{"id":"nonexistent-partial","name":"No"}`,
})
if err == nil {
t.Error("expected error for nonexistent partial")
}
})
// Delete a partial
t.Run("DeletePartial", func(t *testing.T) {
// Create partial to delete
partial := &models.Partial{
Name: "Delete Me Partial",
HTML: "<div>Delete</div>",
Published: true,
}
partial.ID = "delete-test-partial"
models.Partials.Insert(partial)
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "delete_partial",
Arguments: `{"id":"delete-test-partial"}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify deletion
_, err = models.Partials.Get("delete-test-partial")
if err == nil {
t.Error("expected partial to be deleted")
}
})
t.Run("DeletePartial_NotFound", func(t *testing.T) {
_, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "delete_partial",
Arguments: `{"id":"nonexistent-partial"}`,
})
if err == nil {
t.Error("expected error for nonexistent partial")
}
})
// List partials
t.Run("ListPartials", func(t *testing.T) {
// Create partials
for _, p := range []struct {
id, name string
published bool
}{
{"list-partial-1", "List Partial 1", true},
{"list-partial-2", "List Partial 2", true},
{"list-partial-3", "List Partial 3", false},
} {
partial := &models.Partial{
Name: p.name,
HTML: "<div>" + p.name + "</div>",
Published: p.published,
}
partial.ID = p.id
models.Partials.Insert(partial)
}
defer func() {
for _, id := range []string{"list-partial-1", "list-partial-2", "list-partial-3"} {
p, _ := models.Partials.Get(id)
if p != nil {
models.Partials.Delete(p)
}
}
}()
// List published only (default)
result, err := exec.Execute(assistant.ToolCall{
ID: "call_1",
Name: "list_partials",
Arguments: `{}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var resp map[string]any
if err := json.Unmarshal([]byte(result), &resp); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
partials := resp["partials"].([]any)
for _, p := range partials {
pMap := p.(map[string]any)
if pMap["published"] != true {
t.Errorf("expected only published partials in default listing, got unpublished: %v", pMap["id"])
}
}
// List including unpublished
result, err = exec.Execute(assistant.ToolCall{
ID: "call_2",
Name: "list_partials",
Arguments: `{"include_unpublished":true}`,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var respAll map[string]any
if err := json.Unmarshal([]byte(result), &respAll); err != nil {
t.Fatalf("failed to parse result: %v", err)
}
countAll := int(respAll["count"].(float64))
countPublished := int(resp["count"].(float64))
if countAll <= countPublished {
t.Errorf("expected more partials when including unpublished (all=%d, published=%d)", countAll, countPublished)
}
})
}
// =============================================================================
// Helpers
// =============================================================================
// assertRequiredParams verifies that a tool has the expected required parameters.
func assertRequiredParams(t *testing.T, tool *assistant.Tool, expected ...string) {
t.Helper()
if tool == nil {
t.Error("tool is nil")
return
}
if tool.Parameters == nil {
if len(expected) > 0 {
t.Errorf("tool %q has no parameters, expected required: %v", tool.Name, expected)
}
return
}
for _, param := range expected {
found := false
for _, req := range tool.Parameters.Required {
if req == param {
found = true
break
}
}
if !found {
t.Errorf("tool %q: expected parameter %q to be required (required list: %v)", tool.Name, param, tool.Parameters.Required)
}
}
}