# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: dispatch.spec.js >> Dispatch — Phase 3 prep (ADR-122 booking-point adapters, ADR-119 cheapest-within-SLA, carrier scorecard) >> new mock booking points are reachable through the adapter interface
- Location: tests/dispatch.spec.js:320:3

# Error details

```
Error: HUB_JWT_SECRET not set in environment — source /root/360lm-web/.env before running these tests
```

# Test source

```ts
  159 |     // This is the load-bearing safety rail for the real, funded YEMO account — verified
  160 |     // with a VALID JWT so the test proves the kill-switch itself refuses, not auth.
  161 |     const health = await (await page.request.get('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy')).json();
  162 |     if (health.mode !== 'live') test.skip(true, 'kill-switch only meaningful once live creds are wired');
  163 |     expect(health.live_booking_enabled).toBe(false);
  164 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/book', {
  165 |       headers: { Authorization: `Bearer ${mintTestJwt()}` },
  166 |       data: { order: { test: true } },
  167 |     });
  168 |     const body = await resp.json();
  169 |     expect(body.success).toBe(false);
  170 |     expect(body.booking_disabled).toBe(true);
  171 |   });
  172 | 
  173 |   test('booking-point call without a Bearer token fails closed (ADR-105)', async ({ page }) => {
  174 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/rates', {
  175 |       data: { delivery_pincode: '400001' },
  176 |     });
  177 |     expect(resp.status()).toBe(401);
  178 |   });
  179 | 
  180 |   test('select-courier (ADR-119) picks a serviceable courier with a valid JWT', async ({ page }) => {
  181 |     // Hand-mints an HS256 token with the same HUB_JWT_SECRET the proxy verifies against —
  182 |     // no PWA session carries a real one yet (see dbt_dispatch.md), this proves the auth+
  183 |     // engine path independent of that still-pending plumbing. No new npm dep: HS256 is a
  184 |     // plain HMAC-SHA256 over base64url(header).base64url(payload) (see mintTestJwt below).
  185 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/select-courier', {
  186 |       headers: { Authorization: `Bearer ${mintTestJwt()}` },
  187 |       data: { delivery_pincode: '400001', weight_kg: 3 },
  188 |     });
  189 |     expect(resp.status()).toBe(200);
  190 |     const body = await resp.json();
  191 |     expect(body.success).toBe(true);
  192 |     expect(typeof body.mock).toBe('boolean');
  193 |     expect(body.selected).toBeTruthy();
  194 |     expect(body.zone).toBeTruthy();
  195 |   });
  196 | 
  197 |   test('ndr worklist returns labeled mock data when mock, or an honest not-yet-implemented flag when live (ADR-118)', async ({ page }) => {
  198 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/ndr', {
  199 |       headers: { Authorization: `Bearer ${mintTestJwt()}` },
  200 |     });
  201 |     expect(resp.status()).toBe(200);
  202 |     const body = await resp.json();
  203 |     if (body.mock) {
  204 |       expect(body.items.length).toBeGreaterThan(0);
  205 |       expect(body.items[0].mock).toBe(true);
  206 |     } else {
  207 |       expect(body.items.length).toBe(0);
  208 |       expect(body.error).toContain('not yet implemented');
  209 |     }
  210 |   });
  211 | 
  212 |   test('wallet-balance endpoint returns a numeric balance (mock or live)', async ({ page }) => {
  213 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/wallet', {
  214 |       headers: { Authorization: `Bearer ${mintTestJwt()}` },
  215 |     });
  216 |     expect(resp.status()).toBe(200);
  217 |     const body = await resp.json();
  218 |     expect(body.success).toBe(true);
  219 |     expect(typeof body.balance).toBe('number');
  220 |   });
  221 | 
  222 |   test('proof photo upload -> sign -> view roundtrip, tampered/expired rejected', async ({ page }) => {
  223 |     const token = mintTestJwt();
  224 |     const tinyJpeg = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=';
  225 |     const shipmentId = 'PWTEST-PROOF-' + Date.now();
  226 | 
  227 |     const upload = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/proof-photos/upload', {
  228 |       headers: { Authorization: `Bearer ${token}` },
  229 |       data: { shipment_id: shipmentId, image_b64: tinyJpeg },
  230 |     });
  231 |     expect(upload.status()).toBe(200);
  232 |     const { file_ref } = await upload.json();
  233 | 
  234 |     const sign = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/proof-photos/sign', {
  235 |       headers: { Authorization: `Bearer ${token}` },
  236 |       data: { file_ref },
  237 |     });
  238 |     expect(sign.status()).toBe(200);
  239 |     const { url } = await sign.json();
  240 | 
  241 |     const view = await page.request.get(`https://dev.srv1111289.hstgr.cloud${url}`);
  242 |     expect(view.status()).toBe(200);
  243 |     expect((await view.body()).length).toBeGreaterThan(0);
  244 | 
  245 |     const tampered = await page.request.get(`https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/proof-photos/view?file_ref=${encodeURIComponent(file_ref)}&exp=9999999999&sig=deadbeef`);
  246 |     expect(tampered.status()).toBe(403);
  247 |     const expired = await page.request.get(`https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/proof-photos/view?file_ref=${encodeURIComponent(file_ref)}&exp=1&sig=deadbeef`);
  248 |     expect(expired.status()).toBe(403);
  249 |   });
  250 | });
  251 | 
  252 | function mintTestJwt() {
  253 |   const crypto = require('crypto');
  254 |   const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
  255 |   // Read from the environment, never hardcoded — this is the real HUB_JWT_SECRET
  256 |   // shiprocket-proxy verifies against (ADR-105). Source /root/360lm-web/.env before
  257 |   // running this spec file, e.g.: `set -a; source /root/360lm-web/.env; set +a; npx playwright test ...`
  258 |   const secret = process.env.HUB_JWT_SECRET;
> 259 |   if (!secret) throw new Error('HUB_JWT_SECRET not set in environment — source /root/360lm-web/.env before running these tests');
      |                      ^ Error: HUB_JWT_SECRET not set in environment — source /root/360lm-web/.env before running these tests
  260 |   const header = b64url({ alg: 'HS256', typ: 'JWT' });
  261 |   const payload = b64url({ sub: 'pw-test', exp: Math.floor(Date.now() / 1000) + 1800 });
  262 |   const sig = crypto.createHmac('sha256', secret).update(`${header}.${payload}`).digest('base64url');
  263 |   return `${header}.${payload}.${sig}`;
  264 | }
  265 | 
  266 | test.describe(`${PWA_NAME} — Xpressbees manual worklist (ADR-118)`, () => {
  267 |   test('worklist reachable from TRACK pane', async ({ page }) => {
  268 |     await page.addInitScript(() => {
  269 |       localStorage.setItem('lm360-session', JSON.stringify({ empId: 'test', name: 'Test User', role: 'rep', loginAt: new Date().toISOString() }));
  270 |     });
  271 |     await page.goto(BASE_URL);
  272 |     await page.getByRole('button', { name: 'TRACK' }).click();
  273 |     await expect(page.locator('#xbWorklist')).toBeVisible();
  274 |   });
  275 | });
  276 | 
  277 | test.describe(`${PWA_NAME} — DTDC manual worklist (ADR-118 amended — relay retired)`, () => {
  278 |   test('worklist reachable from TRACK pane', async ({ page }) => {
  279 |     await page.addInitScript(() => {
  280 |       localStorage.setItem('lm360-session', JSON.stringify({ empId: 'test', name: 'Test User', role: 'rep', loginAt: new Date().toISOString() }));
  281 |     });
  282 |     await page.goto(BASE_URL);
  283 |     await page.getByRole('button', { name: 'TRACK' }).click();
  284 |     await expect(page.locator('#dtdcWorklist')).toBeVisible();
  285 |   });
  286 | 
  287 |   test('dtdc-relay-proxy endpoints are torn down (not orphaned)', async ({ page }) => {
  288 |     const resp = await page.request.get('https://dev.srv1111289.hstgr.cloud/dtdc-relay').catch(() => null);
  289 |     // Either the request errors outright (container gone) or Traefik 404s the unmatched route —
  290 |     // either way it must NOT still answer as a live service.
  291 |     if (resp) expect(resp.status()).not.toBe(200);
  292 |   });
  293 | });
  294 | 
  295 | test.describe(`${PWA_NAME} — offline proof-photo draft queue (Q0.3/Q0.10)`, () => {
  296 |   test('IndexedDB queue save/list/delete round-trips', async ({ page }) => {
  297 |     await page.addInitScript(() => {
  298 |       localStorage.setItem('lm360-session', JSON.stringify({ empId: 'test', name: 'Test User', role: 'rep', loginAt: new Date().toISOString() }));
  299 |     });
  300 |     await page.goto(BASE_URL);
  301 |     const result = await page.evaluate(async () => {
  302 |       const id = 'pw_test_' + Date.now();
  303 |       await window._podQueueSave({ id, shipmentId: 'PWTEST-1', imageB64: 'data:image/jpeg;base64,AAA', caption: 'test', docket: 'D1', dispatchId: 'DISP1' });
  304 |       const badgeBefore = document.getElementById('podQueueBadge').textContent;
  305 |       await window.updatePodQueueBadge();
  306 |       const all = await window._podQueueAll();
  307 |       const found = all.find(x => x.id === id);
  308 |       await window._podQueueDelete(id);
  309 |       const afterDelete = await window._podQueueAll();
  310 |       return { found: !!found, foundShipmentId: found?.shipmentId, stillThereAfterDelete: !!afterDelete.find(x => x.id === id), badgeUpdatedTo: document.getElementById('podQueueBadge').textContent };
  311 |     });
  312 |     expect(result.found).toBe(true);
  313 |     expect(result.foundShipmentId).toBe('PWTEST-1');
  314 |     expect(result.stillThereAfterDelete).toBe(false);
  315 |     expect(result.badgeUpdatedTo).toContain('queued offline');
  316 |   });
  317 | });
  318 | 
  319 | test.describe(`${PWA_NAME} — Phase 3 prep (ADR-122 booking-point adapters, ADR-119 cheapest-within-SLA, carrier scorecard)`, () => {
  320 |   test('new mock booking points are reachable through the adapter interface', async ({ page }) => {
  321 |     const token = mintTestJwt();
  322 |     const dl360 = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/360dl-shiprocket/rates', {
  323 |       headers: { Authorization: `Bearer ${token}` }, data: { delivery_pincode: '400001' },
  324 |     });
  325 |     expect(dl360.status()).toBe(200);
  326 |     expect((await dl360.json()).booking_point).toBe('360dl-shiprocket');
  327 | 
  328 |     const bd = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/bluedart-direct-b2b/rates', {
  329 |       headers: { Authorization: `Bearer ${token}` }, data: { delivery_pincode: '400001' },
  330 |     });
  331 |     expect(bd.status()).toBe(200);
  332 |     const bdBody = await bd.json();
  333 |     expect(bdBody.couriers[0].note).toContain('no live implementation yet');
  334 |   });
  335 | 
  336 |   test('cheapest_within_sla rule picks the cheapest courier meeting the SLA, not the fastest', async ({ page }) => {
  337 |     const health = await (await page.request.get('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy')).json();
  338 |     if (health.mode !== 'mock') test.skip(true, 'asserts against fixed mock-rate values (Blue Dart/Delhivery/Ecom Express @ ₹145/118/95) — meaningless once rates are live and variable');
  339 |     const token = mintTestJwt();
  340 |     const ruleId = 'PWTEST-SLA-' + Date.now();
  341 |     const create = await page.request.post('https://dev.srv1111289.hstgr.cloud/db/booking_rules', {
  342 |       headers: { 'Content-Profile': 'dispatch' },
  343 |       data: { rule_id: ruleId, name: 'pw sla test', selection_mode: 'cheapest_within_sla', conditions: { max_sla_days: 4 }, priority: 5 },
  344 |     });
  345 |     expect(create.status()).toBe(201);
  346 | 
  347 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/shiprocket-proxy/booking-points/shiprocket-yemo/select-courier', {
  348 |       headers: { Authorization: `Bearer ${token}` }, data: { delivery_pincode: '400001', weight_kg: 3 },
  349 |     });
  350 |     const body = await resp.json();
  351 |     expect(body.rule_matched).toBe('pw sla test');
  352 |     expect(body.selected.courier_name).toBe('Ecom Express'); // cheapest (₹95) among couriers with ETD <= 4 days
  353 | 
  354 |     await page.request.delete(`https://dev.srv1111289.hstgr.cloud/db/booking_rules?rule_id=eq.${ruleId}`, { headers: { 'Content-Profile': 'dispatch' } });
  355 |   });
  356 | 
  357 |   test('carrier scorecard RPC reachable via web_anon (empty in dev — no real bookings yet)', async ({ page }) => {
  358 |     const resp = await page.request.post('https://dev.srv1111289.hstgr.cloud/db/rpc/carrier_scorecard', {
  359 |       headers: { 'Content-Profile': 'dispatch' }, data: {},
```