package db import ( "testing" "time" ) func ptr[T any](v T) *T { return &v } func TestCreate_Note(t *testing.T) { s := testStore(t) e := &Entity{Body: "hello world", Glyph: GlyphNote} if err := s.Create(e); err != nil { t.Fatal(err) } if e.ID == "" { t.Fatal("ID not set") } got, err := s.Get(e.ID) if err != nil { t.Fatal(err) } if got.Body != "hello world" { t.Errorf("body: got %q", got.Body) } if got.Glyph != GlyphNote { t.Errorf("glyph: got %q", got.Glyph) } } func TestCreate_TodoWithTimeAnchor(t *testing.T) { s := testStore(t) e := &Entity{Body: "deploy", Glyph: GlyphTodo, TimeAnchor: ptr("14:00")} if err := s.Create(e); err != nil { t.Fatal(err) } got, err := s.Get(e.ID) if err != nil { t.Fatal(err) } if got.TimeAnchor == nil || *got.TimeAnchor != "14:00" { t.Errorf("time_anchor: got %v", got.TimeAnchor) } } func TestCreate_WithTags(t *testing.T) { s := testStore(t) e := &Entity{Body: "deploy nginx", Glyph: GlyphNote, Tags: []string{"ops", "nginx"}} if err := s.Create(e); err != nil { t.Fatal(err) } got, err := s.Get(e.ID) if err != nil { t.Fatal(err) } if len(got.Tags) != 2 { t.Fatalf("expected 2 tags, got %d", len(got.Tags)) } } func TestCreate_WithCardType(t *testing.T) { s := testStore(t) ct := CardSnippet e := &Entity{Body: "trick", Glyph: GlyphNote, CardType: &ct} if err := s.Create(e); err != nil { t.Fatal(err) } got, err := s.Get(e.ID) if err != nil { t.Fatal(err) } if got.CardType == nil || *got.CardType != CardSnippet { t.Errorf("card_type: got %v", got.CardType) } } func TestGet_NotFound(t *testing.T) { s := testStore(t) _, err := s.Get("01NONEXISTENT0000000000000") if err != ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } } func TestList_DefaultParams(t *testing.T) { s := testStore(t) for i := 0; i < 3; i++ { s.Create(&Entity{Body: "note", Glyph: GlyphNote}) } entities, err := s.List(DefaultListParams()) if err != nil { t.Fatal(err) } if len(entities) != 3 { t.Fatalf("expected 3, got %d", len(entities)) } // desc order: newest first if entities[0].CreatedAt.Before(entities[2].CreatedAt) { t.Error("expected newest first") } } func TestList_FilterByTag(t *testing.T) { s := testStore(t) s.Create(&Entity{Body: "a", Glyph: GlyphNote, Tags: []string{"ops"}}) s.Create(&Entity{Body: "b", Glyph: GlyphNote, Tags: []string{"home"}}) s.Create(&Entity{Body: "c", Glyph: GlyphNote, Tags: []string{"ops", "home"}}) p := DefaultListParams() tag := "ops" p.Tag = &tag entities, err := s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 2 { t.Errorf("expected 2 entities with tag ops, got %d", len(entities)) } } func TestList_FilterByDate(t *testing.T) { s := testStore(t) s.Create(&Entity{Body: "today", Glyph: GlyphNote}) p := DefaultListParams() date := time.Now().UTC().Format("2006-01-02") p.Date = &date entities, err := s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 1 { t.Errorf("expected 1, got %d", len(entities)) } otherDate := "2020-01-01" p.Date = &otherDate entities, err = s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 0 { t.Errorf("expected 0 for past date, got %d", len(entities)) } } func TestList_CardsOnly(t *testing.T) { s := testStore(t) s.Create(&Entity{Body: "fluid", Glyph: GlyphNote}) ct := CardSnippet s.Create(&Entity{Body: "card", Glyph: GlyphNote, CardType: &ct}) p := DefaultListParams() p.CardsOnly = true entities, err := s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 1 { t.Fatalf("expected 1 card, got %d", len(entities)) } if entities[0].Body != "card" { t.Errorf("expected card entity, got %q", entities[0].Body) } } func TestList_IncludeDeleted(t *testing.T) { s := testStore(t) e := &Entity{Body: "doomed", Glyph: GlyphNote} s.Create(e) s.SoftDelete(e.ID) p := DefaultListParams() entities, err := s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 0 { t.Error("deleted entity should be excluded by default") } p.IncludeDeleted = true entities, err = s.List(p) if err != nil { t.Fatal(err) } if len(entities) != 1 { t.Error("deleted entity should appear with include_deleted") } } func TestList_SortByUseCount(t *testing.T) { s := testStore(t) ct := CardSnippet e1 := &Entity{Body: "low", Glyph: GlyphNote, CardType: &ct} e2 := &Entity{Body: "high", Glyph: GlyphNote, CardType: &ct} s.Create(e1) s.Create(e2) s.IncrementUse(e2.ID) s.IncrementUse(e2.ID) p := DefaultListParams() p.Sort = "use_count" entities, err := s.List(p) if err != nil { t.Fatal(err) } if entities[0].Body != "high" { t.Errorf("expected highest use first, got %q", entities[0].Body) } } func TestList_Pagination(t *testing.T) { s := testStore(t) for i := 0; i < 10; i++ { s.Create(&Entity{Body: "note", Glyph: GlyphNote}) } p := DefaultListParams() p.Limit = 3 p.Offset = 0 page1, err := s.List(p) if err != nil { t.Fatal(err) } if len(page1) != 3 { t.Fatalf("expected 3, got %d", len(page1)) } p.Offset = 3 page2, err := s.List(p) if err != nil { t.Fatal(err) } if len(page2) != 3 { t.Fatalf("expected 3, got %d", len(page2)) } if page1[0].ID == page2[0].ID { t.Error("pages should not overlap") } } func TestUpdate_Body(t *testing.T) { s := testStore(t) e := &Entity{Body: "old", Glyph: GlyphNote} s.Create(e) time.Sleep(1100 * time.Millisecond) newBody := "new" if err := s.Update(e.ID, &EntityUpdate{Body: &newBody}); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.Body != "new" { t.Errorf("body not updated: %q", got.Body) } if !got.ModifiedAt.After(e.ModifiedAt) { t.Error("modified_at not updated") } } func TestUpdate_Tags(t *testing.T) { s := testStore(t) e := &Entity{Body: "test", Glyph: GlyphNote, Tags: []string{"old"}} s.Create(e) newTags := []string{"new1", "new2"} if err := s.Update(e.ID, &EntityUpdate{Tags: &newTags}); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if len(got.Tags) != 2 { t.Fatalf("expected 2 tags, got %d: %v", len(got.Tags), got.Tags) } } func TestPromote_Success(t *testing.T) { s := testStore(t) e := &Entity{Body: "trick", Glyph: GlyphNote} s.Create(e) if err := s.Promote(e.ID, CardSnippet, nil); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.CardType == nil || *got.CardType != CardSnippet { t.Errorf("expected snippet, got %v", got.CardType) } } func TestPromote_AlreadyPromoted(t *testing.T) { s := testStore(t) ct := CardSnippet e := &Entity{Body: "trick", Glyph: GlyphNote, CardType: &ct} s.Create(e) if err := s.Promote(e.ID, CardTemplate, nil); err != ErrAlreadyPromoted { t.Errorf("expected ErrAlreadyPromoted, got %v", err) } } func TestDemote_Success(t *testing.T) { s := testStore(t) e := &Entity{Body: "trick", Glyph: GlyphNote} s.Create(e) s.Promote(e.ID, CardSnippet, nil) if err := s.Demote(e.ID); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.CardType != nil { t.Errorf("expected nil card_type, got %v", got.CardType) } if got.UseCount != 0 { t.Errorf("expected use_count reset to 0, got %d", got.UseCount) } } func TestDemote_AlreadyFluid(t *testing.T) { s := testStore(t) e := &Entity{Body: "trick", Glyph: GlyphNote} s.Create(e) if err := s.Demote(e.ID); err != ErrAlreadyFluid { t.Errorf("expected ErrAlreadyFluid, got %v", err) } } func TestSoftDelete_First(t *testing.T) { s := testStore(t) e := &Entity{Body: "doomed", Glyph: GlyphNote} s.Create(e) result, err := s.SoftDelete(e.ID) if err != nil { t.Fatal(err) } if result != DeletedSoft { t.Errorf("expected DeletedSoft, got %d", result) } got, _ := s.Get(e.ID) if got.DeletedAt == nil { t.Error("expected deleted_at to be set") } } func TestSoftDelete_Second(t *testing.T) { s := testStore(t) e := &Entity{Body: "doomed", Glyph: GlyphNote} s.Create(e) s.SoftDelete(e.ID) result, err := s.SoftDelete(e.ID) if err != nil { t.Fatal(err) } if result != DeletedHard { t.Errorf("expected DeletedHard, got %d", result) } _, err = s.Get(e.ID) if err != ErrNotFound { t.Errorf("expected ErrNotFound after hard delete, got %v", err) } } func TestSoftDelete_NotFound(t *testing.T) { s := testStore(t) _, err := s.SoftDelete("01NONEXISTENT0000000000000") if err != ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } } func TestIncrementUse(t *testing.T) { s := testStore(t) ct := CardSnippet e := &Entity{Body: "trick", Glyph: GlyphNote, CardType: &ct} s.Create(e) if err := s.IncrementUse(e.ID); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.UseCount != 1 { t.Errorf("expected use_count=1, got %d", got.UseCount) } if got.LastUsedAt == nil { t.Error("expected last_used_at to be set") } } func TestResolve_FullID(t *testing.T) { s := testStore(t) e := &Entity{Body: "test", Glyph: GlyphNote} s.Create(e) got, err := s.Resolve(e.ID) if err != nil { t.Fatal(err) } if got != e.ID { t.Errorf("expected %s, got %s", e.ID, got) } } func TestResolve_Prefix(t *testing.T) { s := testStore(t) e := &Entity{Body: "test", Glyph: GlyphNote} s.Create(e) got, err := s.Resolve(e.ID[:6]) if err != nil { t.Fatal(err) } if got != e.ID { t.Errorf("expected %s, got %s", e.ID, got) } } func TestResolve_NotFound(t *testing.T) { s := testStore(t) _, err := s.Resolve("ZZZZZZZZZ") if err != ErrNotFound { t.Errorf("expected ErrNotFound, got %v", err) } } func TestAbsorb_SourceIsCard(t *testing.T) { s := testStore(t) target := &Entity{Body: "target", Glyph: GlyphNote, Tags: []string{"a"}} s.Create(target) source := &Entity{Body: "source", Glyph: GlyphNote} s.Create(source) s.Promote(source.ID, CardSnippet, nil) s.IncrementUse(source.ID) if err := s.Absorb(target.ID, source.ID); err != nil { t.Fatal(err) } got, _ := s.Get(target.ID) if got.Body != "target\nsource" { t.Errorf("merged body: %q", got.Body) } src, _ := s.Get(source.ID) if src.CardType != nil { t.Error("source card_type should be cleared after absorb") } if src.UseCount != 0 { t.Errorf("source use_count should be reset, got %d", src.UseCount) } if src.DeletedAt == nil { t.Error("source should be soft-deleted") } } func TestCreate_WithTitleAndDescription(t *testing.T) { s := testStore(t) e := &Entity{ Body: "body text", Title: ptr("nginx trick"), Description: ptr("always forget this"), Glyph: GlyphNote, Tags: []string{"ops"}, } if err := s.Create(e); err != nil { t.Fatal(err) } got, err := s.Get(e.ID) if err != nil { t.Fatal(err) } if got.Title == nil || *got.Title != "nginx trick" { t.Errorf("title: got %v", got.Title) } if got.Description == nil || *got.Description != "always forget this" { t.Errorf("description: got %v", got.Description) } if got.Body != "body text" { t.Errorf("body: got %q", got.Body) } } func TestCreate_WithoutTitle(t *testing.T) { s := testStore(t) e := &Entity{Body: "just body", Glyph: GlyphNote} if err := s.Create(e); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.Title != nil { t.Errorf("expected nil title, got %v", got.Title) } if got.Description != nil { t.Errorf("expected nil description, got %v", got.Description) } } func TestUpdate_Title(t *testing.T) { s := testStore(t) e := &Entity{Body: "body", Glyph: GlyphNote} s.Create(e) newTitle := "new title" if err := s.Update(e.ID, &EntityUpdate{Title: &newTitle}); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.Title == nil || *got.Title != "new title" { t.Errorf("title: got %v", got.Title) } } func TestUpdate_Description(t *testing.T) { s := testStore(t) e := &Entity{Body: "body", Glyph: GlyphNote} s.Create(e) newDesc := "new desc" if err := s.Update(e.ID, &EntityUpdate{Description: &newDesc}); err != nil { t.Fatal(err) } got, _ := s.Get(e.ID) if got.Description == nil || *got.Description != "new desc" { t.Errorf("description: got %v", got.Description) } } func TestAbsorb_PreservesTargetTitle(t *testing.T) { s := testStore(t) target := &Entity{Body: "target body", Title: ptr("target title"), Glyph: GlyphNote} source := &Entity{Body: "source body", Title: ptr("source title"), Glyph: GlyphNote} s.Create(target) s.Create(source) if err := s.Absorb(target.ID, source.ID); err != nil { t.Fatal(err) } got, _ := s.Get(target.ID) if got.Title == nil || *got.Title != "target title" { t.Errorf("target title should be preserved, got %v", got.Title) } if got.Body != "target body\nsource body" { t.Errorf("body: got %q", got.Body) } }