Merge #167: A failure names itself on the page
This commit is contained in:
@@ -56,6 +56,10 @@ type laneRow struct {
|
|||||||
// offer Resume from the moment the owner presses Pause, with no pass
|
// offer Resume from the moment the owner presses Pause, with no pass
|
||||||
// having run to record it (issue #147).
|
// having run to record it (issue #147).
|
||||||
Paused bool
|
Paused bool
|
||||||
|
// FailingHref is the one navigation the row offers: the Site's name
|
||||||
|
// links to that Site's failing Series. The chips beside it stay
|
||||||
|
// unlinked; the withdrawn promise lives on outcomeChips (issue #167).
|
||||||
|
FailingHref string
|
||||||
}
|
}
|
||||||
|
|
||||||
// chip is one named outcome count over the owner's window.
|
// chip is one named outcome count over the owner's window.
|
||||||
@@ -223,6 +227,7 @@ func buildLaneRow(p store.LanePass, o store.SiteOutcomes, now time.Time) laneRow
|
|||||||
if p.GapMS > 0 {
|
if p.GapMS > 0 {
|
||||||
row.Gap = (time.Duration(p.GapMS) * time.Millisecond).Truncate(time.Second).String()
|
row.Gap = (time.Duration(p.GapMS) * time.Millisecond).Truncate(time.Second).String()
|
||||||
}
|
}
|
||||||
|
row.FailingHref = seriesListHref(store.SeriesFilterFailing, p.Site, "", 0)
|
||||||
row.Chips = outcomeChips(o)
|
row.Chips = outcomeChips(o)
|
||||||
row.HasChips = len(row.Chips) > 0
|
row.HasChips = len(row.Chips) > 0
|
||||||
row.StatePhrase, row.StateGood, row.Attention = laneState(p, now)
|
row.StatePhrase, row.StateGood, row.Attention = laneState(p, now)
|
||||||
@@ -232,18 +237,24 @@ func buildLaneRow(p store.LanePass, o store.SiteOutcomes, now time.Time) laneRow
|
|||||||
|
|
||||||
// outcomeChips lists a Site's nonzero window sums in the taxonomy's fixed
|
// outcomeChips lists a Site's nonzero window sums in the taxonomy's fixed
|
||||||
// order, so the chips never reorder as the window changes. None observed is
|
// order, so the chips never reorder as the window changes. None observed is
|
||||||
// written by the template, not drawn as a confident zero count.
|
// written by the template, not drawn as a confident zero count. The names are
|
||||||
|
// spelled through outcomeWord, the one vocabulary the failure surface shares
|
||||||
|
// with the Series list's fact line (issue #167). The counts stay unlinked
|
||||||
|
// permanently — a withdrawn promise: two of the six words write no per-Series
|
||||||
|
// state, and the other four count attempts inside the owner window while the
|
||||||
|
// failing filter lists Series failing now, so neither set contains the other
|
||||||
|
// and no chip can be a door to its list.
|
||||||
func outcomeChips(o store.SiteOutcomes) []chip {
|
func outcomeChips(o store.SiteOutcomes) []chip {
|
||||||
fixed := []struct {
|
fixed := []struct {
|
||||||
name string
|
name string
|
||||||
count int
|
count int
|
||||||
}{
|
}{
|
||||||
{"refused", o.Refused},
|
{outcomeWord("refused"), o.Refused},
|
||||||
{"unreachable", o.Unreachable},
|
{outcomeWord("unreachable"), o.Unreachable},
|
||||||
{"no chapter", o.NoChapter},
|
{outcomeWord("no_chapter"), o.NoChapter},
|
||||||
{"unfetchable", o.Unfetchable},
|
{outcomeWord("unfetchable"), o.Unfetchable},
|
||||||
{"not found", o.NotFound},
|
{outcomeWord("not_found"), o.NotFound},
|
||||||
{"errors", o.Errors},
|
{outcomeWord("errors"), o.Errors},
|
||||||
}
|
}
|
||||||
var out []chip
|
var out []chip
|
||||||
for _, f := range fixed {
|
for _, f := range fixed {
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ type seriesRowView struct {
|
|||||||
// page, where a press that retires a Series from the Lane is on purpose
|
// page, where a press that retires a Series from the Lane is on purpose
|
||||||
// and confirm-gated (issue #158).
|
// and confirm-gated (issue #158).
|
||||||
Finished bool
|
Finished bool
|
||||||
|
// Failure names the standing failure and how long it has stood — "not
|
||||||
|
// found · 3d ago", "" while no failure row stands. Its own field, never a
|
||||||
|
// Notes chip: the chips cap at two plus a tail, so the one fact that
|
||||||
|
// names the failure would be the most likely to be truncated away.
|
||||||
|
Failure string
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminSeries renders the filterable, bookmarkable Series list: filter, Site,
|
// adminSeries renders the filterable, bookmarkable Series list: filter, Site,
|
||||||
@@ -683,6 +688,9 @@ func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView {
|
|||||||
row.Ch = "—"
|
row.Ch = "—"
|
||||||
}
|
}
|
||||||
row.Age = checkedAge(now, a.LatestCheckedAt)
|
row.Age = checkedAge(now, a.LatestCheckedAt)
|
||||||
|
if a.FailureOutcome != "" {
|
||||||
|
row.Failure = outcomeWord(a.FailureOutcome) + " · " + checkedAge(now, a.FailingSince)
|
||||||
|
}
|
||||||
notes := seriesNotes(a, now)
|
notes := seriesNotes(a, now)
|
||||||
if n := len(notes); n > 2 {
|
if n := len(notes); n > 2 {
|
||||||
row.Notes, row.More = notes[:2], n-2
|
row.Notes, row.More = notes[:2], n-2
|
||||||
@@ -693,6 +701,20 @@ func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView {
|
|||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outcomeWord spells the wire failure word as the Lanes chips spell it — a
|
||||||
|
// space, not the underscore: not_found reads "not found", no_chapter "no
|
||||||
|
// chapter". The remaining words are their own spelling, so an unknown word
|
||||||
|
// degrades to itself rather than vanishing from the page.
|
||||||
|
func outcomeWord(wire string) string {
|
||||||
|
switch wire {
|
||||||
|
case "not_found":
|
||||||
|
return "not found"
|
||||||
|
case "no_chapter":
|
||||||
|
return "no chapter"
|
||||||
|
}
|
||||||
|
return wire
|
||||||
|
}
|
||||||
|
|
||||||
// seriesNotes are a row's hygiene chips in the design's order: no URL, no
|
// seriesNotes are a row's hygiene chips in the design's order: no URL, no
|
||||||
// cover, orphan, stale, reader sighting.
|
// cover, orphan, stale, reader sighting.
|
||||||
func seriesNotes(a store.AdminSeries, now time.Time) []string {
|
func seriesNotes(a store.AdminSeries, now time.Time) []string {
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ type seriesDetailView struct {
|
|||||||
// stands. It rides the meta fragment both presses swap, so the answer
|
// stands. It rides the meta fragment both presses swap, so the answer
|
||||||
// itself shows how long the Series has been finished.
|
// itself shows how long the Series has been finished.
|
||||||
FinishedSince string
|
FinishedSince string
|
||||||
|
// Unverified is the sentence beside the Latest Chapter correction
|
||||||
|
// control while a Reader's number stands behind a failure past the
|
||||||
|
// owner window: the value is unconfirmed and the owner should not trust
|
||||||
|
// it. It never names the Reader. "" otherwise.
|
||||||
|
Unverified string
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminSeriesDetail renders one Series' page, keyed by the composite
|
// adminSeriesDetail renders one Series' page, keyed by the composite
|
||||||
@@ -151,6 +156,14 @@ func (h *Handler) seriesDetailView(a store.AdminSeries) seriesDetailView {
|
|||||||
v.Corrected = correctedAge(time.Now(), a.LatestCorrectedAt)
|
v.Corrected = correctedAge(time.Now(), a.LatestCorrectedAt)
|
||||||
v.Finished = a.FinishedAt != 0
|
v.Finished = a.FinishedAt != 0
|
||||||
v.FinishedSince = finishedAge(time.Now(), a.FinishedAt)
|
v.FinishedSince = finishedAge(time.Now(), a.FinishedAt)
|
||||||
|
// Unverified: a Reader's number standing behind a failure that has outlived
|
||||||
|
// the owner window is a number nobody has re-checked since — say so next to
|
||||||
|
// the correction control, without naming the Reader. A machine read or a
|
||||||
|
// failure still inside the window carries no sentence; the failure itself
|
||||||
|
// has not yet outlived the twelve hours' worth of trust.
|
||||||
|
if a.RaisedByReader && a.FailureOutcome != "" && a.FailingSince < time.Now().Add(-ownerWindow).UnixMilli() {
|
||||||
|
v.Unverified = "Unverified Reader number: the page has been failing for over 12h, so this Reader-reported chapter is unconfirmed."
|
||||||
|
}
|
||||||
|
|
||||||
// Provenance: the actor class behind the current number, evaluated in the
|
// Provenance: the actor class behind the current number, evaluated in the
|
||||||
// order the classes outrank one another — the owner's stamp, which a
|
// order the classes outrank one another — the owner's stamp, which a
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{range .Rows}}
|
{{range .Rows}}
|
||||||
<div class="trow{{if .Attention}} attention{{end}}">
|
<div class="trow{{if .Attention}} attention{{end}}">
|
||||||
<span class="c-site">{{.Site}}</span>
|
<a class="c-site" href="{{.FailingHref}}">{{.Site}}</a>
|
||||||
<span>{{.Due}}</span>
|
<span>{{.Due}}</span>
|
||||||
<span>{{.Checked}}</span>
|
<span>{{.Checked}}</span>
|
||||||
<span>{{.Gap}}</span>
|
<span>{{.Gap}}</span>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<input type="number" name="chapter" step="any" placeholder="{{.Chapter}}" required>
|
<input type="number" name="chapter" step="any" placeholder="{{.Chapter}}" required>
|
||||||
<button type="submit" class="ghost">Set</button>
|
<button type="submit" class="ghost">Set</button>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Unverified}}<p class="hint">{{.Unverified}}</p>{{end}}
|
||||||
</form>
|
</form>
|
||||||
<form class="dform" hx-post="/admin/series/{{.Key}}/series-url" hx-target="#detail-meta" hx-swap="outerHTML">
|
<form class="dform" hx-post="/admin/series/{{.Key}}/series-url" hx-target="#detail-meta" hx-swap="outerHTML">
|
||||||
<h3>Repair series URL</h3>
|
<h3>Repair series URL</h3>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<span class="c-ch">{{.Ch}}</span>
|
<span class="c-ch">{{.Ch}}</span>
|
||||||
<span>{{.Age}}</span>
|
<span>{{.Age}}</span>
|
||||||
<span class="c-rd">{{.Readers}}</span>
|
<span class="c-rd">{{.Readers}}</span>
|
||||||
<span class="c-note">{{range .Notes}}<span class="mark">{{.}}</span>{{end}}{{if .More}}<span class="mark mark-faint">+{{.More}}</span>{{end}}{{if .Finished}}<span class="mark mark-faint">finished</span>{{end}}</span>
|
<span class="c-note">{{if .Failure}}<span class="mark">{{.Failure}}</span>{{end}}{{range .Notes}}<span class="mark">{{.}}</span>{{end}}{{if .More}}<span class="mark mark-faint">+{{.More}}</span>{{end}}{{if .Finished}}<span class="mark mark-faint">finished</span>{{end}}</span>
|
||||||
<span class="c-act">{{if .CanPoll}}<a class="ghost act" hx-post="/admin/series/{{.Key}}/poll" hx-target="closest .trow" hx-swap="outerHTML" hx-vals='{"band":{{if .Band}}1{{else}}0{{end}}}' href="#">Check now</a>{{end}}{{if .CanRemove}}<button class="ghost danger" hx-post="/admin/series/{{.Key}}/remove" hx-target="closest .trow" hx-swap="outerHTML" hx-include="#filterbar" hx-vals='{"band":{{if .Band}}1{{else}}0{{end}}}' hx-confirm="Removes this series and its stored cover. No Reader has it bookmarked; one re-bookmarking it recreates the row.">Remove</button>{{end}}</span>
|
<span class="c-act">{{if .CanPoll}}<a class="ghost act" hx-post="/admin/series/{{.Key}}/poll" hx-target="closest .trow" hx-swap="outerHTML" hx-vals='{"band":{{if .Band}}1{{else}}0{{end}}}' href="#">Check now</a>{{end}}{{if .CanRemove}}<button class="ghost danger" hx-post="/admin/series/{{.Key}}/remove" hx-target="closest .trow" hx-swap="outerHTML" hx-include="#filterbar" hx-vals='{"band":{{if .Band}}1{{else}}0{{end}}}' hx-confirm="Removes this series and its stored cover. No Reader has it bookmarked; one re-bookmarking it recreates the row.">Remove</button>{{end}}</span>
|
||||||
{{if .RemovalRefused}}<span class="row-msg">a Reader has bookmarked this Series again</span>{{end}}
|
{{if .RemovalRefused}}<span class="row-msg">a Reader has bookmarked this Series again</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+115
-1
@@ -918,7 +918,7 @@ func TestLanesRenderFromSeededRowsAfterRestart(t *testing.T) {
|
|||||||
seedPass(t, st, store.LanePass{Site: "asura", RanAt: now.Add(-time.Minute).UnixMilli(), Due: 5, Checked: 5, GapMS: 20_000})
|
seedPass(t, st, store.LanePass{Site: "asura", RanAt: now.Add(-time.Minute).UnixMilli(), Due: 5, Checked: 5, GapMS: 20_000})
|
||||||
|
|
||||||
body := lanesBody(t, router, st)
|
body := lanesBody(t, router, st)
|
||||||
if !strings.Contains(body, `class="c-site">asura</span>`) {
|
if !strings.Contains(body, `<a class="c-site" href="/admin/series?filter=failing&site=asura">asura</a>`) {
|
||||||
t.Fatalf("asura row missing from a restart-read database:\n%s", body)
|
t.Fatalf("asura row missing from a restart-read database:\n%s", body)
|
||||||
}
|
}
|
||||||
// The fragment carries its own single timer and answers the swap it asked
|
// The fragment carries its own single timer and answers the swap it asked
|
||||||
@@ -989,6 +989,31 @@ func TestNamedOutcomeChips(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each Site row on the Lanes page carries one plain navigation link to that
|
||||||
|
// Site's failing Series — a navigation, not a number: the outcome chips
|
||||||
|
// beside it stay unlinked permanently, so the row offers one click and one
|
||||||
|
// answer. The clean Site links too, into an empty list (issue #167).
|
||||||
|
func TestLaneSiteLinksToFailingSeriesAndChipsStayUnlinked(t *testing.T) {
|
||||||
|
router, st := newWebTestServer(t, lanesConfig())
|
||||||
|
now := time.Now()
|
||||||
|
seedPass(t, st, store.LanePass{Site: "asura", RanAt: now.UnixMilli(), NotFound: 6})
|
||||||
|
seedPass(t, st, store.LanePass{Site: "demonic", RanAt: now.UnixMilli()})
|
||||||
|
|
||||||
|
body := lanesBody(t, router, st)
|
||||||
|
if !strings.Contains(body, `<a class="c-site" href="/admin/series?filter=failing&site=asura">asura</a>`) {
|
||||||
|
t.Errorf("the failing Site's name is not a link to its failing Series:\n%s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `<a class="c-site" href="/admin/series?filter=failing&site=demonic">demonic</a>`) {
|
||||||
|
t.Errorf("the clean Site's name is not a link to its (empty) failing Series:\n%s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `<span class="mark">not found 6</span>`) {
|
||||||
|
t.Errorf("the outcome chip no longer renders beside the link:\n%s", body)
|
||||||
|
}
|
||||||
|
if got := strings.Count(body, `<a class="c-site"`); got != 2 {
|
||||||
|
t.Errorf("Site anchor count = %d, want exactly 2 (only the two Site names navigate):\n%s", got, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Browser configuration is a deployment fact and reachability is derived from
|
// Browser configuration is a deployment fact and reachability is derived from
|
||||||
// the latest browser-Site passes inside the refusal backoff — no poller in any
|
// the latest browser-Site passes inside the refusal backoff — no poller in any
|
||||||
// of the three.
|
// of the three.
|
||||||
@@ -2629,6 +2654,44 @@ func TestSeriesListRowShape(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The row's fact line names the standing failure and how long it has stood,
|
||||||
|
// through one format for every age: a one-hour-old failure and a
|
||||||
|
// three-month-old one both render as "<word> · <checkedAge>", and a Series
|
||||||
|
// with no failure row renders no marker at all. The marker is its own field,
|
||||||
|
// never a Notes chip — the chips cap at two plus a tail, so the failure
|
||||||
|
// would be the fact most likely to be truncated away (issue #167).
|
||||||
|
func TestSeriesListFailureMarkerAges(t *testing.T) {
|
||||||
|
st, dsn := newTestStoreURL(t)
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open %s: %v", dsn, err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
now := time.Now()
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:hour", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:months", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:fine", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||||
|
if err := st.RecordSeriesFailure("asura", "hour", "not_found", now.Add(-time.Hour).UnixMilli()); err != nil {
|
||||||
|
t.Fatalf("seed failure row on hour: %v", err)
|
||||||
|
}
|
||||||
|
if err := st.RecordSeriesFailure("asura", "months", "no_chapter", now.Add(-90*24*time.Hour).UnixMilli()); err != nil {
|
||||||
|
t.Fatalf("seed failure row on months: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := adminSeriesPage(t, newRouter(st, testConfig()), st, "")
|
||||||
|
if !strings.Contains(body, `<span class="mark">not found · 1h ago</span>`) {
|
||||||
|
t.Errorf("the one-hour failure lacks its marker:\n%s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `<span class="mark">no chapter · 90d ago</span>`) {
|
||||||
|
t.Errorf("the three-month failure lacks its marker:\n%s", body)
|
||||||
|
}
|
||||||
|
// The clean row carries none, and neither word leaks anywhere else on
|
||||||
|
// the page: exactly one marker per failing row.
|
||||||
|
if strings.Count(body, "not found") != 1 || strings.Count(body, "no chapter") != 1 {
|
||||||
|
t.Errorf("failure words appear more than once, or on a clean row:\n%s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The per-Series page renders every Series-level fact the admin read model
|
// The per-Series page renders every Series-level fact the admin read model
|
||||||
// holds for the key the list row already shows: title, the composite key with
|
// holds for the key the list row already shows: title, the composite key with
|
||||||
// Site and kind, the Latest Chapter, the check age and the anonymous Reader
|
// Site and kind, the Latest Chapter, the check age and the anonymous Reader
|
||||||
@@ -2768,6 +2831,57 @@ func TestAdminSeriesDetailRendersMarks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The detail page adds one sentence beside the Latest Chapter correction
|
||||||
|
// control only while the number came from a Reader and the failure row has
|
||||||
|
// stood past the owner window: a Reader-attributed failing Series carries it,
|
||||||
|
// a machine-read failing Series does not, and neither does a
|
||||||
|
// Reader-attributed Series whose failure is still inside the window. The
|
||||||
|
// sentence names no Reader (issue #167).
|
||||||
|
func TestAdminSeriesDetailUnverifiedSentence(t *testing.T) {
|
||||||
|
st, dsn := newTestStoreURL(t)
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open %s: %v", dsn, err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
now := time.Now()
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:reader-fail", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(2), bookmarks: 1, raisedBy: true})
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:machine-fail", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(1), bookmarks: 1})
|
||||||
|
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:reader-recent", url: "u", cover: "c", checkedAt: now.UnixMilli(), latestNum: floatPtr(3), bookmarks: 1, raisedBy: true})
|
||||||
|
if err := st.RecordSeriesFailure("asura", "reader-fail", "not_found", now.Add(-24*time.Hour).UnixMilli()); err != nil {
|
||||||
|
t.Fatalf("seed failure row on reader-fail: %v", err)
|
||||||
|
}
|
||||||
|
if err := st.RecordSeriesFailure("asura", "machine-fail", "not_found", now.Add(-24*time.Hour).UnixMilli()); err != nil {
|
||||||
|
t.Fatalf("seed failure row on machine-fail: %v", err)
|
||||||
|
}
|
||||||
|
if err := st.RecordSeriesFailure("asura", "reader-recent", "not_found", now.Add(-time.Hour).UnixMilli()); err != nil {
|
||||||
|
t.Fatalf("seed failure row on reader-recent: %v", err)
|
||||||
|
}
|
||||||
|
srv := newRouter(st, testConfig())
|
||||||
|
|
||||||
|
want := "Unverified Reader number: the page has been failing for over 12h, so this Reader-reported chapter is unconfirmed."
|
||||||
|
fetch := func(key string) string {
|
||||||
|
t.Helper()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/admin/series/"+key, nil)
|
||||||
|
req.AddCookie(sessionCookie(t, st))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /admin/series/%s status = %d, want 200", key, rr.Code)
|
||||||
|
}
|
||||||
|
return rr.Body.String()
|
||||||
|
}
|
||||||
|
if body := fetch("asura:reader-fail"); !strings.Contains(body, want) {
|
||||||
|
t.Errorf("a Reader-attributed failing Series lacks the unverified sentence:\n%s", body)
|
||||||
|
}
|
||||||
|
if body := fetch("asura:machine-fail"); strings.Contains(body, want) {
|
||||||
|
t.Errorf("a machine-read failing Series carries the unverified sentence:\n%s", body)
|
||||||
|
}
|
||||||
|
if body := fetch("asura:reader-recent"); strings.Contains(body, want) {
|
||||||
|
t.Errorf("a Reader-attributed Series failing inside the window carries the sentence:\n%s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The provenance line beside the chapter names the actor class behind the
|
// The provenance line beside the chapter names the actor class behind the
|
||||||
// value — "machine read" for a checked Series, "correction" for the owner's
|
// value — "machine read" for a checked Series, "correction" for the owner's
|
||||||
// stamp, "sighting" for a Reader-raised one — and appears nowhere in the
|
// stamp, "sighting" for a Reader-raised one — and appears nowhere in the
|
||||||
|
|||||||
Reference in New Issue
Block a user