, label and
// value as a sibling pair.
const demonicCompletedFixture = `
Status
Completed
`
// Trimmed from https://demonicscans.org/manga/Catastrophic-Necromancer
// fetched 2026-08-22. Same block, ongoing value.
const demonicOngoingFixture = `
Status
Ongoing
`
// Trimmed from https://comix.to/title/q77m-countach fetched 2026-08-22 over a
// cleared Chrome tab (the in-tab fetch of the Series URL, the same
// server-rendered payload the poll reads). The served page escapes the query
// map keys as \u0022; the fixture carries the decoded form, which parses to
// the same key. The recommended strip on this very page carries a finished
// entry; only the ["manga","detail","q77m"] entry counts.
const comixCompletedFixture = ``
// Trimmed from https://comix.to/title/n8we-dungeons-and-crayons fetched
// 2026-08-22 the same way (keys in their decoded form, as above). The
// recommended strip includes a finished entry; the target detail is
// releasing, and only the detail read counts.
const comixOngoingFixture = ``
// Trimmed from GET https://kagane.to/api/v2/series/019c29c3-5abd-70e6-9efc-1f1f22d839f6
// on 2026-08-22.
const kaganeCompletedFixture = `{"series_id":"019c29c3-5abd-70e6-9efc-1f1f22d839f6","title":"Real Account 1-17","publication_status":"Completed","upload_status":"Completed"}`
// Trimmed from GET https://kagane.to/api/v2/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b
// on 2026-08-22.
const kaganeOngoingFixture = `{"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption: The Strongest Level 0","publication_status":"Ongoing","upload_status":"Ongoing"}`
// Composed, not a single live trim: upload Completed alongside a
// non-Completed publication status is the research note's inferred inverse
// case and did not turn up in the ~1900-series live scan of 2026-08-22 (both
// field vocabularies are live-verified; the note's live divergence is 'Cause
// Calypso Can, publication Ongoing + upload Hiatus). The predicate must read
// publication_status only.
const kaganeDivergentFixture = `{"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption: The Strongest Level 0","publication_status":"Ongoing","upload_status":"Completed"}`
// Trimmed from https://novelfull.com/reverend-insanity.html fetched
// 2026-08-22 (the challenge served to plain TLS cleared in a real Chrome the
// same day).
const novelfullCompletedFixture = `
`
// Trimmed from https://novelfull.com/a-cunning-pervert-in-the-cultivation-world.html
// fetched 2026-08-22.
const novelfullOngoingFixture = `
`
// Trimmed from the JSON-LD block in the head of
// https://lightnovelworld.net/novel/a-will-eternal/ fetched 2026-08-22. The
// block sits ahead of the wpdiscuz thread, so the predicate reads the whole
// body and needs no comment marker, unlike lnwLatestChapter.
const lnwCompletedFixture = ``
// Trimmed from
// https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/
// fetched 2026-08-22. Same block, ongoing value.
const lnwOngoingFixture = ``
// Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af
// (redirected to ...-00dcbf97) on 2026-08-10.
const asuraCoverFixture = `
`
// Trimmed from https://demonicscans.org/manga/Catastrophic-Necromancer on 2026-08-10.
// The source publishes the raw space in this URL.
const demonicCoverFixture = `
`
// Trimmed from https://comix.to/title/n8we-dungeons-and-crayons on 2026-08-10.
// The state includes a recommended poster before the target detail object and
// nested IDs inside that object; no og:image is present.
const comixCoverFixture = ``
// Trimmed from GET https://kagane.to/api/v2/series/019fe11a-8670-7cf3-8343-0b02057d3787 on 2026-08-10.
const kaganeCoverFixture = `{"series_covers":[{"cover_id":"019fe11a-84d1-714b-9cf4-2827f277f3c0","language":"en","volume_number":"1","chapter_number":null,"note":null,"image_id":"019fe11a-84c3-7fc3-a84b-88787374b617"}]}`
// Trimmed from https://novelfull.com/reverend-insanity.html on 2026-08-10.
const novelfullCoverFixture = `
`
// Trimmed from
// https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/
// on 2026-08-11.
const lnwCoverFixture = `
`
func TestCoverFrom(t *testing.T) {
const comixURL = "https://comix.to/title/n8we-dungeons-and-crayons"
tests := []struct {
name string
site string
seriesURL string
body string
wantOK bool
wantCover string
}{
{
name: "asura uses published metadata URL",
site: "asura", body: asuraCoverFixture, wantOK: true,
wantCover: "https://cdn.asurascans.com/asura-images/covers/chronicles-of-the-demon-faction.d4dcb8.webp",
},
{
name: "demonic escapes raw spaces",
site: "demonic", body: demonicCoverFixture, wantOK: true,
wantCover: "https://readermc.org/images/thumbnails/Catastrophic%20Necromancer.webp",
},
{
name: "comix takes target medium poster",
site: "comix", seriesURL: comixURL, body: comixCoverFixture, wantOK: true,
wantCover: "https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg",
},
{
name: "kagane reads API cover image ID",
site: "kagane", body: kaganeCoverFixture, wantOK: true,
wantCover: "https://kagane.to/api/v2/image/019fe11a-84c3-7fc3-a84b-88787374b617/compressed",
},
{
name: "novelfull reads image metadata",
site: "novelfull", body: novelfullCoverFixture, wantOK: true,
wantCover: "https://novelfull.com/uploads/webp/novel/reverend-insanity-82661d911a.webp",
},
{
name: "lightnovelworld reads og image",
site: "lightnovelworld", body: lnwCoverFixture, wantOK: true,
wantCover: "https://i1.wp.com/lightnovelworld.net/wp-content/uploads/2025/10/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all.jpg",
},
{
name: "later metadata cover survives empty match",
site: "asura",
body: `
` + asuraCoverFixture,
wantOK: true,
wantCover: "https://cdn.asurascans.com/asura-images/covers/chronicles-of-the-demon-faction.d4dcb8.webp",
},
{
name: "page without cover is empty",
site: "asura", body: `
`,
},
{
name: "unknown site is empty",
site: "unknown", body: asuraCoverFixture,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := coverFrom(tt.site, tt.seriesURL, tt.body)
if ok != tt.wantOK {
t.Fatalf("ok = %v, want %v (got %q)", ok, tt.wantOK, got)
}
if got != tt.wantCover {
t.Errorf("cover = %q, want %q", got, tt.wantCover)
}
})
}
}
func TestCoverFromChallenge(t *testing.T) {
tests := []struct {
site string
seriesURL string
}{
{"asura", "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af"},
{"demonic", "https://demonicscans.org/manga/Catastrophic-Necromancer"},
{"comix", "https://comix.to/title/n8we-dungeons-and-crayons"},
{"kagane", "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b"},
{"novelfull", "https://novelfull.com/reverend-insanity.html"},
{"lightnovelworld", "https://lightnovelworld.net/novel/a-will-eternal/"},
}
for _, tt := range tests {
t.Run(tt.site, func(t *testing.T) {
if got, ok := coverFrom(tt.site, tt.seriesURL, challengeFixture); ok || got != "" {
t.Fatalf("cover = %q, ok = %v, want empty", got, ok)
}
})
}
}
func TestLatestChapterFrom(t *testing.T) {
const asuraURL = "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af"
const demonicURL = "https://demonicscans.org/manga/Catastrophic-Necromancer"
tests := []struct {
name string
site string
seriesURL string
body string
wantOK bool
wantNum float64
wantLabel string
}{
{
name: "asura takes the max, not the last listed",
site: "asura", seriesURL: asuraURL, body: asuraSeriesFixture,
wantOK: true, wantNum: 181, wantLabel: "Chapter 181",
},
{
name: "asura ignores another series' chapter links",
site: "asura", seriesURL: asuraURL, body: asuraCrossSeriesFixture,
wantOK: true, wantNum: 181, wantLabel: "Chapter 181",
},
{
name: "asura scoping survives a build-hash rotation",
site: "asura",
seriesURL: "https://asurascans.com/comics/chronicles-of-the-demon-faction-059befe1",
body: asuraCrossSeriesFixture,
wantOK: true, wantNum: 181, wantLabel: "Chapter 181",
},
{
name: "asura with an unparseable series url",
site: "asura", seriesURL: "https://asurascans.com/", body: asuraSeriesFixture,
wantOK: false,
},
{
name: "demonic takes the max across raw and escaped ampersands",
site: "demonic", seriesURL: demonicURL, body: demonicSeriesFixture,
wantOK: true, wantNum: 296, wantLabel: "Chapter 296",
},
{
name: "demonic keeps decimal chapters parseable",
site: "demonic", seriesURL: demonicURL,
body: `
Chapter 0.5`,
wantOK: true, wantNum: 0.5, wantLabel: "Chapter 0.5",
},
{
name: "empty body",
site: "asura", seriesURL: asuraURL, body: "",
wantOK: false,
},
{
name: "cloudflare challenge page",
site: "asura", seriesURL: asuraURL, body: challengeFixture,
wantOK: false,
},
{
name: "demonic markup handed to the asura rule",
site: "asura", seriesURL: asuraURL, body: demonicSeriesFixture,
wantOK: false,
},
{
name: "unknown site",
site: "mangadex", seriesURL: "https://example.com/x", body: asuraSeriesFixture,
wantOK: false,
},
{
name: "comix reads latestChapterUrl, scoped to this series",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: comixSeriesFixture,
wantOK: true, wantNum: 80, wantLabel: "Chapter 80",
},
{
name: "comix yields nothing on a challenge page",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: challengeFixture,
wantOK: false,
},
{
name: "kagane takes the max chapter_no from API json",
site: "kagane",
seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b",
body: kaganeAPIFixture,
wantOK: true, wantNum: 41, wantLabel: "Chapter 41",
},
{
name: "kagane yields nothing on a challenge page",
site: "kagane",
seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b",
body: challengeFixture,
wantOK: false,
},
{
name: "novelfull takes the max and ignores another series",
site: "novelfull",
seriesURL: "https://novelfull.com/reverend-insanity.html",
body: novelfullSeriesFixture,
wantOK: true, wantNum: 2334, wantLabel: "Chapter 2334",
},
{
name: "novelfull yields nothing on a challenge page",
site: "novelfull",
seriesURL: "https://novelfull.com/reverend-insanity.html",
body: challengeFixture,
wantOK: false,
},
{
name: "novelfull with an unparseable series url",
site: "novelfull",
seriesURL: "https://novelfull.com/genre/Fantasy",
body: novelfullSeriesFixture,
wantOK: false,
},
// Stored before the slug split, so the address carries the ...-not
// Chapter Slug; the 100-423 block under the other slug must still win.
// The body is the chapter-list portion of lnwSeriesFixture with the
// comment block omitted; the marker is kept, because a body without it
// is skipped, not scanned.
{
name: "lightnovelworld max spans both chapter slugs",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not/",
body: strings.SplitN(lnwSeriesFixture, lnwCommentMarker, 2)[0] + lnwCommentMarker,
wantOK: true, wantNum: 423, wantLabel: "Chapter 423",
},
{
name: "lightnovelworld comment anchor cannot set the latest chapter",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/",
body: lnwSeriesFixture,
wantOK: true, wantNum: 423, wantLabel: "Chapter 423",
},
// Marker removed from the fixture, comment block still present: a
// redesign must degrade into a skip, never into the comment's number.
{
name: "lightnovelworld body without the comment marker is skipped",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/",
body: strings.ReplaceAll(lnwSeriesFixture, lnwCommentMarker, ""),
wantOK: false,
},
{
name: "lightnovelworld yields nothing on a challenge page",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/",
body: challengeFixture,
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := latestChapterFrom(tt.site, tt.seriesURL, tt.body)
if ok != tt.wantOK {
t.Fatalf("ok = %v, want %v (got %+v)", ok, tt.wantOK, got)
}
if !tt.wantOK {
return
}
if got.Num != tt.wantNum {
t.Errorf("Num = %v, want %v", got.Num, tt.wantNum)
}
if got.Label != tt.wantLabel {
t.Errorf("Label = %q, want %q", got.Label, tt.wantLabel)
}
})
}
}
func TestSiteCompletedFrom(t *testing.T) {
const asuraURL = "https://asurascans.com/comics/solo-leveling-b60d532c"
const demonicURL = "https://demonicscans.org/manga/Solo-Leveling"
const comixURL = "https://comix.to/title/q77m-countach"
const kaganeURL = "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b"
const novelfullURL = "https://novelfull.com/reverend-insanity.html"
const lnwURL = "https://lightnovelworld.net/novel/a-will-eternal/"
tests := []struct {
name string
site string
seriesURL string
body string
want bool
}{
{
name: "asura completed via escaped props status",
site: "asura", seriesURL: asuraURL, body: asuraCompletedFixture,
want: true,
},
{
name: "asura ongoing value is not completed",
site: "asura",
seriesURL: "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af",
body: asuraOngoingFixture,
want: false,
},
{
name: "asura with the status key removed",
site: "asura", seriesURL: asuraURL,
body: strings.ReplaceAll(asuraCompletedFixture, `"status":[0,"completed"]`, ""),
want: false,
},
{
name: "asura challenge page",
site: "asura", seriesURL: asuraURL, body: challengeFixture,
want: false,
},
{
name: "demonic completed info-block pair",
site: "demonic", seriesURL: demonicURL, body: demonicCompletedFixture,
want: true,
},
{
name: "demonic ongoing value is not completed",
site: "demonic",
seriesURL: "https://demonicscans.org/manga/Catastrophic-Necromancer",
body: demonicOngoingFixture,
want: false,
},
{
name: "demonic with the value li removed",
site: "demonic", seriesURL: demonicURL,
body: strings.ReplaceAll(demonicCompletedFixture, "
Completed", ""),
want: false,
},
{
name: "comix recommended finished does not count",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: comixOngoingFixture,
want: false,
},
{
name: "comix detail finished wins over a finished recommended strip",
site: "comix",
seriesURL: comixURL,
body: comixCompletedFixture,
want: true,
},
{
name: "comix with the detail status removed",
site: "comix", seriesURL: comixURL,
body: strings.ReplaceAll(comixCompletedFixture, `"status":"finished",`, ""),
want: false,
},
{
name: "comix challenge page",
site: "comix", seriesURL: comixURL, body: challengeFixture,
want: false,
},
{
name: "kagane publication Completed",
site: "kagane",
seriesURL: "https://kagane.to/series/019c29c3-5abd-70e6-9efc-1f1f22d839f6",
body: kaganeCompletedFixture,
want: true,
},
{
name: "kagane upload Completed does not count",
site: "kagane", seriesURL: kaganeURL, body: kaganeDivergentFixture,
want: false,
},
{
name: "kagane ongoing values are not completed",
site: "kagane", seriesURL: kaganeURL, body: kaganeOngoingFixture,
want: false,
},
{
name: "kagane with publication_status removed",
site: "kagane", seriesURL: kaganeURL,
body: strings.ReplaceAll(kaganeCompletedFixture, `"publication_status":"Completed",`, ""),
want: false,
},
{
name: "kagane challenge page",
site: "kagane", seriesURL: kaganeURL, body: challengeFixture,
want: false,
},
{
name: "novelfull status link Completed",
site: "novelfull",
seriesURL: novelfullURL,
body: novelfullCompletedFixture,
want: true,
},
{
name: "novelfull ongoing link is not completed",
site: "novelfull",
seriesURL: "https://novelfull.com/a-cunning-pervert-in-the-cultivation-world.html",
body: novelfullOngoingFixture,
want: false,
},
{
name: "novelfull with the status link removed",
site: "novelfull", seriesURL: novelfullURL,
body: strings.ReplaceAll(novelfullCompletedFixture, `
Completed`, ""),
want: false,
},
{
name: "novelfull challenge page",
site: "novelfull", seriesURL: novelfullURL, body: challengeFixture,
want: false,
},
{
name: "lightnovelworld JSON-LD CompletedActionStatus",
site: "lightnovelworld",
seriesURL: lnwURL,
body: lnwCompletedFixture,
want: true,
},
{
name: "lightnovelworld ActiveActionStatus is not completed",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/",
body: lnwOngoingFixture,
want: false,
},
{
name: "lightnovelworld with creativeWorkStatus removed",
site: "lightnovelworld", seriesURL: lnwURL,
body: strings.ReplaceAll(lnwCompletedFixture, `"creativeWorkStatus": "https://schema.org/CompletedActionStatus"`, ""),
want: false,
},
{
name: "lightnovelworld challenge page",
site: "lightnovelworld", seriesURL: lnwURL, body: challengeFixture,
want: false,
},
{
name: "unknown site",
site: "mangadex", seriesURL: "https://mangadex.org/title/x", body: asuraCompletedFixture,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := siteCompletedFrom(tt.site, tt.seriesURL, tt.body); got != tt.want {
t.Errorf("siteCompletedFrom(%q, %q, body) = %v, want %v", tt.site, tt.seriesURL, got, tt.want)
}
})
}
}
where’s everyone
https://lightnovelworld.net/overgeared-chapter-2059/