Closes #136. Spec #136 end to end: `finished` becomes a fact about the Series, written only by the owner, and the reader-facing Lifecycle bucket is gone. ## What landed - **#157** — `series.finished_at bigint NOT NULL DEFAULT 0` plus the migration whose statement order is load-bearing (seed from the buckets, then flip them); both Lane queries lose the `HAVING COUNT(*) FILTER (WHERE b.status <> 'finished')` clause and gate on `finished_at = 0` instead, with the due-query/eligible-count force asymmetry kept deliberate and commented; `StatusFinished`, its API special-case 400, the web tab and the templates' Finished bucket deleted. - **#158** — owner Finish control on the Series detail page: confirm-gated finish, instant un-finish, admin accent (never ember, nothing is destroyed), `Store.SetSeriesFinished`, the two routes behind the owner gate, and the state displayed on the list row without offering the control there. - **#160** — reader side: derived `finished` bool on the flat Bookmark (`s.finished_at > 0`), rendered as a text-only label in both userscripts and on the web card; read-only inbound by omission from `Upsert`'s explicit `series` column list, same mechanism that already protects `cover`. - **#161** — glossary and the stale Reader-count divergence note catch up. - **#159** — `finished` joins the admin filter vocabulary (predicate `finished_at > 0`, label `Finished`, own aggregate count, figure last in the stats block as informational); the four clock-driven hygiene predicates (stale, never-checked, no-cover, no-chapter) exclude finished Series while unpollable, orphan and sighting-raised deliberately do not. ## Verification `go vet ./...` and `go test ./...` green on the merged branch (Docker-backed, throwaway `postgres:17-alpine` per package). Each ticket also passed a two-axis review (spec + standards) on its own branch before merge. Reviewed-on: #163 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #163.
This commit is contained in:
+48
-1
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -298,7 +299,7 @@ func TestFlatWireFieldSet(t *testing.T) {
|
||||
"series_url": true, "cover": true, "last_chapter": true,
|
||||
"last_chapter_num": true, "last_chapter_url": true, "favorite": true,
|
||||
"latest_chapter": true, "latest_chapter_num": true, "updated_at": true,
|
||||
"status": true, "kind": true,
|
||||
"status": true, "kind": true, "finished": true,
|
||||
}
|
||||
checkFlat := func(t *testing.T, payload []byte) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
@@ -362,6 +363,52 @@ func TestFlatWireFieldSet(t *testing.T) {
|
||||
checkFlat(t, body2)
|
||||
}
|
||||
|
||||
// finished is derived on the wire and read-only: a client PUT echoing a cached
|
||||
// value, forward progress or not, must not change the Series' retired state,
|
||||
// so GET still reports the truth after the echo (issues #157, #160).
|
||||
func TestFinishedWireRoundTrip(t *testing.T) {
|
||||
s, url := newTestStoreURL(t)
|
||||
srv := newRouter(s, testConfig())
|
||||
key := "asura:done"
|
||||
|
||||
putBookmark(t, srv, key, store.Bookmark{
|
||||
Title: "Solo Leveling", SeriesURL: "https://asurascans.com/comics/done",
|
||||
LastChapterNum: 10,
|
||||
})
|
||||
|
||||
// The store writer for the flag is the admin surface's own and lands in
|
||||
// the same wave (#158), so seed the fact with SQL, like store_test.go.
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(
|
||||
`UPDATE series SET finished_at = 1000 WHERE site = 'asura' AND series_id = 'done'`); err != nil {
|
||||
t.Fatalf("seed finished: %v", err)
|
||||
}
|
||||
|
||||
got := getBookmarks(t, srv)
|
||||
if len(got) != 1 || !got[0].Finished {
|
||||
t.Fatalf("GET = %+v, want one bookmark carrying finished: true", got)
|
||||
}
|
||||
|
||||
// A stale cache echoing the flag cannot un-finish (or finish) the Series.
|
||||
for _, sent := range []bool{false, true} {
|
||||
echoed := putBookmark(t, srv, key, store.Bookmark{
|
||||
Title: "Solo Leveling", SeriesURL: "https://asurascans.com/comics/done",
|
||||
LastChapterNum: 11, Finished: sent,
|
||||
})
|
||||
if !echoed.Finished {
|
||||
t.Fatalf("PUT echoing Finished: %v reported finished = false, want true", sent)
|
||||
}
|
||||
got = getBookmarks(t, srv)
|
||||
if !got[0].Finished {
|
||||
t.Fatalf("GET after echoing Finished: %v = false, want the Series state preserved", sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A PUT naming an existing series must ignore client-supplied title, cover and
|
||||
// URL — the security boundary from ADR-0003, where a hostile site's scraped
|
||||
// values could otherwise land on a shared row — while progress still lands.
|
||||
|
||||
Reference in New Issue
Block a user