From 663eeef775f1269a51b69552c3e6db07c35f579e Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Mon, 27 Jul 2026 20:41:57 -0400 Subject: [PATCH] Stop stripping ?hash on mount, it breaks deep links to older packets The analyzer drawer and path-map restore both read the live ?hash and fetch by hash directly, independent of whether the packet is in the loaded list. Deleting the param once the first page settles without a match unmounts the drawer and can drop path-map restores, for any shared link more than a couple of minutes old. --- src/App.tsx | 3 +- src/features/packets/PacketList.tsx | 19 +------- tests/features/packets/PacketList.test.tsx | 55 ++-------------------- 3 files changed, 7 insertions(+), 70 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4207830..08cd63a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -183,8 +183,7 @@ function AppInner() { }, []); const handleAnalyze = useCallback((hash: string | null) => { - // No reset here: observation ids are globally unique, so a stale one from another packet - // can't accidentally match — this lets a pick made inside an expanded row survive into the drawer. + // No reset: observation ids are globally unique, so a pick inside an expanded row survives into the drawer. setSearchParams((p) => { const n = new URLSearchParams(p); if (hash) { n.set("hash", hash); n.set("analyze", "1"); n.delete("path"); } diff --git a/src/features/packets/PacketList.tsx b/src/features/packets/PacketList.tsx index 58d8a60..0340b6b 100644 --- a/src/features/packets/PacketList.tsx +++ b/src/features/packets/PacketList.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { useState, useCallback, useEffect, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { usePackets } from "./usePackets"; @@ -79,23 +79,6 @@ export function PacketList({ wsManager, onAnalyze, onViewPath, selectedObservati // ?hash is the selected packet — it expands the row inline. The analyzer is a separate state (?analyze=1). const expandedHash = searchParams.get("hash"); - // A deep-linked ?hash that matches nothing once the first page has loaded is stale (bogus, or - // long-expired) — strip it so it doesn't linger forever. Only the hash present at mount is - // checked, once, so a user's own click-to-expand (always a packet already in allPackets) never - // trips this. Gated on isLoading so a slow first page can't strip a link before its packet arrives. - const [initialHash] = useState(() => searchParams.get("hash")); - const strippedInitialHashRef = useRef(false); - useEffect(() => { - if (strippedInitialHashRef.current || isLoading || !initialHash) return; - strippedInitialHashRef.current = true; - if (allPackets.some((p) => p.packetHash === initialHash)) return; - setSearchParams((p) => { - const n = new URLSearchParams(p); - if (n.get("hash") === initialHash) n.delete("hash"); - return n; - }, { replace: true }); - }, [isLoading, initialHash, allPackets, setSearchParams]); - const handleToggleExpand = useCallback((hash: string) => { const next = expandedHash === hash ? null : hash; setSearchParams((p) => { diff --git a/tests/features/packets/PacketList.test.tsx b/tests/features/packets/PacketList.test.tsx index 274367e..d704489 100644 --- a/tests/features/packets/PacketList.test.tsx +++ b/tests/features/packets/PacketList.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi, afterEach } from "vitest"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { MemoryRouter, useLocation } from "react-router-dom"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { PacketList } from "../../../src/features/packets/PacketList"; import type { WsManager } from "../../../src/api/ws-manager"; @@ -100,11 +100,6 @@ const observation = (hash: string): WsPacketObservation["data"] => ({ }, }); -function LocationProbe() { - const location = useLocation(); - return
{location.search}
; -} - function renderList(url = "/", props: Partial[0]> = {}) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const invalidate = vi.spyOn(queryClient, "invalidateQueries"); @@ -112,7 +107,7 @@ function renderList(url = "/", props: Partial[0]> const onViewPath = props.onViewPath ?? vi.fn(); const onSelectObservation = props.onSelectObservation ?? vi.fn(); - const tree = ( + render( [0]> selectedObservationId={null} onSelectObservation={onSelectObservation} /> - - + , ); - const utils = render(tree); - - // MemoryRouter only reads initialEntries on its first mount, so rerendering with the identical - // element (same position in the tree) keeps whatever location the component has navigated to. - return { onAnalyze, onViewPath, onSelectObservation, invalidate, rerender: () => utils.rerender(tree) }; + return { onAnalyze, onViewPath, onSelectObservation, invalidate }; } describe("PacketList server filter wiring", () => { @@ -280,38 +270,3 @@ describe("PacketList live observation invalidation", () => { expect(invalidate).not.toHaveBeenCalled(); }); }); - -describe("PacketList stale ?hash strip", () => { - afterEach(() => { - usePackets.mockImplementation(basePackets); - }); - - it("strips a ?hash matching no loaded packet once the first page has loaded", async () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("AA11")] })); - - renderList("/?tab=Packets&hash=BOGUS"); - - await waitFor(() => expect(screen.getByTestId("search").textContent).not.toContain("hash=")); - }); - - it("does not strip while the first page is still loading", () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: true, allPackets: [] })); - - const { rerender } = renderList("/?tab=Packets&hash=BOGUS"); - expect(screen.getByTestId("search").textContent).toContain("hash=BOGUS"); - - // the packet that matches the deep link arrives only after the first page resolves - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("BOGUS")] })); - rerender(); - - expect(screen.getByTestId("search").textContent).toContain("hash=BOGUS"); - }); - - it("does not strip a ?hash that matches a loaded packet", () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("AA11")] })); - - renderList("/?tab=Packets&hash=AA11"); - - expect(screen.getByTestId("search").textContent).toContain("hash=AA11"); - }); -});