diff --git a/app/__tests__/lib/priceStore-poll.test.ts b/app/__tests__/lib/priceStore-poll.test.ts index add370e4a..21aff5001 100644 --- a/app/__tests__/lib/priceStore-poll.test.ts +++ b/app/__tests__/lib/priceStore-poll.test.ts @@ -6,15 +6,24 @@ */ import { describe, it, expect, vi, afterEach } from "vitest"; -// Capture the WS message listener so tests can inject "live" ticks without -// a real socket. subscribeChannel/onMessage mirror WsManagerHandle's shape. -const messageListeners: Array<(data: unknown) => void> = []; +// Capture the WS message listeners so tests can inject "live" ticks without a +// real socket. priceStore subscribes via `onMessageForChannel(slab, listener)`, +// which dispatches ONLY to listeners registered for that channel — so the mock +// keys listeners by channel rather than holding one global list. A global list +// would deliver every tick to every slab, which the real manager never does. +const channelListeners = new Map void>>(); vi.mock("@/lib/priceStore/wsManager", () => ({ getWsManager: () => ({ subscribeChannel: () => () => {}, - onMessage: (l: (data: unknown) => void) => { - messageListeners.push(l); - return () => {}; + onMessage: () => () => {}, + onMessageForChannel: (channel: string, l: (data: unknown) => void) => { + const list = channelListeners.get(channel) ?? []; + list.push(l); + channelListeners.set(channel, list); + return () => { + const cur = channelListeners.get(channel); + if (cur) channelListeners.set(channel, cur.filter((x) => x !== l)); + }; }, onStatusChange: () => () => {}, }), @@ -31,7 +40,7 @@ import { * visibilitychange handler flushes pending ticks synchronously — same * path the real store uses when a backgrounded tab returns). */ function deliverLiveTick(slab: string, price: number) { - for (const l of messageListeners) l({ type: "price", slab, price }); + for (const l of channelListeners.get(slab) ?? []) l({ type: "price", slab, price }); document.dispatchEvent(new Event("visibilitychange")); } diff --git a/app/__tests__/lib/v17-matcher-state.test.ts b/app/__tests__/lib/v17-matcher-state.test.ts index 66b10eaca..bd8790951 100644 --- a/app/__tests__/lib/v17-matcher-state.test.ts +++ b/app/__tests__/lib/v17-matcher-state.test.ts @@ -62,11 +62,18 @@ describe('v17 matcher-state helpers', () => { expect(inspectV17MatcherContext(data, publicKey(21))).toBe('uninitialized'); }); + // The 16-byte VAMM magic occupies CTX_VAMM_OFFSET; the delegate starts + // immediately after it. Writing at CTX_VAMM_OFFSET itself lands on the magic + // and leaves the real delegate slot half-zeroed — which is why the 'invalid' + // case below used to pass for the wrong reason. Matches the offset already + // used by __tests__/hooks/useCreateMarket.v17-matcher-{resume,recovery}.test.ts. + const DELEGATE_OFFSET = CTX_VAMM_OFFSET + 16; + it('classifies a context containing the expected delegate as initialized', () => { const data = new Uint8Array(MATCHER_CONTEXT_LEN); const delegate = publicKey(22); - data.set(delegate.toBytes(), CTX_VAMM_OFFSET); + data.set(delegate.toBytes(), DELEGATE_OFFSET); expect(inspectV17MatcherContext(data, delegate)).toBe('initialized'); }); @@ -74,11 +81,22 @@ describe('v17 matcher-state helpers', () => { it('rejects a context bound to another delegate', () => { const data = new Uint8Array(MATCHER_CONTEXT_LEN); - data.set(publicKey(23).toBytes(), CTX_VAMM_OFFSET); + data.set(publicKey(23).toBytes(), DELEGATE_OFFSET); expect(inspectV17MatcherContext(data, publicKey(24))).toBe('invalid'); }); + it('does not read the delegate from the magic bytes at CTX_VAMM_OFFSET', () => { + // Guards the exact regression the reader's comment describes: a context + // whose delegate slot is untouched must read as uninitialized, no matter + // what the magic region contains. + const data = new Uint8Array(MATCHER_CONTEXT_LEN); + data.set(publicKey(25).toBytes(), CTX_VAMM_OFFSET); + data.fill(0, DELEGATE_OFFSET, DELEGATE_OFFSET + 32); + + expect(inspectV17MatcherContext(data, publicKey(25))).toBe('uninitialized'); + }); + it('rejects an undersized matcher context', () => { const data = new Uint8Array(MATCHER_CONTEXT_LEN - 1); diff --git a/app/__tests__/unit/gh1654-market-info-bar-visibility.test.ts b/app/__tests__/unit/gh1654-market-info-bar-visibility.test.ts index 137bc3334..54f4034ac 100644 --- a/app/__tests__/unit/gh1654-market-info-bar-visibility.test.ts +++ b/app/__tests__/unit/gh1654-market-info-bar-visibility.test.ts @@ -4,7 +4,7 @@ * 1. data-testid="market-info-bar" attribute present * 2. sticky positioning class * 3. No `hidden` class on the wrapper (visible on all breakpoints) - * 4. MarketLogo is imported and rendered + * 4. A market logo still reaches the bar (now via MarketSwitcher) */ import { readFileSync } from "fs"; @@ -20,6 +20,11 @@ const pageSource = readFileSync( "utf-8" ); +const switcherSource = readFileSync( + join(__dirname, "../../components/trade/MarketSwitcher.tsx"), + "utf-8" +); + describe("GH#1654 — MarketInfoBar visibility", () => { test("has data-testid attribute", () => { expect(barSource).toContain('data-testid="market-info-bar"'); @@ -29,9 +34,21 @@ describe("GH#1654 — MarketInfoBar visibility", () => { expect(barSource).toContain("sticky"); }); - test("imports MarketLogo", () => { - expect(barSource).toContain("MarketLogo"); - expect(barSource).toContain("@/components/market/MarketLogo"); + // MarketInfoBar no longer imports MarketLogo directly — the logo moved into + // MarketSwitcher, which the bar renders. The GH#1654 guarantee ("the info bar + // shows a market logo") is unchanged, so follow the composition rather than + // asserting on an import that legitimately moved one level down. + test("renders MarketSwitcher with the logo props", () => { + expect(barSource).toContain("@/components/trade/MarketSwitcher"); + const usage = barSource.match(/]*>/)?.[0] ?? ""; + expect(usage).toContain("logoUrl"); + expect(usage).toContain("mintAddress"); + expect(usage).toContain("symbol"); + }); + + test("MarketSwitcher renders MarketLogo", () => { + expect(switcherSource).toContain("@/components/market/MarketLogo"); + expect(switcherSource).toMatch(/]*mintAddress/); }); test("page renders MarketInfoBar without hidden wrapper", () => {