upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests/integration/test-price-discovery.mjs
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 13:14:48 +0530
committerYour Name <you@example.com>2026-05-19 13:14:48 +0530
commitfe6aa9663d4cdabdc6e71db6068f8cd9e3739ffe (patch)
tree8cadb07243c07a6b3fa9453b239c9ac5cb02b454 /tests/integration/test-price-discovery.mjs
parent77031f06a9a87320d011f501590985161d1eb305 (diff)
feat: WiFi beacon price discovery via Vendor IE (two-board verified)
Price discovery allows TollGate ESP32 boards to advertise their per-step price via WiFi Vendor-Specific Information Elements (OUI 0xC0FFEE) in beacon and probe response frames. Nearby boards passively scan and build a market view of competing TollGates without requiring internet access. Features: - beacon_price.c/h: 26-byte packed Vendor IE payload (price, step, metric, mint_hash, geohash, npub_hash), injected via esp_wifi_set_vendor_ie() - market.c/h: Passive WiFi scan receiver, vendor IE callback parsing, BSSID-correlated market entries, effective price ranking - GET /market API endpoint: JSON market snapshot with discovered entries - AP-only services: beacon + market + API start on WIFI_EVENT_AP_START, independent of STA connectivity - STA reconnect fix: 2s delay between retries creates scan windows; s_sta_connecting guard prevents double-connect - write-config-ap-only-a/b Makefile targets for STA-less testing - market_tick() in main loop, client price comparison logging Hardware verified: both boards discover each other via Vendor IE beacons. Board A sees TollGate-C0E9CA (RSSI=-30), Board B sees TollGate-B96D80 (RSSI=-25). test-market.mjs: 9/9, test-price-discovery.mjs: 7/7 per board. Unit tests: 45 new assertions across test_beacon_price (28) and test_market (17). All 15 test suites pass. ESP-IDF build clean for ESP32-S3.
Diffstat (limited to 'tests/integration/test-price-discovery.mjs')
-rw-r--r--tests/integration/test-price-discovery.mjs138
1 files changed, 138 insertions, 0 deletions
diff --git a/tests/integration/test-price-discovery.mjs b/tests/integration/test-price-discovery.mjs
new file mode 100644
index 0000000..6762130
--- /dev/null
+++ b/tests/integration/test-price-discovery.mjs
@@ -0,0 +1,138 @@
1import { execSync } from 'child_process';
2
3const BOARD_A_IP = process.env.TOLLGATE_IP || '10.185.47.1';
4const BOARD_B_IP = process.env.TOLLGATE_B_IP || process.env.TOLLGATE_IP_B || '10.192.45.1';
5const API_A = `http://${BOARD_A_IP}:2121`;
6const API_B = `http://${BOARD_B_IP}:2121`;
7
8function run(cmd) {
9 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
10 catch (e) { return e.stdout || null; }
11}
12
13function runJson(cmd) {
14 const out = run(cmd);
15 try { return out ? JSON.parse(out) : null; }
16 catch { return null; }
17}
18
19let passed = 0, failed = 0;
20function assert(cond, msg) {
21 if (cond) { console.log(` PASS: ${msg}`); passed++; }
22 else { console.log(` FAIL: ${msg}`); failed++; }
23}
24
25function canReach(url) {
26 const result = run(`curl -s --connect-timeout 3 --max-time 5 -o /dev/null -w "%{http_code}" ${url}`);
27 return result && result.trim() !== '000' && result.trim() !== '';
28}
29
30console.log('=== test-price-discovery (two-board) ===\n');
31
32const reachA = canReach(`${API_A}/market`);
33const reachB = canReach(`${API_B}/market`);
34
35console.log(`Reachability: Board A=${reachA ? 'YES' : 'NO'}, Board B=${reachB ? 'YES' : 'NO'}\n`);
36
37if (!reachA && !reachB) {
38 console.log('FATAL: Neither board reachable. Check TOLLGATE_IP and TOLLGATE_B_IP');
39 process.exit(1);
40}
41
42console.log('--- Board A: market endpoint ---');
43{
44 if (reachA) {
45 const data = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`);
46 assert(data !== null, 'Board A /market returns JSON');
47 assert(typeof data?.count === 'number', `Board A count is ${data?.count}`);
48 if (data && data.entries) {
49 console.log(` Board A sees ${data.count} nearby TollGate(s):`);
50 for (const e of data.entries) {
51 console.log(` ${e.ssid} (BSSID: ${e.bssid}) — ${e.price_per_step} sats/step, RSSI: ${e.rssi}`);
52 }
53 }
54 } else {
55 console.log(' SKIP: Board A not reachable');
56 }
57}
58
59console.log('\n--- Board B: market endpoint ---');
60{
61 if (reachB) {
62 const data = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`);
63 assert(data !== null, 'Board B /market returns JSON');
64 assert(typeof data?.count === 'number', `Board B count is ${data?.count}`);
65 if (data && data.entries) {
66 console.log(` Board B sees ${data.count} nearby TollGate(s):`);
67 for (const e of data.entries) {
68 console.log(` ${e.ssid} (BSSID: ${e.bssid}) — ${e.price_per_step} sats/step, RSSI: ${e.rssi}`);
69 }
70 }
71 } else {
72 console.log(' SKIP: Board B not reachable');
73 }
74}
75
76console.log('\n--- Cross-discovery: Board A sees Board B ---');
77{
78 if (reachA) {
79 const mktA = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`);
80 if (mktA && mktA.count > 0) {
81 const foundB = mktA.entries.some(e =>
82 e.ssid.startsWith('TollGate-') && e.bssid !== '' && e.price_per_step > 0
83 );
84 assert(foundB, `Board A discovered another TollGate (count=${mktA.count})`);
85 } else {
86 console.log(' INFO: Board A has 0 entries. Scan may need more time.');
87 }
88 } else {
89 console.log(' SKIP: Board A not reachable');
90 }
91}
92
93console.log('\n--- Cross-discovery: Board B sees Board A ---');
94{
95 if (reachB) {
96 const mktB = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`);
97 if (mktB && mktB.count > 0) {
98 const foundA = mktB.entries.some(e =>
99 e.ssid.startsWith('TollGate-') && e.bssid !== '' && e.price_per_step > 0
100 );
101 assert(foundA, `Board B discovered another TollGate (count=${mktB.count})`);
102 } else {
103 console.log(' INFO: Board B has 0 entries. Scan may need more time.');
104 }
105 } else {
106 console.log(' SKIP: Board B not reachable');
107 }
108}
109
110console.log('\n--- Discovery data integrity ---');
111{
112 const boards = [];
113 if (reachA) {
114 const mktA = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`);
115 if (mktA?.entries) boards.push({ name: 'A', data: mktA });
116 }
117 if (reachB) {
118 const mktB = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`);
119 if (mktB?.entries) boards.push({ name: 'B', data: mktB });
120 }
121
122 for (const { name, data } of boards) {
123 for (const e of data.entries) {
124 assert(typeof e.price_per_step === 'number' && e.price_per_step > 0,
125 `Board ${name} entry has valid price (${e.price_per_step})`);
126 assert(typeof e.step_size === 'number' && e.step_size > 0,
127 `Board ${name} entry has valid step_size (${e.step_size})`);
128 assert(typeof e.metric === 'string' && e.metric.length > 0,
129 `Board ${name} entry has valid metric (${e.metric})`);
130 assert(typeof e.rssi === 'number',
131 `Board ${name} entry has valid RSSI (${e.rssi})`);
132 break;
133 }
134 }
135}
136
137console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
138process.exit(failed > 0 ? 1 : 0);