Spec #137: per-Series poll failure state, completion hint, and outbound owner notification (#174)

Implements spec #137 (spec 4 of 4 from wayfinder map #114).

Closes #137.

Tickets: #164, #165, #166, #167, #168, #169, #170, #171, #172, #173 — all closed, landed on this branch.

## Summary
- #164/#168: sixth outcome word `not_found`; per-Site completed marker predicate.
- #165/#169: `poll_failures` row is the failure state; the pass remembers a Site-reported completion.
- #166/#171: two new admin filters (`failing`, `unverified`); outbound owner notification + stall condition.
- #167/#170: a failure names itself on the Series page; the completion hint reaches the owner and decides nothing.
- #172: the other three fault conditions (no-browser-route, sidecar-down, adapter-broken) feeding the notifier.
- #173: the landing verdict line shares the same `latest.FaultsFrom` judgement the notifier uses, so the page and the push cannot disagree.

`cd backend && go test ./...` green on the merged branch (8 packages).

Reviewed-on: #174
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #174.
This commit is contained in:
2026-08-23 11:31:10 +07:00
committed by sulthan
parent 8e4fa6448e
commit e93e79c1bb
31 changed files with 3745 additions and 176 deletions
+23 -1
View File
@@ -28,6 +28,20 @@ func TestLoadConfigReadsCoverDirectory(t *testing.T) {
}
}
func TestLoadConfigReadsDiscordWebhook(t *testing.T) {
// The address is read, never defaulted: unset stays empty (the whole
// path is off), set flows into the Config for the poller's notifier.
const url = "https://discord.com/api/webhooks/000000/secret"
t.Setenv("DISCORD_WEBHOOK_URL", url)
if got := loadConfig().DiscordWebhookURL; got != url {
t.Fatalf("DiscordWebhookURL = %q, want %q", got, url)
}
t.Setenv("DISCORD_WEBHOOK_URL", "")
if got := loadConfig().DiscordWebhookURL; got != "" {
t.Fatalf("DiscordWebhookURL = %q, want empty when unset", got)
}
}
func TestLoadLatestPollEnabledParsing(t *testing.T) {
tests := []struct {
raw string
@@ -51,7 +65,8 @@ func TestLoadLatestPollEnabledParsing(t *testing.T) {
// nothing here sizes a cooldown any more.
func TestNewLatestPollerWiresFetchers(t *testing.T) {
tls := &latest.TLSFetcher{}
p := newLatestPoller(nil, LatestPoll{Enabled: true}, tls, nil)
notifier := &stubNotifier{}
p := newLatestPoller(nil, LatestPoll{Enabled: true}, tls, nil, notifier)
if p.Fetch != tls {
t.Fatalf("Fetch not wired")
}
@@ -67,8 +82,15 @@ func TestNewLatestPollerWiresFetchers(t *testing.T) {
if p.Now == nil {
t.Fatalf("Now = nil, want the live clock")
}
if p.Notify != notifier {
t.Fatalf("Notify = %v, want the configured notifier", p.Notify)
}
}
// stubNotifier satisfies latest.Notifier so newLatestPoller's wiring can be
// asserted; it is never called.
type stubNotifier struct{ latest.Notifier }
func TestPutStatusValidation(t *testing.T) {
cases := []struct {
name string