From 98e4246472c0b8d5264df2a40bdf2f04864a2994 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 17:35:13 -0500 Subject: [PATCH 1/6] Drop change history from the mapWithConcurrency doc comment --- packages/graph-explorer/src/utils/mapWithConcurrency.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/graph-explorer/src/utils/mapWithConcurrency.ts b/packages/graph-explorer/src/utils/mapWithConcurrency.ts index 288743926..3176c84ef 100644 --- a/packages/graph-explorer/src/utils/mapWithConcurrency.ts +++ b/packages/graph-explorer/src/utils/mapWithConcurrency.ts @@ -10,10 +10,6 @@ * leave the remaining lanes issuing requests nobody is waiting for. Callbacks * already running are not cancelled. * - * That stop applies to every caller, including the openCypher and SPARQL schema - * and edge connection fetches. They previously drained the whole queue after a - * failure their caller had already given up on, so the change is theirs too. - * * @param items The items to process * @param concurrency The maximum number of callbacks running at once * @param callback The async operation to run for each item From 4f8f7fd5ff3507402d8bc20c60af1fac7975f462 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 17:35:52 -0500 Subject: [PATCH 2/6] Test that a legacy direct default connection keeps its endpoint path --- .../src/connections/defaultConnection.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/graph-explorer/src/connections/defaultConnection.test.ts b/packages/graph-explorer/src/connections/defaultConnection.test.ts index 0908d6a34..411cc7450 100644 --- a/packages/graph-explorer/src/connections/defaultConnection.test.ts +++ b/packages/graph-explorer/src/connections/defaultConnection.test.ts @@ -257,6 +257,20 @@ describe("backward compatibility: main-era defaultConnection.json", () => { }); }); + test("keeps the full path of a direct public endpoint", () => { + const connection = readFile({ + GRAPH_EXP_PUBLIC_OR_PROXY_ENDPOINT: + "http://blazegraph:9999/blazegraph/namespace/kb", + GRAPH_EXP_USING_PROXY_SERVER: false, + GRAPH_EXP_GRAPH_TYPE: "sparql", + }); + + expect(connection?.graphDbUrl).toBe( + "http://blazegraph:9999/blazegraph/namespace/kb", + ); + expect(connection?.proxyConnection).toBe(false); + }); + // Earlier versions defaulted a missing flag to false for this file test("connects directly to the public endpoint when the flag is absent and both URLs are set", () => { const connection = readFile(createMainEraFile()); From 0aa5643245b88f6ef9c536196547e7364211aea7 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 17:36:21 -0500 Subject: [PATCH 3/6] Assert the username-only database URL is rejected for its credentials --- packages/graph-explorer-proxy-server/src/app.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/graph-explorer-proxy-server/src/app.test.ts b/packages/graph-explorer-proxy-server/src/app.test.ts index 411f25f27..9d44cdd95 100644 --- a/packages/graph-explorer-proxy-server/src/app.test.ts +++ b/packages/graph-explorer-proxy-server/src/app.test.ts @@ -498,6 +498,13 @@ describe("createApp", () => { .send({ gremlin: "test" }); expect(response.status).toBe(400); + expect(response.body.error.message).toContain( + "Must not include a username or password", + ); + expect(mockFetch).not.toHaveBeenCalledWith( + expect.stringContaining("my-graph-db.example.com"), + expect.anything(), + ); }); it("does not echo the rejected value back to the client", async () => { From b00884dcee39fda78b3c94351dc2870905ac25a0 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 17:36:58 -0500 Subject: [PATCH 4/6] Assert the exact problems an invalid connection link reports --- .../src/core/connectionLink/validation.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/graph-explorer/src/core/connectionLink/validation.test.ts b/packages/graph-explorer/src/core/connectionLink/validation.test.ts index 5a230f09c..b408577bd 100644 --- a/packages/graph-explorer/src/core/connectionLink/validation.test.ts +++ b/packages/graph-explorer/src/core/connectionLink/validation.test.ts @@ -294,6 +294,10 @@ describe("reading a connection link", () => { test("reports every offending parameter, not just the first", () => { expect( problemsOf("?graphDbUrl=not-a-url&queryEngine=sql&serviceType=bogus"), - ).toHaveLength(3); + ).toStrictEqual([ + "graphDbUrl must be a valid http or https URL", + 'queryEngine must be one of "gremlin", "openCypher", "sparql"', + 'serviceType must be one of "neptune-db", "neptune-graph"', + ]); }); }); From 820fa2c899f449a2cd14f00760f65bf8525760c3 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 18:36:49 -0500 Subject: [PATCH 5/6] Remove cross-tab isolation tests that cannot fail Each simulated tab gets its own Jotai store, and createSessionScopedAtom reads the breadcrumb only at creation with no cross-tab listener, so with this harness an open tab never observes another tab's write and the tests pass trivially. The reload tests cover the isolation that can regress. --- .../sessionScopedStorage.test.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts b/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts index 36ae0bf31..095603b0a 100644 --- a/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts @@ -317,16 +317,6 @@ describe("createSessionScopedAtom", () => { }); describe("createSessionScopedAtom across tabs", () => { - test("writing in one tab does not change an already-open tab", async () => { - const tabB = await openTab(); - await tabB.write({ count: 2 }); - - const tabA = await openTab(); - await tabA.write({ count: 99 }); - - expect(tabB.read()).toStrictEqual({ count: 2 }); - }); - test("a tab opened later cold-starts to the value an earlier tab wrote", async () => { const earlierTab = await openTab(); await earlierTab.write({ count: 3 }); @@ -469,24 +459,6 @@ describe("backward compatibility: graph view layout breadcrumb", () => { // the array serialization across a write-in-one-tab / cold-start-in-another // sequence, which the toy counter codec above cannot reach. describe("graph view layout across tabs", () => { - test("changing the view layout in one tab does not change an already-open tab", async () => { - const tabB = await openGraphViewTab(); - const tabBLayout: GraphViewLayout = { - ...defaultGraphViewLayout, - activeSidebarItem: "filters", - activeToggles: new Set(["graph-viewer"]), - }; - await tabB.write(tabBLayout); - - const tabA = await openGraphViewTab(); - await tabA.write({ - ...defaultGraphViewLayout, - activeSidebarItem: "styles", - }); - - expect(tabB.read()).toStrictEqual(tabBLayout); - }); - test("a later tab cold-starts to the view layout an earlier tab wrote, with toggles rebuilt as a Set", async () => { const earlierTab = await openGraphViewTab(); const written: GraphViewLayout = { @@ -515,23 +487,6 @@ describe("schema view layout across tabs", () => { codec: schemaViewLayoutCodec, }); - test("changing the view layout in one tab does not change an already-open tab", async () => { - const tabB = await openSchemaViewTab(); - const tabBLayout: SchemaViewLayout = { - ...defaultSchemaViewLayout, - activeSidebarItem: "styles", - }; - await tabB.write(tabBLayout); - - const tabA = await openSchemaViewTab(); - await tabA.write({ - ...defaultSchemaViewLayout, - activeSidebarItem: "details", - }); - - expect(tabB.read()).toStrictEqual(tabBLayout); - }); - test("a later tab cold-starts to the view layout an earlier tab wrote", async () => { const earlierTab = await openSchemaViewTab(); const written: SchemaViewLayout = { From fe557dc7be6408f1843034d9c429054204350061 Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 20:18:15 -0500 Subject: [PATCH 6/6] Keep the cross-tab isolation tests and subscribe the open tab Restores the already-open tab tests removed earlier. They could not fail because the open tab was never mounted, so a cross-tab sync wired through onMount would not reach it. Subscribing the tab first makes each test fail under such a sync, which was confirmed by injecting one. --- .../activeConnectionStorage.test.ts | 6 +++ .../sessionScopedStorage.test.ts | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/graph-explorer/src/core/StateProvider/activeConnectionStorage.test.ts b/packages/graph-explorer/src/core/StateProvider/activeConnectionStorage.test.ts index e26689ebc..e696e2eda 100644 --- a/packages/graph-explorer/src/core/StateProvider/activeConnectionStorage.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/activeConnectionStorage.test.ts @@ -26,6 +26,11 @@ async function openTab() { let atom = await createActiveConnectionIdAtom({ sessionStorage }); return { read: () => store.get(atom), + /** + * Mounts the atom the way a rendered component does, so a cross-tab sync + * wired through `onMount` would reach this tab. + */ + subscribe: () => store.sub(atom, () => {}), /** Activates a connection; resolves once the breadcrumb has landed. */ activate: (id: ReturnType | null) => { store.set(atom, id); @@ -144,6 +149,7 @@ describe("activeConnectionStorage across tabs", () => { test("activating a connection in one tab does not change an already-open tab", async () => { const tabB = await openTab(); + tabB.subscribe(); const tabBConnection = createConnectionId(); await tabB.activate(tabBConnection); diff --git a/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts b/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts index 095603b0a..96eae2ce8 100644 --- a/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/sessionScopedStorage.test.ts @@ -58,6 +58,11 @@ function tabOpener( let atom = await createSessionScopedAtom({ ...options, sessionStorage }); return { read: () => store.get(atom), + /** + * Mounts the atom the way a rendered component does, so a cross-tab sync + * wired through `onMount` would reach this tab. + */ + subscribe: () => store.sub(atom, () => {}), write: (value: T) => { store.set(atom, value); return persistenceStatusStore.waitForIdle(); @@ -317,6 +322,17 @@ describe("createSessionScopedAtom", () => { }); describe("createSessionScopedAtom across tabs", () => { + test("writing in one tab does not change an already-open tab", async () => { + const tabB = await openTab(); + tabB.subscribe(); + await tabB.write({ count: 2 }); + + const tabA = await openTab(); + await tabA.write({ count: 99 }); + + expect(tabB.read()).toStrictEqual({ count: 2 }); + }); + test("a tab opened later cold-starts to the value an earlier tab wrote", async () => { const earlierTab = await openTab(); await earlierTab.write({ count: 3 }); @@ -459,6 +475,25 @@ describe("backward compatibility: graph view layout breadcrumb", () => { // the array serialization across a write-in-one-tab / cold-start-in-another // sequence, which the toy counter codec above cannot reach. describe("graph view layout across tabs", () => { + test("changing the view layout in one tab does not change an already-open tab", async () => { + const tabB = await openGraphViewTab(); + tabB.subscribe(); + const tabBLayout: GraphViewLayout = { + ...defaultGraphViewLayout, + activeSidebarItem: "filters", + activeToggles: new Set(["graph-viewer"]), + }; + await tabB.write(tabBLayout); + + const tabA = await openGraphViewTab(); + await tabA.write({ + ...defaultGraphViewLayout, + activeSidebarItem: "styles", + }); + + expect(tabB.read()).toStrictEqual(tabBLayout); + }); + test("a later tab cold-starts to the view layout an earlier tab wrote, with toggles rebuilt as a Set", async () => { const earlierTab = await openGraphViewTab(); const written: GraphViewLayout = { @@ -487,6 +522,24 @@ describe("schema view layout across tabs", () => { codec: schemaViewLayoutCodec, }); + test("changing the view layout in one tab does not change an already-open tab", async () => { + const tabB = await openSchemaViewTab(); + tabB.subscribe(); + const tabBLayout: SchemaViewLayout = { + ...defaultSchemaViewLayout, + activeSidebarItem: "styles", + }; + await tabB.write(tabBLayout); + + const tabA = await openSchemaViewTab(); + await tabA.write({ + ...defaultSchemaViewLayout, + activeSidebarItem: "details", + }); + + expect(tabB.read()).toStrictEqual(tabBLayout); + }); + test("a later tab cold-starts to the view layout an earlier tab wrote", async () => { const earlierTab = await openSchemaViewTab(); const written: SchemaViewLayout = {