Release / Build and publish image (push) Failing after 10m3s
Første etape af noter. Tabel, CRUD, fuldtekstsøgning og de links, en note bærer. Markdown i en kolonne, ikke rich text og ikke et format af vores eget. Eksport var et krav, før der var skrevet en linje, og er den gemte form den eksporterede form, er der ingen konvertering tilbage at tabe noget i — noten kan læses af enhver anden editor den dag programmet ikke findes. Et #tag i en note er DET SAMME tag som i en opgave. Ikke en anden slags, der ligner. Det er hele svaret på "tag en note og se den i projektet": referencen er den samme, så der ikke opstår to systemer, brugeren selv skal holde i sync. Links gemmes som rækker, udledt af teksten ved hver gemning. Teksten er stadig sandheden; tabellen er et indeks over den. Uden det ville "hvad peger på denne opgave" betyde en gennemsøgning af al prosa, og den rude ville være for langsom til at lade stå åben. De rives ned og bygges op frem for at blive sammenlignet: en sammenligning er en anden beskrivelse af samme kendsgerning, og den kan komme til at være uenig med teksten. Noter ligger under projekter og arver deling, roller, papirkurv og historik. En mappestruktur ved siden af ville være ét hierarki for meget. MatchExpr fik navngivne kolonner. Den var skrevet til opgavernes og gav "no such column" mod noternes — hvilket i det mindste er højlydt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
248 lines
6.5 KiB
Go
248 lines
6.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The point of the whole thing: a #tag in a note is the same tag as in a task.
|
|
// Not a second kind that looks alike and has to be kept in step by hand.
|
|
func TestATagInANoteIsTheSameTagAsInATask(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
n := &Note{
|
|
CreatedBy: userID,
|
|
Body: "Mødet med #Firma gik fint. Følg op sammen med #Regnskab.",
|
|
}
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var projects []string
|
|
for _, l := range n.Links {
|
|
if l.Kind == "project" {
|
|
projects = append(projects, l.TargetID)
|
|
}
|
|
}
|
|
if len(projects) != 2 {
|
|
t.Fatalf("read %v out of the note, want the two projects", n.Links)
|
|
}
|
|
if projects[0] != "Firma" || projects[1] != "Regnskab" {
|
|
t.Errorf("got %v", projects)
|
|
}
|
|
}
|
|
|
|
// Backwards is the direction that makes it a second brain rather than a folder.
|
|
func TestANoteIsFoundFromWhatItPointsAt(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
const taskID = "01a01694-a25c-7000-a50f-d72ab33f9b6a"
|
|
notes := []*Note{
|
|
{CreatedBy: userID, Body: "Aftalt på mødet, se /opgave/" + taskID},
|
|
{CreatedBy: userID, Body: "Intet at gøre med den sag."},
|
|
}
|
|
for _, n := range notes {
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
found, err := db.NotesLinking(ctx, "task", taskID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(found) != 1 {
|
|
t.Fatalf("%d notes point at the task, want 1", len(found))
|
|
}
|
|
if !strings.HasPrefix(found[0].Title, "Aftalt") {
|
|
t.Errorf("the wrong note came back: %q", found[0].Title)
|
|
}
|
|
}
|
|
|
|
// The links are an index over the text, so editing the text must move them.
|
|
// A stale link is worse than none: it says a connection exists that does not.
|
|
func TestEditingANoteRewritesWhatItPointsAt(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
n := &Note{CreatedBy: userID, Body: "Handler om #Firma"}
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if before, _ := db.NotesLinking(ctx, "project", "Firma"); len(before) != 1 {
|
|
t.Fatal("the first save did not record the link")
|
|
}
|
|
|
|
n.Body = "Handler om #Regnskab i stedet"
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stale, err := db.NotesLinking(ctx, "project", "Firma")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(stale) != 0 {
|
|
t.Errorf("the note still points at Firma after the mention was removed")
|
|
}
|
|
if fresh, _ := db.NotesLinking(ctx, "project", "Regnskab"); len(fresh) != 1 {
|
|
t.Errorf("the new mention was not recorded")
|
|
}
|
|
}
|
|
|
|
// A list of untitled notes is a list nobody can read.
|
|
func TestANoteTakesItsTitleFromItsFirstLine(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
for _, c := range []struct{ body, want string }{
|
|
{"# Møde med Anders\n\nHan ville gerne...", "Møde med Anders"},
|
|
{"\n\n Løse tanker \nmere", "Løse tanker"},
|
|
{"", ""},
|
|
} {
|
|
n := &Note{CreatedBy: userID, Body: c.body}
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n.Title != c.want {
|
|
t.Errorf("body %q gave title %q, want %q", c.body, n.Title, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Danish spelling, both ways round — the same generosity tasks already get.
|
|
func TestNotesAreFoundInEitherSpelling(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
if err := db.SaveNote(ctx, &Note{
|
|
CreatedBy: userID,
|
|
Title: "Grønt regnskab",
|
|
Body: "Målt på årsbasis for Århus.",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, q := range []string{"grønt", "gront", "århus", "aarhus", "regnskab"} {
|
|
found, err := db.SearchNotes(ctx, q, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(found) != 1 {
|
|
t.Errorf("%q found %d notes, want 1", q, len(found))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deleting is the trash, the way it is everywhere else in this program. A note is
|
|
// the one place somebody can lose an hour of writing to one keystroke.
|
|
func TestADeletedNoteCanComeBack(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
n := &Note{CreatedBy: userID, Body: "Vigtigt"}
|
|
if err := db.SaveNote(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.DeleteNote(ctx, n.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gone, _ := db.Note(ctx, n.ID); gone != nil {
|
|
t.Error("a deleted note is still returned")
|
|
}
|
|
if err := db.RestoreNote(ctx, n.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := db.Note(ctx, n.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back == nil || back.Body != "Vigtigt" {
|
|
t.Error("the note did not come back whole")
|
|
}
|
|
}
|
|
|
|
func TestWhatIsReadOutOfABody(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name, body string
|
|
want []NoteLink
|
|
}{
|
|
{"a hash inside a word is not a tag", "C#Sharp og x#y", nil},
|
|
{"a note by title", "se [[Møde med Anders]]", []NoteLink{{"note", "Møde med Anders"}}},
|
|
{"the same tag twice is one link", "#Firma og #Firma igen",
|
|
[]NoteLink{{"project", "Firma"}}},
|
|
{"a heading is not a tag", "# Overskrift", nil},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := LinksIn(c.body)
|
|
if len(got) != len(c.want) {
|
|
t.Fatalf("got %v, want %v", got, c.want)
|
|
}
|
|
for i := range got {
|
|
if got[i] != c.want[i] {
|
|
t.Errorf("got %v, want %v", got[i], c.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The number beside a project is what is left to do: not finished, not deleted.
|
|
// It replaced a count of people, which said 2 on an empty project.
|
|
func TestTheProjectCountIsWhatIsLeft(t *testing.T) {
|
|
db, userID := sealedStore(t)
|
|
ctx := context.Background()
|
|
|
|
p := &Project{Name: "Tallene", OwnerID: userID}
|
|
if err := db.CreateProject(ctx, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
count := func() int {
|
|
list, err := db.ListProjects(ctx, userID, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, x := range list {
|
|
if x.ID == p.ID {
|
|
return x.OpenCount
|
|
}
|
|
}
|
|
t.Fatal("the project is not in the list")
|
|
return -1
|
|
}
|
|
|
|
if n := count(); n != 0 {
|
|
t.Errorf("an empty project counts %d", n)
|
|
}
|
|
|
|
var ids []string
|
|
for _, c := range []string{"en", "to", "tre"} {
|
|
task := &Task{ProjectID: p.ID, Content: c, CreatedBy: userID}
|
|
if err := db.CreateTask(ctx, task, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids = append(ids, task.ID)
|
|
}
|
|
if n := count(); n != 3 {
|
|
t.Errorf("three tasks count %d", n)
|
|
}
|
|
|
|
if _, err := db.CompleteTask(ctx, ids[0], userID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := count(); n != 2 {
|
|
t.Errorf("after finishing one, the count is %d, want 2", n)
|
|
}
|
|
|
|
if err := db.DeleteTask(ctx, ids[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := count(); n != 1 {
|
|
t.Errorf("after deleting one, the count is %d, want 1", n)
|
|
}
|
|
}
|