Diskussionsketten: jüngste Antwort im Teilbaum an Feed- und Antwortkarten

Jede Karte in Feed und Antwortliste zeigt die Antwort, die ihre Diskussion
zuletzt fortgesetzt hat: [Beitrag] -- "k weitere Beiträge" -- [Antwort].
Gelöschte Beiträge werden als jüngste übersprungen, zählen auf dem Pfad mit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
irrlicht
2026-09-27 00:52:17 +02:00
co-authored by Claude Opus 5.5
parent 8e79e94bbb
commit ade853f560
7 changed files with 196 additions and 5 deletions
+62
View File
@@ -314,6 +314,10 @@ type threadEntry struct {
Deleted int64 `json:"deleted"`
Content string `json:"content"`
Username string `json:"username"`
Latest *struct {
Entry threadEntry `json:"entry"`
Skipped int64 `json:"skipped"`
} `json:"latest"`
}
// threadJSON ist die Antwortform von /entry/{pid}/thread. Nicht zu verwechseln
@@ -438,6 +442,64 @@ func TestEditEntry(t *testing.T) {
}
}
// latestOf beschreibt Latest eines Beitrags knapp für Vergleiche und
// Fehlermeldungen: "pid/skipped" oder "-".
func latestOf(e threadEntry) string {
if e.Latest == nil {
return "-"
}
return fmt.Sprintf("%d/%d", e.Latest.Entry.PID, e.Latest.Skipped)
}
func TestLatestDescendant(t *testing.T) {
srv := newTestServer(t)
alice := registerAndLogin(t, srv, "alice")
// root ── a ── b ── c
// └─ d (d vor b und c erstellt)
root := createEntry(t, alice, srv, "wurzel")
a := createReply(t, alice, srv, "a", root)
d := createReply(t, alice, srv, "d", root)
b := createReply(t, alice, srv, "b", a)
c := createReply(t, alice, srv, "c", b)
feed := func() []threadEntry {
resp, err := http.Get(srv.URL + "/api/entry/feed/0")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var f []threadEntry
json.NewDecoder(resp.Body).Decode(&f)
return f
}
// Feed: Root mit c, dazwischen a und b.
if f := feed(); len(f) != 1 || latestOf(f[0]) != fmt.Sprintf("%d/2", c) {
t.Fatalf("feed latest: erwartet %d/2, bekam %s", c, latestOf(f[0]))
}
// Antworten auf Root: a mit c (b dazwischen), d ohne Nachfahren.
want := map[int64]string{a: fmt.Sprintf("%d/1", c), d: "-"}
for _, r := range getThread(t, srv, root).Replies {
if got := latestOf(r); got != want[r.PID] {
t.Fatalf("reply %d latest: erwartet %s, bekam %s", r.PID, want[r.PID], got)
}
}
// Direkte Antwort: skipped 0.
if r := getThread(t, srv, a).Replies; len(r) != 1 || latestOf(r[0]) != fmt.Sprintf("%d/0", c) {
t.Fatalf("b latest: erwartet %d/0, bekam %+v", c, r)
}
// Gelöschter jüngster Beitrag wird übersprungen -> nächstjüngerer (b).
resp := postForm(t, alice, fmt.Sprintf("%s/api/entry/%d/delete", srv.URL, c), url.Values{})
resp.Body.Close()
if f := feed(); latestOf(f[0]) != fmt.Sprintf("%d/1", b) {
t.Fatalf("feed latest nach delete: erwartet %d/1, bekam %s", b, latestOf(f[0]))
}
}
func TestSoftDelete(t *testing.T) {
srv := newTestServer(t)
alice := registerAndLogin(t, srv, "alice")