Event log: show only the events with a failed or a pending delivery (closes #390)
check / check (push) Successful in 3m14s

The event log gains three links above the list: All, Failed (N) and Pending (N), carried in a `show` query parameter (`failed`, `pending`); any other value shows every event. Failed lists the events with at least one failed delivery, Pending those with one pending or retrying, under the same 50-row limit and newest-first order as the full list. Events are picked by their deliveries' status through `idx_deliveries_status`; each count is the number of distinct matching events, read from the deliveries alone, and can exceed the 50 shown. An empty filtered list says that no event matches. The README and the comments that name the query parameters the service reads now include `show`.

Model: opus-5-5
This commit is contained in:
2026-10-03 02:08:35 +00:00
parent ea8384f4a2
commit ae2ce55575
11 changed files with 363 additions and 39 deletions
+136
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"time"
@@ -186,6 +187,141 @@ func TestHandleSourceLogs_ShowsFiftyNewestEvents(t *testing.T) {
assert.Contains(t, body, "50 most recent of 51 events")
}
// TestHandleSourceLogs_ShowsEventsByDeliveryStatus proves that the
// Failed list holds exactly the events with a failed delivery, once
// each, the Pending list exactly those with a delivery pending or
// retrying, and any other show value every event; that each link
// counts the events its list holds; and that the shown link is marked.
func TestHandleSourceLogs_ShowsEventsByDeliveryStatus(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
now := time.Now()
// Each event is named by its content type. The failed one was
// then replayed and delivered, so it has two deliveries.
events := []struct {
contentType string
deliveries []database.DeliveryStatus
}{
{"application/x-failed", []database.DeliveryStatus{
database.DeliveryStatusFailed,
database.DeliveryStatusDelivered,
}},
{"application/x-pending", []database.DeliveryStatus{
database.DeliveryStatusPending,
}},
{"application/x-retrying", []database.DeliveryStatus{
database.DeliveryStatusRetrying,
}},
{"application/x-delivered", []database.DeliveryStatus{
database.DeliveryStatusDelivered,
}},
{"application/x-no-delivery", nil},
}
all := make([]string, len(events))
for i, e := range events {
event := f.event(
t, e.contentType, "{}", now.Add(time.Duration(i)*time.Second),
)
for _, status := range e.deliveries {
f.delivery(t, event, target.ID, status)
}
all[i] = e.contentType
}
for _, tc := range []struct {
query string
current string
listed []string
}{
{"", "All", all},
{"?show=failed", "Failed (1)", []string{"application/x-failed"}},
{"?show=pending", "Pending (2)", []string{
"application/x-pending", "application/x-retrying",
}},
{"?show=unknown", "All", all},
} {
body := renderSourceLogsPageWithQuery(
t, f.h, f.sess, f.webhook.ID, tc.query,
)
assert.Equal(t, len(tc.listed),
strings.Count(body, `role="button"`), tc.query)
for _, contentType := range all {
assert.Equal(t,
slices.Contains(tc.listed, contentType),
strings.Contains(body, contentType),
tc.query+" "+contentType)
}
assert.Contains(t, body, "Failed (1)", tc.query)
assert.Contains(t, body, "Pending (2)", tc.query)
assert.Equal(t, 1, strings.Count(body, "aria-current"), tc.query)
assert.Contains(t, body,
`aria-current="page">`+tc.current+"</a>", tc.query)
}
}
// TestHandleSourceLogs_FilteredListShowsFiftyNewest proves a filtered
// list holds the 50 newest matching events, as the full log does,
// while its link and heading count every matching event.
func TestHandleSourceLogs_FilteredListShowsFiftyNewest(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
base := time.Now().Add(-time.Hour)
for i := range 51 {
event := f.event(
t, fmt.Sprintf("application/x-failed-%02d", i), "{}",
base.Add(time.Duration(i)*time.Second),
)
f.delivery(t, event, target.ID, database.DeliveryStatusFailed)
}
// The newest event has no failed delivery.
f.event(t, "application/x-no-delivery", "{}", time.Now())
body := renderSourceLogsPageWithQuery(
t, f.h, f.sess, f.webhook.ID, "?show=failed",
)
assert.Equal(t, 50, strings.Count(body, `role="button"`))
assert.NotContains(t, body, "application/x-failed-00")
assert.NotContains(t, body, "application/x-no-delivery")
assert.Contains(t, body, "Failed (51)")
assert.Contains(t, body, "50 most recent of 51 events")
}
// TestHandleSourceLogs_EmptyFilteredList proves an empty filtered list
// says that no event matches rather than that none was recorded.
func TestHandleSourceLogs_EmptyFilteredList(t *testing.T) {
t.Parallel()
f := newRecentEventsFixture(t)
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
f.delivery(
t, f.event(t, contentTypeJSON, "{}", time.Now()),
target.ID, database.DeliveryStatusDelivered,
)
assert.Contains(t, renderSourceLogsPageWithQuery(
t, f.h, f.sess, f.webhook.ID, "?show=failed",
), "No event has a failed delivery.")
assert.Contains(t, renderSourceLogsPageWithQuery(
t, f.h, f.sess, f.webhook.ID, "?show=pending",
), "No event has a delivery pending or retrying.")
}
// TestHandleSourceLogs_OnlyNewestStartsExpanded proves that of the
// events in the log only the newest starts expanded.
func TestHandleSourceLogs_OnlyNewestStartsExpanded(t *testing.T) {