mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-29 00:58:21 +00:00
Fixes #1800. ## Three changes 1. **`routed_through` field** — new `FIELDS` entry. Resolves against `packet.resolved_path` (handles both the JSON-string form from `/api/packets/by-id` and the already-parsed-array form from `/api/packets`). Returns a space-joined lower-case hex string so `contains` / `starts_with` / `==` work the same way they already do for `hash`. 2. **`path` desc clarified + `path_prefixes` alias** — `path` desc now reads `Hop path as 1-byte prefixes joined (e.g. a3→7f). For pubkey search use routed_through.` `path_prefixes` is added as a discoverability alias and resolves to the same value. 3. **Lexer hex-token error** — when the number/duration tokenizer hits an unknown unit AND the slice (extended forward through any remaining `[0-9a-fA-F]`) is pure hex of length ≥ 4, the lexer now returns: ``` Hex value must be quoted: try 'field == "<hex>"' or use the starts_with/contains operator ``` instead of `Invalid duration unit 'f' at position N (expected s/m/h/d/w)`. The duration-unit error is preserved for non-hex cases (`age < 5x` still errors with the original message). ## TDD Red commit `e44ac00a` adds 7 assertions that fail with the unmodified code (proven by stashing the impl and re-running — output: `7 failed`). Green commit `7b623721` makes them pass. Tests added in `test-packet-filter.js` (`#1800: …`): - `routed_through starts_with "2f0b00"` matches packet with JSON-string `resolved_path` - same, against array-form `resolved_path` (handles real `/api/packets` shape) - `routed_through contains "<full-pubkey>"` matches - `routed_through contains "2f0b"` matches - `routed_through starts_with "deadbe"` does NOT match - `path 2f0b001247a047ca` → error contains `Hex value must be quoted` - regression: `path contains "a3"` still matches `path_json=["a3","7f"]` - `routed_through` listed in `FIELDS` - `path_prefixes` alias resolves like `path` `node test-packet-filter.js` → `=== Results: 92 passed, 0 failed ===` Sibling JS tests (`test-packet-filter-ux.js`, `test-packet-filter-time.js`, in-file self-tests) all green. ## Browser verification Browser tool was unavailable this session, so I executed `public/packet-filter.js` in a Node VM context (identical execution) and exercised it against a live `/api/packets?limit=200` response from staging: - `routed_through starts_with "41b1"` returned 1 matching packet (whose `resolved_path[0]` is `41b1eabc3c6e88997242051ee53fa5840761dff02ac5f6d9904f23985395ec31`) - `routed_through starts_with "bccf91"` matched a packet with that hop - `routed_through starts_with "deadbe"` matched nothing (correct) - `PF.suggest('routed_t', 8)` returned `['routed_through']` - `PF.suggest('route', 5)` returned `['route', 'routed_through']` - `PF.compile('path 2f0b001247a047ca').error` is verbatim: `Hex value must be quoted: try 'field == "<hex>"' or use the starts_with/contains operator` - `PF.compile('age < 5x').error` is still `Invalid duration unit 'x' at position 7 (expected s/m/h/d/w)` — duration-unit message preserved for non-hex cases. ## Out of scope (per issue) - No server-side filter pushdown. - No operator-list changes. - No `resolved_path` changes — it already ships on `/api/packets`, `/api/packets/by-id`, `/api/live`. --------- Co-authored-by: meshcore-bot <meshcore-bot@users.noreply.github.com>
This commit is contained in:
co-authored by
meshcore-bot
parent
d5ceb27334
commit
5e096147e5
+33
-2
@@ -78,6 +78,18 @@
|
||||
while (i < len && /[a-zA-Z]/.test(input[i])) i++;
|
||||
var unit = input.slice(unitStart, i);
|
||||
if (!DURATION_UNITS[unit]) {
|
||||
// Issue #1800: when the user typed a bare hex token (e.g.
|
||||
// `path 2f0b001247a047ca`), the number/duration tokenizer eats
|
||||
// only the leading "2f". Extend the scan to capture any further
|
||||
// [0-9a-fA-F] chars; if the combined slice is pure hex of
|
||||
// length >= 4, emit a targeted error rather than the cryptic
|
||||
// duration-unit message.
|
||||
var probe = i;
|
||||
while (probe < len && /[0-9a-fA-F]/.test(input[probe])) probe++;
|
||||
var fullSlice = input.slice(start, probe);
|
||||
if (/^[0-9a-fA-F]+$/.test(fullSlice) && fullSlice.length >= 4) {
|
||||
return { tokens: null, error: "Hex value must be quoted: try 'field == \"<hex>\"' or use the starts_with/contains operator" };
|
||||
}
|
||||
return { tokens: null, error: "Invalid duration unit '" + unit + "' at position " + unitStart + " (expected s/m/h/d/w)" };
|
||||
}
|
||||
tokens.push({ type: TK.DURATION, value: parseFloat(numStr) * DURATION_UNITS[unit], raw: numStr + unit });
|
||||
@@ -274,9 +286,26 @@
|
||||
if (isNaN(ms2)) return null;
|
||||
return Math.max(0, (Date.now() - ms2) / 1000);
|
||||
}
|
||||
if (field === 'path') {
|
||||
if (field === 'path' || field === 'path_prefixes') {
|
||||
try { return JSON.parse(packet.path_json || '[]').join(' → '); } catch(e) { return ''; }
|
||||
}
|
||||
// Issue #1800: routed_through matches against resolved_path full pubkeys.
|
||||
// resolved_path may arrive as a JSON string OR as an already-parsed array
|
||||
// (depends on which API surface produced the packet). Handles both.
|
||||
if (field === 'routed_through') {
|
||||
var rp = packet.resolved_path;
|
||||
if (rp == null) return '';
|
||||
var arr;
|
||||
if (typeof rp === 'string') {
|
||||
try { arr = JSON.parse(rp); } catch (e) { return ''; }
|
||||
} else if (Array.isArray(rp)) {
|
||||
arr = rp;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
if (!Array.isArray(arr)) return '';
|
||||
return arr.map(function(h) { return String(h || '').toLowerCase(); }).join(' ');
|
||||
}
|
||||
if (field === 'payload_bytes') {
|
||||
return packet.raw_hex ? Math.max(0, packet.raw_hex.length / 2 - 2) : 0;
|
||||
}
|
||||
@@ -444,7 +473,9 @@
|
||||
{ name: 'observer_iata', desc: 'Observer IATA region code (e.g. SJC, SFO)' },
|
||||
{ name: 'iata', desc: 'Alias of observer_iata' },
|
||||
{ name: 'observations', desc: 'Number of observations of this packet' },
|
||||
{ name: 'path', desc: 'Hop path (joined with arrows)' },
|
||||
{ name: 'path', desc: 'Hop path as 1-byte prefixes joined (e.g. a3→7f). For pubkey search use routed_through.' },
|
||||
{ name: 'path_prefixes', desc: 'Alias of path (1-byte hop prefixes joined with arrows)' },
|
||||
{ name: 'routed_through', desc: 'Match packets routed through a node by pubkey (full or prefix)' },
|
||||
{ name: 'payload_bytes', desc: 'Payload size in bytes (size - 2 header bytes)' },
|
||||
{ name: 'payload_hex', desc: 'Payload bytes as hex (raw without header)' },
|
||||
{ name: 'time', desc: 'Packet timestamp (epoch ms)' },
|
||||
|
||||
@@ -255,5 +255,48 @@ test('#1774: payload.srcHash listed in FIELDS suggestions', () => {
|
||||
assert(names.indexOf('payload.srcHash') !== -1, 'payload.srcHash missing from FIELDS');
|
||||
});
|
||||
|
||||
// --- Issue #1800: routed_through field + path desc + hex lexer error ---
|
||||
const routedPkt = {
|
||||
...pkt,
|
||||
resolved_path: '["2f0b001247a047cabcdef0123456789a","aabbccddeeff00112233445566778899"]',
|
||||
};
|
||||
const routedArrPkt = {
|
||||
...pkt,
|
||||
resolved_path: ['2f0b001247a047cabcdef0123456789a', 'aabbccddeeff00112233445566778899'],
|
||||
};
|
||||
test('#1800: routed_through starts_with prefix matches', () => {
|
||||
assert(PF.compile('routed_through starts_with "2f0b00"').filter(routedPkt));
|
||||
});
|
||||
test('#1800: routed_through starts_with prefix matches (array form)', () => {
|
||||
assert(PF.compile('routed_through starts_with "2f0b00"').filter(routedArrPkt));
|
||||
});
|
||||
test('#1800: routed_through == full pubkey matches', () => {
|
||||
assert(PF.compile('routed_through contains "2f0b001247a047cabcdef0123456789a"').filter(routedPkt));
|
||||
});
|
||||
test('#1800: routed_through contains "2f0b" matches', () => {
|
||||
assert(PF.compile('routed_through contains "2f0b"').filter(routedPkt));
|
||||
});
|
||||
test('#1800: routed_through does not match unrelated prefix', () => {
|
||||
assert(!PF.compile('routed_through starts_with "deadbe"').filter(routedPkt));
|
||||
});
|
||||
test('#1800: bare hex value after path → "Hex value must be quoted" error', () => {
|
||||
const c = PF.compile('path 2f0b001247a047ca');
|
||||
assert(c.error !== null, 'should have parse/lex error');
|
||||
assert(c.error.indexOf('Hex value must be quoted') !== -1,
|
||||
'error must mention Hex value must be quoted, got: ' + c.error);
|
||||
});
|
||||
test('#1800 regression: path contains "a3" still matches existing prefix list', () => {
|
||||
const prefixPkt = { ...pkt, path_json: '["a3","7f"]' };
|
||||
assert(PF.compile('path contains "a3"').filter(prefixPkt));
|
||||
});
|
||||
test('#1800: routed_through listed in FIELDS suggestions', () => {
|
||||
const names = PF.FIELDS.map(f => f.name);
|
||||
assert(names.indexOf('routed_through') !== -1, 'routed_through missing from FIELDS');
|
||||
});
|
||||
test('#1800: path_prefixes alias resolves like path', () => {
|
||||
const prefixPkt = { ...pkt, path_json: '["a3","7f"]' };
|
||||
assert(PF.compile('path_prefixes contains "a3"').filter(prefixPkt));
|
||||
});
|
||||
|
||||
console.log(`\n=== Results: ${pass} passed, ${fail} failed ===`);
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user