Files
felhom.eu/hub/internal/store/app_log_residue_test.go
T
admin 655f5f66f8
gates / gates (push) Successful in 6m0s
hub v0.147.0 (code): the operator's answers S4, S6, S10, S29, S30, S32, S33
- S4 / R-242: GoldenVouchChecker - a published golden unvouched for 2 h raises golden_not_vouched (operator only)
- S6 / R-244: the delete cascade de-references app_log_issues; one-time sweep flag -sweep-app-log-residue=count|apply
- S10 / R-266: read_failed storage entry shows "read failed", not 0 %
- S29 / R-250: host-key scan IPv4 first, ~2 min ladder, "safe to press Save again" error
- S30 / R-246: stale_at retired (reader, setter, badge)
- S32 / R-521: storage_reconnected ends the storage_disconnected cooldown on both channels
- S33 / R-402: host page "Last full check (off-site)" line; two wire-gate allowlist entries removed
Each with a test seen failing first. Not built or deployed yet (operator-attended deploy).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-12 14:54:14 +02:00

85 lines
3.8 KiB
Go

package store
import (
"encoding/json"
"testing"
)
func seedIssue(t *testing.T, s *Store, app, fp, affected, ctxCust, ctx string) {
t.Helper()
if _, err := s.db.Exec(`INSERT INTO app_log_issues (app_name, fingerprint, severity, message, first_seen, last_seen, affected_customers, context, context_customer)
VALUES (?, ?, 'error', 'm', datetime('now'), datetime('now'), ?, ?, ?)`, app, fp, affected, ctx, ctxCust); err != nil {
t.Fatal(err)
}
}
func issueRow(t *testing.T, s *Store, app, fp string) (affected []string, ctxCust, ctx string, found bool) {
t.Helper()
var a string
err := s.db.QueryRow(`SELECT affected_customers, COALESCE(context_customer,''), COALESCE(context,'') FROM app_log_issues WHERE app_name=? AND fingerprint=?`, app, fp).Scan(&a, &ctxCust, &ctx)
if err != nil {
return nil, "", "", false
}
_ = json.Unmarshal([]byte(a), &affected)
return affected, ctxCust, ctx, true
}
// R-244 (S6): the customer delete de-references the shared rows. The CONSEQUENCE: the gone customer is
// named nowhere, a row only they had is gone, a row shared with a live customer keeps that customer's
// history, and a near-namesake ("gone" vs "gone2") is untouched.
// RED-PROOF: remove the dereferenceCustomerFromAppIssues call from PurgeCustomerResidue → the orphan row
// survives ("orphan row survived").
func TestDereferenceCustomerFromAppIssues(t *testing.T) {
s := newTestStore(t)
seedIssue(t, s, "immich", "a", `["gone"]`, "gone", `["line from gone"]`)
seedIssue(t, s, "immich", "b", `["gone","live"]`, "gone", `["line from gone"]`)
seedIssue(t, s, "immich", "c", `["live","gone"]`, "live", `["line from live"]`)
seedIssue(t, s, "immich", "d", `["gone2"]`, "gone2", `["x"]`)
if err := s.PurgeCustomerResidue("gone"); err != nil {
t.Fatal(err)
}
if _, _, _, found := issueRow(t, s, "immich", "a"); found {
t.Fatal("orphan row survived — a row only the deleted customer had must go")
}
if a, cc, ctx, _ := issueRow(t, s, "immich", "b"); len(a) != 1 || a[0] != "live" || cc != "" || ctx != "" {
t.Fatalf("shared row b: affected=%v context_customer=%q context=%q — want [live], the gone customer's lines dropped", a, cc, ctx)
}
if a, cc, ctx, _ := issueRow(t, s, "immich", "c"); len(a) != 1 || a[0] != "live" || cc != "live" || ctx == "" {
t.Fatalf("shared row c: affected=%v context_customer=%q — the live customer's lines must stay", a, cc)
}
if a, _, _, found := issueRow(t, s, "immich", "d"); !found || len(a) != 1 || a[0] != "gone2" {
t.Fatalf("near-namesake row d was touched: %v found=%v", a, found)
}
}
// The one-time sweep: count first (changes nothing), then apply; a customer the hub still knows by any
// of config / host / report is never touched.
func TestSweepDeletedAppIssueRefs(t *testing.T) {
s := newTestStore(t)
if err := s.SaveReport("live", []byte(`{}`)); err != nil {
t.Fatal(err)
}
seedIssue(t, s, "x", "1", `["old-venue"]`, "old-venue", `["l"]`)
seedIssue(t, s, "x", "2", `["old-venue","live"]`, "live", `["l"]`)
plan, err := s.SweepDeletedAppIssueRefs(false)
if err != nil || len(plan) != 1 || plan[0].CustomerID != "old-venue" || plan[0].Deleted != 1 || plan[0].Dereferenced != 1 {
t.Fatalf("dry run plan = %+v err=%v, want old-venue deleted 1 dereferenced 1", plan, err)
}
if _, _, _, found := issueRow(t, s, "x", "1"); !found {
t.Fatal("the dry run changed the table")
}
if _, err := s.SweepDeletedAppIssueRefs(true); err != nil {
t.Fatal(err)
}
if _, _, _, found := issueRow(t, s, "x", "1"); found {
t.Fatal("apply left the orphan row")
}
if a, cc, _, _ := issueRow(t, s, "x", "2"); len(a) != 1 || a[0] != "live" || cc != "live" {
t.Fatalf("apply damaged the live customer's row: %v %q", a, cc)
}
again, _ := s.SweepDeletedAppIssueRefs(false)
if len(again) != 0 {
t.Fatalf("the sweep is not idempotent: %+v", again)
}
}