mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-03 22:51:58 +00:00
test(#1811): rewrite issue1809 fixture to spread rows past hot window
Round-1 fix (B1 — tautology trap): the original Test1809 fixture
seeded all 100 rows inside the 1h hot window, so LoadChunked alone
produced coverage=1.0 and the assertions passed even if
loadBackgroundChunks was a complete no-op. That made the test a
TDD theatre piece, not a regression gate.
Rewrite:
* Spread 100 rows over 14 days (first_seen + last_seen both
distributed evenly across the span).
* RetentionHours = 14*24, HotStartupHours = 24 — so LoadChunked
catches only ~7 rows; the remaining ~93 MUST be loaded by
loadBackgroundChunks for the assertions to hold.
Added assertions on top of the original red-commit assertions
(which remain intact for TDD audit):
* len(packets) materially > hot-only cap → bg loader actually ran
* oldestLoaded older than (hot cutoff - 12h) → bg loader advanced
through the retention window
Refs #1809, PR #1811.
This commit is contained in:
@@ -10,8 +10,19 @@ package main
|
||||
// The fix extracts a `RunStartupLoad` helper that runs LoadChunked first
|
||||
// and only then spawns the background loader. This test calls the helper
|
||||
// directly and asserts the post-load state.
|
||||
//
|
||||
// PR #1811 round-1 fixture rewrite (B1 — tautology trap): the original
|
||||
// fixture put all 100 rows inside the 1h hot window, so LoadChunked alone
|
||||
// produced coverage=1.0 and the test passed even if loadBackgroundChunks
|
||||
// was a no-op. We now spread rows across 14 days with hotStartupHours=24,
|
||||
// so a no-op bg loader leaves a deliberately incomplete store and the
|
||||
// assertions fail. The original red-commit assertions
|
||||
// (oldestLoaded != "", !backgroundLoadFailed, backgroundLoadDone) are
|
||||
// kept intact; we add the coverage assertions on top.
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -27,11 +38,14 @@ func Test1809_StartupLoad_BgLoaderSeesOldestLoaded(t *testing.T) {
|
||||
dbPath := filepath.Join(dir, "test.db")
|
||||
|
||||
nowSec := time.Now().UTC().Unix()
|
||||
// 100 rows, all within the 1h hot window so LoadChunked picks them up
|
||||
// and bg loader has only ancient (empty) territory to walk back to.
|
||||
createTestDBWithLastSeen(t, dbPath, 100, 1, nowSec,
|
||||
30*time.Minute, // first_seen
|
||||
30*time.Minute) // last_seen
|
||||
// Seed 100 rows spread over 14 days with last_seen also spread, so
|
||||
// hotStartupHours=24 picks up only ~24/(14*24) ≈ 7 rows in the hot
|
||||
// window. The remaining ~93 rows MUST be loaded by the background
|
||||
// loader; if it is a no-op the post-load len(packets) and
|
||||
// oldestLoaded will betray the regression.
|
||||
const totalRows = 100
|
||||
const spanDays = 14
|
||||
createTestDBSpreadOverDays(t, dbPath, totalRows, spanDays, nowSec)
|
||||
|
||||
db, err := OpenDB(dbPath)
|
||||
if err != nil {
|
||||
@@ -40,14 +54,15 @@ func Test1809_StartupLoad_BgLoaderSeesOldestLoaded(t *testing.T) {
|
||||
defer db.conn.Close()
|
||||
|
||||
store := NewPacketStore(db, &PacketStoreConfig{
|
||||
RetentionHours: 168,
|
||||
HotStartupHours: 1,
|
||||
RetentionHours: float64(spanDays * 24), // cover the full seed span
|
||||
HotStartupHours: 24, // hot window = 1 day
|
||||
})
|
||||
|
||||
if err := store.RunStartupLoad(500); err != nil {
|
||||
t.Fatalf("RunStartupLoad: %v", err)
|
||||
}
|
||||
|
||||
// --- original red-commit assertions: KEEP INTACT ---
|
||||
if store.oldestLoaded == "" {
|
||||
t.Fatalf("oldestLoaded is empty after RunStartupLoad; bg loader would bail")
|
||||
}
|
||||
@@ -60,4 +75,95 @@ func Test1809_StartupLoad_BgLoaderSeesOldestLoaded(t *testing.T) {
|
||||
if !store.backgroundLoadDone.Load() {
|
||||
t.Fatalf("backgroundLoadDone=false after RunStartupLoad; expected true on success")
|
||||
}
|
||||
|
||||
// --- B1 anti-tautology assertions: bg loader actually did work ---
|
||||
|
||||
// Bound the rows the hot window alone could have loaded. The seeder
|
||||
// places rows evenly across spanDays so the hot window (24h) catches
|
||||
// at most ~totalRows/spanDays + a small fudge for boundary edge.
|
||||
hotOnlyMax := (totalRows/spanDays)*2 + 5
|
||||
if len(store.packets) <= hotOnlyMax {
|
||||
t.Fatalf("len(packets)=%d after RunStartupLoad — bg loader appears to be a no-op "+
|
||||
"(hot window alone caps at ~%d rows for %d rows spread over %d days). "+
|
||||
"Coverage = backgroundLoadDone may have flipped without bg work.",
|
||||
len(store.packets), hotOnlyMax, totalRows, spanDays)
|
||||
}
|
||||
|
||||
// oldestLoaded must be older than the hot cutoff after the bg loader
|
||||
// has retreated through the retention window. Pre-fix it would equal
|
||||
// the hot cutoff (or empty), proving bg loader never advanced.
|
||||
oldest, err := time.Parse(time.RFC3339, store.oldestLoaded)
|
||||
if err != nil {
|
||||
t.Fatalf("oldestLoaded=%q is not RFC3339: %v", store.oldestLoaded, err)
|
||||
}
|
||||
hotCutoff := time.Unix(nowSec, 0).UTC().Add(-24 * time.Hour)
|
||||
// Allow a small margin since the bg loader chunks daily; oldest
|
||||
// should be at least one full day before the hot cutoff.
|
||||
if !oldest.Before(hotCutoff.Add(-12 * time.Hour)) {
|
||||
t.Fatalf("oldestLoaded=%s is not materially older than hot cutoff %s — "+
|
||||
"bg loader did not advance through the retention window",
|
||||
oldest.Format(time.RFC3339), hotCutoff.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
// createTestDBSpreadOverDays seeds a DB with rows whose first_seen +
|
||||
// last_seen are evenly spread across `spanDays` ending at `nowSec`.
|
||||
// Reuses the same schema shape as createTestDBWithLastSeen.
|
||||
func createTestDBSpreadOverDays(t *testing.T, dbPath string, numTx, spanDays int, nowSec int64) {
|
||||
t.Helper()
|
||||
conn, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
execOrFail := func(s string) {
|
||||
if _, err := conn.Exec(s); err != nil {
|
||||
t.Fatalf("test DB exec: %v\nSQL: %s", err, s)
|
||||
}
|
||||
}
|
||||
execOrFail(`CREATE TABLE transmissions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
raw_hex TEXT, hash TEXT, first_seen TEXT,
|
||||
route_type INTEGER, payload_type INTEGER,
|
||||
payload_version INTEGER, decoded_json TEXT,
|
||||
last_seen INTEGER NOT NULL DEFAULT 0
|
||||
)`)
|
||||
execOrFail(`CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY, transmission_id INTEGER, observer_id TEXT, observer_name TEXT,
|
||||
direction TEXT, snr REAL, rssi REAL, score INTEGER,
|
||||
path_json TEXT, timestamp TEXT, raw_hex TEXT
|
||||
)`)
|
||||
execOrFail(`CREATE TABLE observers (rowid INTEGER PRIMARY KEY, id TEXT, name TEXT, iata TEXT)`)
|
||||
execOrFail(`CREATE TABLE nodes (pubkey TEXT PRIMARY KEY, name TEXT, role TEXT, lat REAL, lon REAL, last_seen TEXT, first_seen TEXT, frequency REAL)`)
|
||||
execOrFail(`CREATE TABLE schema_version (version INTEGER)`)
|
||||
execOrFail(`INSERT INTO schema_version (version) VALUES (1)`)
|
||||
execOrFail(`CREATE INDEX idx_tx_first_seen ON transmissions(first_seen)`)
|
||||
execOrFail(`CREATE INDEX idx_tx_last_seen ON transmissions(last_seen)`)
|
||||
|
||||
txStmt, err := conn.Prepare("INSERT INTO transmissions (id, raw_hex, hash, first_seen, route_type, payload_type, payload_version, decoded_json, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
t.Fatalf("prepare tx: %v", err)
|
||||
}
|
||||
defer txStmt.Close()
|
||||
obsStmt, err := conn.Prepare("INSERT INTO observations (id, transmission_id, observer_id, observer_name, direction, snr, rssi, score, path_json, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
t.Fatalf("prepare obs: %v", err)
|
||||
}
|
||||
defer obsStmt.Close()
|
||||
|
||||
// Evenly distribute first_seen/last_seen across the span (ending now).
|
||||
spanSeconds := int64(spanDays) * 86400
|
||||
for i := 1; i <= numTx; i++ {
|
||||
ago := spanSeconds * int64(numTx-i) / int64(numTx) // newest at i==numTx → 0s ago
|
||||
seenUnix := nowSec - ago
|
||||
seenStr := time.Unix(seenUnix, 0).UTC().Format(time.RFC3339)
|
||||
hash := fmt.Sprintf("h%06d", i)
|
||||
if _, err := txStmt.Exec(i, "aabb", hash, seenStr, 0, 4, 1, "{}", seenUnix); err != nil {
|
||||
t.Fatalf("insert tx %d: %v", i, err)
|
||||
}
|
||||
if _, err := obsStmt.Exec(i, i, "obs1", "Obs1", "RX", -10.0, -80.0, 5, "[]", seenStr); err != nil {
|
||||
t.Fatalf("insert obs %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user