package suggestions import ( "encoding/json" "net/http" "strconv" "testing" "gitea.parodia.dev/drwily/petal/internal/db" ) // resolved seeds one already-settled suggestion, dated `daysAgo` at the moment // she decided it (the journal reads decisions, not proposals). func resolved(t *testing.T, h *Handler, docID, status, original, replacement string, daysAgo int) { t.Helper() _, err := h.DB.Exec( `INSERT INTO suggestions (doc_id, from_pos, to_pos, original, replacement, explanation, type, status, created_at, resolved_at) VALUES (?, 0, 0, ?, ?, '', 'collocation', ?, datetime('now', ?), datetime('now', ?))`, docID, original, replacement, status, "-"+strconv.Itoa(daysAgo)+" days", "-"+strconv.Itoa(daysAgo)+" days", ) if err != nil { t.Fatalf("seed resolved suggestion: %v", err) } } func seedDoc(t *testing.T, h *Handler, userID, text string) string { t.Helper() var id string if err := h.DB.QueryRow( `INSERT INTO documents (user_id, content_text) VALUES (?, ?) RETURNING id`, userID, text, ).Scan(&id); err != nil { t.Fatalf("seed doc: %v", err) } return id } func readJournal(t *testing.T, srv http.Handler) Journal { t.Helper() rec := do(t, srv, http.MethodGet, "/suggestions/growth", "") if rec.Code != http.StatusOK { t.Fatalf("growth: got %d, want 200 (body %s)", rec.Code, rec.Body.String()) } var j Journal if err := json.Unmarshal(rec.Body.Bytes(), &j); err != nil { t.Fatalf("decode journal: %v", err) } return j } // TestJournalIsEmptyForANewWriter: nothing to report reports nothing. Empty // lists, not nulls, so the frontend never has to guess. func TestJournalIsEmptyForANewWriter(t *testing.T) { srv, _, _ := newTestServer(t, &stubClient{}) j := readJournal(t, srv) if j.Kept != 0 || j.KeptBefore != 0 || len(j.Stuck) != 0 || len(j.Faded) != 0 { t.Fatalf("new writer got a journal: %+v", j) } } // TestKeptComparesHerToHerOwnPastSelf. func TestKeptCountsTwoWindows(t *testing.T) { srv, docID, h := newTestServer(t, &stubClient{}) for i := 0; i < 3; i++ { resolved(t, h, docID, "accepted", "do a decision", "make a decision", 5) } resolved(t, h, docID, "accepted", "big rain", "heavy rain", 40) resolved(t, h, docID, "rejected", "no thanks", "no, thank you", 5) // decisions kept only resolved(t, h, docID, "accepted", "long ago", "long since", 200) // outside both windows j := readJournal(t, srv) if j.Kept != 3 { t.Errorf("Kept = %d, want 3", j.Kept) } if j.KeptBefore != 1 { t.Errorf("KeptBefore = %d, want 1", j.KeptBefore) } } // TestStuckNeedsASecondDocument: a phrase sitting in the one document it was // applied to has not stuck — it's just the edit, where she left it. A second // document is her reaching for it herself, which is the claim the line makes. func TestStuckNeedsASecondDocument(t *testing.T) { srv, docID, h := newTestServer(t, &stubClient{}) if _, err := h.DB.Exec(`UPDATE documents SET content_text = ? WHERE id = ?`, "I had to make a decision.", docID); err != nil { t.Fatalf("set content: %v", err) } resolved(t, h, docID, "accepted", "do a decision", "make a decision", 10) resolved(t, h, docID, "accepted", "do a photo", "take a photo", 10) if j := readJournal(t, srv); len(j.Stuck) != 0 { t.Fatalf("one document counted as sticking: %+v", j.Stuck) } // She uses it again, elsewhere, on her own. seedDoc(t, h, db.LocalUserID, "Later I had to Make A Decision about the flat.") j := readJournal(t, srv) if len(j.Stuck) != 1 { t.Fatalf("Stuck = %+v, want just the phrase she reused", j.Stuck) } if j.Stuck[0].Phrase != "make a decision" || j.Stuck[0].Docs != 2 { t.Errorf("Stuck[0] = %+v, want {make a decision 2} (case-insensitive)", j.Stuck[0]) } } // TestFadedNeedsRecentWriting is the guard that keeps this feature honest: a // month away from Petal must never be reported back as progress. func TestFadedNeedsRecentWriting(t *testing.T) { srv, docID, h := newTestServer(t, &stubClient{}) resolved(t, h, docID, "accepted", "在 the morning", "in the morning", 60) resolved(t, h, docID, "accepted", "在 the morning", "in the morning", 55) if j := readJournal(t, srv); len(j.Faded) != 0 { t.Fatalf("silence reported as growth: %+v", j.Faded) } // She has been writing again this month — now the absence means something. resolved(t, h, docID, "accepted", "big rain", "heavy rain", 3) j := readJournal(t, srv) if len(j.Faded) != 1 || j.Faded[0].Pattern != "在 the morning" || j.Faded[0].Times != 2 { t.Fatalf("Faded = %+v, want the pattern she stopped needing (twice, back then)", j.Faded) } } // TestFadedExcludesWhatStillHappens: a pattern corrected again this month has // not faded, however often it came up before. func TestFadedExcludesWhatStillHappens(t *testing.T) { srv, docID, h := newTestServer(t, &stubClient{}) resolved(t, h, docID, "accepted", "在 the morning", "in the morning", 60) resolved(t, h, docID, "accepted", "在 the morning", "in the morning", 55) resolved(t, h, docID, "accepted", "在 the morning", "in the morning", 2) if j := readJournal(t, srv); len(j.Faded) != 0 { t.Fatalf("Faded = %+v, want empty — it still happens", j.Faded) } } // TestJournalIsPerWriter: another account's learning is never anyone else's // journal, and the only comparison Petal draws is with her own past self. func TestJournalIsPerWriter(t *testing.T) { srv, _, h := newTestServer(t, &stubClient{}) if _, err := h.DB.Exec(`INSERT INTO users (id, email) VALUES ('bob', 'bob@example.com')`); err != nil { t.Fatalf("seed user: %v", err) } bobDoc := seedDoc(t, h, "bob", "Bob had to make a decision.") seedDoc(t, h, "bob", "Bob will make a decision again.") resolved(t, h, bobDoc, "accepted", "do a decision", "make a decision", 5) resolved(t, h, bobDoc, "accepted", "big rain", "heavy rain", 60) resolved(t, h, bobDoc, "accepted", "big rain", "heavy rain", 55) j := readJournal(t, srv) if j.Kept != 0 || j.KeptBefore != 0 || len(j.Stuck) != 0 || len(j.Faded) != 0 { t.Fatalf("bob's learning leaked into the local user's journal: %+v", j) } }