655f5f66f8
gates / gates (push) Successful in 6m0s
- 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
85 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|