mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-19 08:25:14 +00:00
b881a09f02
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>
122 lines
3.7 KiB
Go
122 lines
3.7 KiB
Go
// Test (#1188): /api/packets response must include observer_iata per packet
|
|
// so the frontend can render the IATA inline without per-row observer lookups.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestPacketsEndpointIncludesObserverIATA asserts the ungrouped packets endpoint
|
|
// surfaces the joined observer's IATA on each packet row.
|
|
func TestPacketsEndpointIncludesObserverIATA(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
packets, ok := body["packets"].([]interface{})
|
|
if !ok || len(packets) == 0 {
|
|
t.Fatal("expected non-empty packets array")
|
|
}
|
|
|
|
// Seeded observers: obs1 → SJC, obs2 → SFO. At least one packet row
|
|
// must carry a non-empty observer_iata string.
|
|
gotIATA := false
|
|
for _, p := range packets {
|
|
m, _ := p.(map[string]interface{})
|
|
if m == nil {
|
|
continue
|
|
}
|
|
if _, present := m["observer_iata"]; !present {
|
|
t.Fatalf("packet missing observer_iata field; got keys: %v", keysOfMap(m))
|
|
}
|
|
if s, _ := m["observer_iata"].(string); s != "" {
|
|
gotIATA = true
|
|
}
|
|
}
|
|
if !gotIATA {
|
|
t.Fatalf("expected at least one packet with non-empty observer_iata (seed has SJC/SFO)")
|
|
}
|
|
}
|
|
|
|
// TestPacketsGroupedIncludesObserverIATA asserts the grouped (groupByHash)
|
|
// view also surfaces observer_iata for the header row.
|
|
func TestPacketsGroupedIncludesObserverIATA(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
req := httptest.NewRequest("GET", "/api/packets?groupByHash=true&limit=10", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
packets, _ := body["packets"].([]interface{})
|
|
if len(packets) == 0 {
|
|
t.Fatal("expected non-empty grouped packets")
|
|
}
|
|
gotIATA := false
|
|
for _, p := range packets {
|
|
m, _ := p.(map[string]interface{})
|
|
if _, present := m["observer_iata"]; !present {
|
|
t.Fatalf("grouped packet missing observer_iata field; got keys: %v", keysOfMap(m))
|
|
}
|
|
if s, _ := m["observer_iata"].(string); s != "" {
|
|
gotIATA = true
|
|
}
|
|
}
|
|
if !gotIATA {
|
|
t.Fatalf("expected at least one grouped packet with non-empty observer_iata")
|
|
}
|
|
}
|
|
|
|
// TestPacketDetailObservationsIncludeIATA asserts /api/packets/{id} returns
|
|
// per-observation observer_iata so the detail pane can render it.
|
|
func TestPacketDetailObservationsIncludeIATA(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
// transmission_id 1 has two observations (obs1 SJC, obs2 SFO) from seedTestData
|
|
req := httptest.NewRequest("GET", "/api/packets/1", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
obs, _ := body["observations"].([]interface{})
|
|
if len(obs) == 0 {
|
|
t.Fatalf("expected observations in detail response; body: %s", w.Body.String())
|
|
}
|
|
gotIATA := false
|
|
for _, o := range obs {
|
|
m, _ := o.(map[string]interface{})
|
|
if _, present := m["observer_iata"]; !present {
|
|
t.Fatalf("observation missing observer_iata field; got keys: %v", keysOfMap(m))
|
|
}
|
|
if s, _ := m["observer_iata"].(string); s != "" {
|
|
gotIATA = true
|
|
}
|
|
}
|
|
if !gotIATA {
|
|
t.Fatalf("expected at least one observation with non-empty observer_iata")
|
|
}
|
|
}
|
|
|
|
func keysOfMap(m map[string]interface{}) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|