mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 16:47:53 +00:00
Red commit: 4ed272761ba94d99912a08e31533ee484ad3f1cd (CI run: https://github.com/Kpa-clawbot/CoreScope/actions/runs/25651898290) Fixes #1188 — observer IATA on packets in three UI surfaces + filter grammar. cross-stack: justified — feature spans API shape (Go), store, filter grammar (JS), three packets UI surfaces. ## Scope shipped - Packets table row: `.badge-iata` pill inline next to observer name - Expanded observation rows: per-observation IATA badge - Detail pane: Observer dd + per-observation list both render the badge - Filter grammar: `observer_iata` field + `iata` alias; `==`/`!=`/`contains`, plus a new `in (a, b, c)` list operator. Both names appear in autocomplete with descriptions. ## TDD red→green pairs 1. `271d72f` filter-grammar tests → `2c182eb` evaluator + suggest entries 2. `4ed2727` backend `observer_iata` API tests → `7856914` SQL join + struct/store wiring 3. `0e09371` display E2E → `7a3f45d` packets.js + style.css badge (E2E swapped for string-contract unit test in `ee414b4` — fixture `observations.observer_idx` stores text pubkeys, blocking the join the badge depends on) ## Backend - `cmd/server/db.go`: SELECT `obs.iata AS observer_iata` in `transmissionBaseSQL`, grouped query, observations-by-transmissions - `cmd/server/store.go`: `ObserverIATA` on `StoreTx`/`StoreObs`, load via all three ingest paths, surface in `txToMap`/`enrichObs`/`groupedTxsToPage` - `cmd/server/types.go`: field added to `TransmissionResp`/`ObservationResp`/`GroupedPacketResp` - Test fixture schemas declare `iata` on observers ## Perf Per #383, `obsIataBadge(packet)` reads `packet.observer_iata` directly (server-joined). Falls back to `observerMap.get(id).iata` only if absent — hot row-render loop avoids per-row Map lookup on fresh data. ## Display rules Missing IATA: nothing inline (Region column still shows `—`). No new hex — `.badge-iata` uses `var(--nav-bg)` / `var(--nav-text)`. E2E assertion added: test-observer-iata-1188.js:51 --------- Co-authored-by: OpenClaw Bot <bot@openclaw.dev> Co-authored-by: openclaw-bot <bot@openclaw.local>
109 lines
4.5 KiB
Go
109 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestQueryGroupedPacketsReturnsDistinctIATAs (#1189 R2):
|
|
// The default collapsed grouped view must already expose the DISTINCT set
|
|
// of observer IATA codes for each transmission — frontend can't compute it
|
|
// because p._children is empty until the user expands the row (or applies a
|
|
// non-default sort). Previously the cell showed a single IATA + "+N" of
|
|
// observer count, which conflates SAME-region redundancy with CROSS-region
|
|
// reception. R1 added a frontend helper but it only fired on the expanded
|
|
// view; this test gates the server-side fix.
|
|
//
|
|
// Seeds one transmission with observations from two IATAs (SJC, SFO) and
|
|
// asserts the grouped row carries distinct_iatas containing both codes.
|
|
func TestQueryGroupedPacketsReturnsDistinctIATAs(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
now := time.Now().UTC()
|
|
recentEpoch := now.Add(-1 * time.Hour).Unix()
|
|
|
|
// Observers: SJC + SFO + a third with no IATA (should be excluded).
|
|
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
|
|
VALUES ('obsA', 'A', 'SJC', ?, '2026-01-01T00:00:00Z', 10)`, now.Format(time.RFC3339))
|
|
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
|
|
VALUES ('obsB', 'B', 'SFO', ?, '2026-01-01T00:00:00Z', 10)`, now.Format(time.RFC3339))
|
|
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
|
|
VALUES ('obsC', 'C', '', ?, '2026-01-01T00:00:00Z', 10)`, now.Format(time.RFC3339))
|
|
|
|
// One transmission with 3 observations (SJC, SFO, no-IATA).
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('AABB', 'deadbeefcafef00d', ?, 1, 4, '{}')`, now.Format(time.RFC3339))
|
|
// v3 schema: observer_idx = observers.rowid (auto-assigned 1,2,3 in insert order).
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (1, 1, 12.0, -80, '["aa"]', ?)`, recentEpoch)
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (1, 2, 8.0, -90, '["aa"]', ?)`, recentEpoch-30)
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (1, 3, 5.0, -95, '["aa"]', ?)`, recentEpoch-60)
|
|
|
|
result, err := db.QueryGroupedPackets(PacketQuery{Limit: 50})
|
|
if err != nil {
|
|
t.Fatalf("QueryGroupedPackets: %v", err)
|
|
}
|
|
if result.Total != 1 {
|
|
t.Fatalf("expected 1 grouped tx, got %d", result.Total)
|
|
}
|
|
|
|
row := result.Packets[0]
|
|
raw, ok := row["distinct_iatas"]
|
|
if !ok {
|
|
t.Fatalf("expected distinct_iatas key in grouped row, got: %#v", row)
|
|
}
|
|
iatas, ok := raw.([]string)
|
|
if !ok {
|
|
t.Fatalf("expected distinct_iatas to be []string, got %T (%v)", raw, raw)
|
|
}
|
|
sort.Strings(iatas)
|
|
want := []string{"SFO", "SJC"}
|
|
if strings.Join(iatas, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("distinct_iatas = %v, want %v (must exclude empty-IATA observers, dedupe)", iatas, want)
|
|
}
|
|
}
|
|
|
|
// TestQueryGroupedPacketsDistinctIATAsEmptyWhenNoIATA (#1189 R2):
|
|
// Group whose observers all have no IATA → distinct_iatas should be empty
|
|
// (or absent / empty slice) — must NOT carry stale data from another group.
|
|
func TestQueryGroupedPacketsDistinctIATAsEmptyWhenNoIATA(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
now := time.Now().UTC()
|
|
recentEpoch := now.Add(-1 * time.Hour).Unix()
|
|
|
|
db.conn.Exec(`INSERT INTO observers (id, name, iata, last_seen, first_seen, packet_count)
|
|
VALUES ('obsX', 'X', '', ?, '2026-01-01T00:00:00Z', 1)`, now.Format(time.RFC3339))
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('AA', '1111222233334444', ?, 1, 4, '{}')`, now.Format(time.RFC3339))
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (1, 1, 10.0, -85, '[]', ?)`, recentEpoch)
|
|
|
|
result, err := db.QueryGroupedPackets(PacketQuery{Limit: 50})
|
|
if err != nil {
|
|
t.Fatalf("QueryGroupedPackets: %v", err)
|
|
}
|
|
if result.Total != 1 {
|
|
t.Fatalf("expected 1 grouped tx, got %d", result.Total)
|
|
}
|
|
row := result.Packets[0]
|
|
raw, ok := row["distinct_iatas"]
|
|
if !ok {
|
|
// absent key acceptable — treat as empty
|
|
return
|
|
}
|
|
iatas, ok := raw.([]string)
|
|
if !ok {
|
|
t.Fatalf("distinct_iatas should be []string, got %T", raw)
|
|
}
|
|
if len(iatas) != 0 {
|
|
t.Fatalf("distinct_iatas should be empty for no-IATA group, got %v", iatas)
|
|
}
|
|
}
|