2684eb1d24
CI / test (pull_request) Successful in 2m18s
Press [ in detail view to open link picker showing all [[links]] in the current entry. Enter follows a link, resolving by title then body substring. Navigation history stack enables esc to pop back through followed links before returning to list. Adds Store.ResolveLink() for non-transactional link resolution from the TUI layer.
273 lines
6.7 KiB
Go
273 lines
6.7 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestSyncLinks_OnCreate(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
target := &Entity{Body: "nginx proxy config", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
source := &Entity{Body: "see [[nginx proxy config]] for setup", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err := s.LoadBacklinks(ctx, target.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 1 {
|
|
t.Fatalf("expected 1 backlink, got %d", len(backlinks))
|
|
}
|
|
if backlinks[0].EntityID != source.ID {
|
|
t.Errorf("backlink entity = %s, want %s", backlinks[0].EntityID, source.ID)
|
|
}
|
|
if backlinks[0].LinkText != "nginx proxy config" {
|
|
t.Errorf("link text = %q, want %q", backlinks[0].LinkText, "nginx proxy config")
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_TitleMatch(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
title := "deploy checklist"
|
|
target := &Entity{Body: "steps to deploy", Title: &title, Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
source := &Entity{Body: "follow [[deploy checklist]]", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err := s.LoadBacklinks(ctx, target.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 1 {
|
|
t.Fatalf("expected 1 backlink, got %d", len(backlinks))
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_TitlePriority(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
title := "nginx config"
|
|
titled := &Entity{Body: "some body", Title: &title, Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, titled); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bodyMatch := &Entity{Body: "nginx config details", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, bodyMatch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
source := &Entity{Body: "see [[nginx config]]", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err := s.LoadBacklinks(ctx, titled.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 1 {
|
|
t.Fatalf("title match should win, got %d backlinks on titled entity", len(backlinks))
|
|
}
|
|
|
|
bodyBacklinks, err := s.LoadBacklinks(ctx, bodyMatch.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(bodyBacklinks) != 0 {
|
|
t.Fatalf("body match entity should have 0 backlinks, got %d", len(bodyBacklinks))
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_Unresolved(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
source := &Entity{Body: "see [[nonexistent entry]]", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var count int
|
|
err := s.db.QueryRow("SELECT COUNT(*) FROM entity_links WHERE from_id = ?", source.ID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("expected 1 link row (unresolved), got %d", count)
|
|
}
|
|
|
|
var toID *string
|
|
err = s.db.QueryRow("SELECT to_id FROM entity_links WHERE from_id = ?", source.ID).Scan(&toID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if toID != nil {
|
|
t.Errorf("expected NULL to_id for unresolved link, got %v", *toID)
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_OnUpdate(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
target := &Entity{Body: "original target", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
source := &Entity{Body: "no links yet", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newBody := "now has [[original target]]"
|
|
if err := s.Update(ctx, source.ID, &EntityUpdate{Body: &newBody}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err := s.LoadBacklinks(ctx, target.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 1 {
|
|
t.Fatalf("expected 1 backlink after update, got %d", len(backlinks))
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_SelfLinkUnresolved(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
e := &Entity{Body: "I reference [[I reference]]", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var toID *string
|
|
err := s.db.QueryRow("SELECT to_id FROM entity_links WHERE from_id = ?", e.ID).Scan(&toID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if toID != nil {
|
|
t.Fatalf("self-matching link should be unresolved (NULL to_id), got %v", *toID)
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_NoLinks(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
e := &Entity{Body: "plain text no links", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var count int
|
|
err := s.db.QueryRow("SELECT COUNT(*) FROM entity_links WHERE from_id = ?", e.ID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("expected 0 links, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSyncLinks_DeletedSourceHidden(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
target := &Entity{Body: "target entry", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
source := &Entity{Body: "see [[target entry]]", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err := s.LoadBacklinks(ctx, target.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 1 {
|
|
t.Fatalf("expected 1 backlink before delete, got %d", len(backlinks))
|
|
}
|
|
|
|
if _, err := s.SoftDelete(ctx, source.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backlinks, err = s.LoadBacklinks(ctx, target.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(backlinks) != 0 {
|
|
t.Fatalf("soft-deleted source should not appear in backlinks, got %d", len(backlinks))
|
|
}
|
|
}
|
|
|
|
func TestResolveLink_TitleMatch(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
title := "nginx config"
|
|
target := &Entity{Body: "proxy_pass details", Title: &title, Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resolved, err := s.ResolveLink(ctx, "nginx config")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resolved.ID != target.ID {
|
|
t.Errorf("resolved ID = %s, want %s", resolved.ID, target.ID)
|
|
}
|
|
}
|
|
|
|
func TestResolveLink_BodyFallback(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
target := &Entity{Body: "deploy staging checklist", Glyph: "note", Tags: []string{}}
|
|
if err := s.Create(ctx, target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resolved, err := s.ResolveLink(ctx, "deploy staging")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resolved.ID != target.ID {
|
|
t.Errorf("resolved ID = %s, want %s", resolved.ID, target.ID)
|
|
}
|
|
}
|
|
|
|
func TestResolveLink_NotFound(t *testing.T) {
|
|
s := testStore(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := s.ResolveLink(ctx, "nonexistent entry")
|
|
if err != ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|