Files
meshcore-analyzer/test-observer-iata-1188.js
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

204 lines
8.0 KiB
JavaScript

/**
* Behavior test (#1188 / #1189 R1): public/packets.js helper `obsIataBadge`
* must actually USE packet.observer_iata and emit the .badge-iata span.
*
* Earlier version was tautological — a grep over the source. A deliberately
* broken `obsIataBadge` that returned a hardcoded string still passed every
* assertion. This version EXTRACTS the function body, evaluates it in a
* Node sandbox, and asserts the returned HTML for known inputs. Mutating
* the implementation (e.g. ignoring `packet.observer_iata`, returning the
* empty string, dropping `escapeHtml`) MUST flip this test red.
*
* Runs in Node.js — no browser, no jsdom required.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
let passed = 0, failed = 0;
function assert(cond, msg) {
if (cond) { passed++; console.log(' \u2705 ' + msg); }
else { failed++; console.error(' \u274c ' + msg); }
}
const src = fs.readFileSync(path.join(__dirname, 'public/packets.js'), 'utf8');
// ── Extract obsIataBadge source. Function spans a small, bounded block. ──
// We capture everything from the `function obsIataBadge` keyword up to and
// including the closing brace that terminates the *function body*. Use a
// non-greedy match then expand minimally — packets.js keeps the helper
// short and self-contained so this is robust.
function extractFn(name) {
const re = new RegExp(
'function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{[\\s\\S]*?\\n\\s{2}\\}',
'm'
);
const m = src.match(re);
if (!m) throw new Error('could not extract function ' + name);
return m[0];
}
const badgeSrc = extractFn('obsIataBadge');
const groupedSrc = extractFn('groupedObserverIataBadgesHtml');
// Sandbox: provide the helpers the function depends on (escapeHtml,
// observerMap). escapeHtml mirrors the real one in public/app.js.
const ctx = {
observerMap: new Map(),
escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
obsIataBadge: null,
groupedObserverIataBadgesHtml: null,
};
vm.createContext(ctx);
vm.runInContext(badgeSrc + '\n' + groupedSrc + '\nobsIataBadge = obsIataBadge;\ngroupedObserverIataBadgesHtml = groupedObserverIataBadgesHtml;', ctx);
const obsIataBadge = ctx.obsIataBadge;
const groupedObserverIataBadgesHtml = ctx.groupedObserverIataBadgesHtml;
if (typeof obsIataBadge !== 'function') {
console.error(' \u274c failed to load obsIataBadge into sandbox');
process.exit(1);
}
// ── Behavior #1: packet.observer_iata SJC → <span class="badge-iata">SJC</span> ──
{
const html = obsIataBadge({ observer_iata: 'SJC' });
assert(typeof html === 'string', 'returns a string');
assert(html.includes('class="badge-iata"'),
'output contains class="badge-iata"');
assert(html.includes('>SJC<'),
'output contains the IATA value (SJC) as text content');
assert(html === '<span class="badge-iata">SJC</span>',
'output is exactly <span class="badge-iata">SJC</span>, got: ' + html);
}
// ── Behavior #2: no observer_iata and no observerMap entry → empty string ──
{
const html = obsIataBadge({ observer_id: 'unknown-obs' });
assert(html === '',
'returns "" when packet has no observer_iata and observerMap lacks the id');
}
// ── Behavior #3: fallback to observerMap when packet.observer_iata absent ──
{
ctx.observerMap.set('obs-fallback', { name: 'Foo', iata: 'OAK' });
const html = obsIataBadge({ observer_id: 'obs-fallback' });
assert(html === '<span class="badge-iata">OAK</span>',
'falls back to observerMap.get(observer_id).iata when packet.observer_iata absent, got: ' + html);
}
// ── Behavior #4: packet.observer_iata WINS over observerMap (server-joined
// field is authoritative; avoids per-row client lookup divergence) ──
{
ctx.observerMap.set('obs-mismatch', { name: 'Bar', iata: 'WRONG' });
const html = obsIataBadge({ observer_id: 'obs-mismatch', observer_iata: 'MRY' });
assert(html === '<span class="badge-iata">MRY</span>',
'packet.observer_iata wins over observerMap value (got: ' + html + ')');
}
// ── Behavior #5: null packet doesn't crash ──
{
const html = obsIataBadge(null);
assert(html === '', 'null packet returns ""');
}
// ── Behavior #6: HTML-escapes hostile IATA-like input ──
{
const html = obsIataBadge({ observer_iata: '<script>' });
assert(!html.includes('<script>'),
'raw <script> not present in output (escapeHtml applied), got: ' + html);
assert(html.includes('&lt;script&gt;'),
'output contains the escaped form &lt;script&gt;');
}
// ── groupedObserverIataBadgesHtml (#1189 R1 UX): distinct-IATA set ──
// Mesh-operator finding: grouped row must distinguish same-region (redundant)
// from cross-region (interesting). Header pill now shows the DISTINCT IATA set.
// All same region: single badge, no +N
{
const p = { observer_iata: 'SJC', _children: [
{ observer_iata: 'SJC' }, { observer_iata: 'SJC' }, { observer_iata: 'SJC' },
]};
const html = groupedObserverIataBadgesHtml(p);
assert(html === '<span class="badge-iata">SJC</span>',
'all-same-region group renders ONE badge with no +N, got: ' + html);
}
// Two distinct regions: both badges visible
{
const p = { observer_iata: 'SJC', _children: [
{ observer_iata: 'SFO' }, { observer_iata: 'SJC' },
]};
const html = groupedObserverIataBadgesHtml(p);
assert(html.includes('SJC') && html.includes('SFO'),
'two-region group renders both IATAs, got: ' + html);
assert(!/\+\d/.test(html),
'no +N suffix when all distinct IATAs fit in 2 visible slots, got: ' + html);
}
// Three+ distinct regions: first 2 + +N of distinct-region count
{
const p = { observer_iata: 'SJC', _children: [
{ observer_iata: 'SFO' }, { observer_iata: 'OAK' }, { observer_iata: 'MRY' },
]};
const html = groupedObserverIataBadgesHtml(p);
const badges = (html.match(/badge-iata/g) || []).length;
assert(badges === 2, 'shows 2 visible badges when distinct-region count > 2, got: ' + badges);
assert(/\+2$/.test(html.trim()),
'trailing +2 reflects distinct-region overflow count, got: ' + html);
}
// Falls back to observerMap when packets lack observer_iata field
{
ctx.observerMap.clear();
ctx.observerMap.set('obs-a', { name: 'A', iata: 'SJC' });
ctx.observerMap.set('obs-b', { name: 'B', iata: 'SFO' });
const p = { observer_id: 'obs-a', _children: [{ observer_id: 'obs-b' }] };
const html = groupedObserverIataBadgesHtml(p);
assert(html.includes('SJC') && html.includes('SFO'),
'falls back to observerMap for missing observer_iata, got: ' + html);
}
// No IATA anywhere → empty string
{
ctx.observerMap.clear();
const html = groupedObserverIataBadgesHtml({ observer_id: 'unknown', _children: [{ observer_id: 'unknown2' }] });
assert(html === '',
'no IATA known anywhere → empty string, got: ' + html);
}
// #1189 R2: default collapsed view (no _children) must use server-provided
// distinct_iatas. This is the regression that R1 didn't fix.
{
ctx.observerMap.clear();
// Group as it arrives in /api/packets?groupByHash=true — _children is
// EMPTY (not populated until the user expands the row).
const p = { observer_iata: 'SJC', distinct_iatas: ['SJC', 'SFO'] };
const html = groupedObserverIataBadgesHtml(p);
assert(html.includes('SJC') && html.includes('SFO'),
'collapsed view with distinct_iatas=[SJC,SFO] renders both badges, got: ' + html);
}
{
ctx.observerMap.clear();
const p = { observer_iata: 'SJC', distinct_iatas: ['SJC', 'SFO', 'OAK', 'MRY'] };
const html = groupedObserverIataBadgesHtml(p);
assert(/\+2$/.test(html.trim()),
'collapsed view with 4 distinct IATAs renders 2 visible + "+2", got: ' + html);
}
{
ctx.observerMap.clear();
const p = { observer_iata: 'SJC', distinct_iatas: ['SJC'] };
const html = groupedObserverIataBadgesHtml(p);
assert(html === '<span class="badge-iata">SJC</span>',
'all-same-region collapsed view renders ONE badge with no +N, got: ' + html);
}
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
process.exit(failed > 0 ? 1 : 0);