Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
@@ -106,6 +106,16 @@ func TestNewRejectsEmptyChatID(t *testing.T) {
}
}
func TestValidateAllowsEmptySecretsOnlyWhenDisabled(t *testing.T) {
cfg := Config{Disabled: true}
if err := Validate(cfg); err != nil {
t.Fatalf("Validate disabled config: %v", err)
}
if _, err := New(cfg); err == nil {
t.Fatal("New built a live sink from a disabled config")
}
}
func TestNewDefaultTimeout(t *testing.T) {
s, err := New(Config{BotToken: "123:abc", ChatID: "42"})
if err != nil {
@@ -289,6 +299,9 @@ func TestSendReturnsErrorOnTelegramError(t *testing.T) {
if !strings.Contains(err.Error(), "401") {
t.Fatalf("error should mention error_code 401, got: %v", err)
}
if !errors.Is(err, delivery.ErrPermanent) {
t.Fatalf("revoked bot credential must be permanent, got: %v", err)
}
}
// A relay that is up but cannot reach api.telegram.org answers 200 with a page
@@ -525,9 +538,9 @@ func TestSendRoutesThroughProxyWhenConfigured(t *testing.T) {
// ----------------------------- reminder same shape --------------------------
func TestReminderSendUsesSamePath(t *testing.T) {
// reminders away route to ntfy, not telegram — but if the daemon ever
// routes a reminder via telegram (per-reminder override), the sink must
// accept KindReminder undamaged. exercises the kind-agnostic contract.
// Telegram is the second away alternative for reminders. If ntfy is
// unavailable and the dispatcher falls through, the sink must accept
// KindReminder undamaged. exercises the kind-agnostic contract.
rs := newRecordingServer(t, 200, "")
srv := httptest.NewServer(rs.handler())
defer srv.Close()