Files
meshcore-analyzer/cmd/server/issue1189_distinct_iatas_test.go
T
Kpa-clawbot b881a09f02 feat(#1188): show observer IATA on packets + filter grammar (#1189)
Red commit: 4ed272761b (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>
2026-05-17 16:13:11 +00:00

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)
}
}