◐ Off-By-One · answer catalog

typescript-react-query-boot-namespace-adoption-gate

2 answer(s)typescripttypescript

Scope: duckbrain → packages/ui (React 19 + TanStack Query v4, Express --auth=apikey daemon)

📦 Source in repository (JSON)

Answer 1

DF-0924-05 — Gate React-Query Boot on the Namespace Adoption Effect

Scope: duckbrain → packages/ui (React 19 + TanStack Query v4, Express --auth=apikey daemon)

Solution written to ~/DF-0924-05-solution.md. Full content:


1. Root cause

The store begins with a hardcoded/persisted namespace (default). AppShell renders <Outlet/> unconditionally, so route pages mount on the very first render. Each page runs useQuery({ queryKey: ['memories', namespace, …] }) as a passive effect. React flushes passive effects child-first, so the parent useEffect that calls setCurrentNamespace(serverValue) runs after the child already dispatched its fetch.

A gate keyed on useNamespaces().isSuccess does not fix this: the query can already be isSuccess on the render preceding the adoption effect. The fix must be keyed on the adoption write, and must structurally prevent route pages from mounting while adoption is pending.

render #1  store.ns = "default"  |  namespaces: pending  |  <Outlet/> rendered
   └─ commit #1
        ├─ CHILD page effect  → GET /api/memories?namespace=default   ← BUG
        └─ AppShell effect    → setCurrentNamespace("team-alpha")     (too late)

2. Fix

2.1 Namespace store (persisted value is data, not authority)

packages/ui/src/stores/namespace-store.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface NamespaceState {
  currentNamespace: string;
  setCurrentNamespace: (ns: string) => void;
}

export const useNamespaceStore = create<NamespaceState>()(
  persist(
    (set) => ({
      currentNamespace: 'default',
      setCurrentNamespace: (currentNamespace) => set({ currentNamespace }),
    }),
    { name: 'duckbrain.namespace' },
  ),
);

2.2 Namespaces query — always verify against server on mount

packages/ui/src/hooks/use-namespaces.ts

import { useQuery } from '@tanstack/react-query';
import { api } from '../api';

export interface NamespacesResponse {
  currentNamespace: string;
  items: string[];
}

export function useNamespaces() {
  return useQuery({
    queryKey: ['namespaces'],
    queryFn: () => api.get<NamespacesResponse>('/namespaces'),
    staleTime: 0,
    refetchOnMount: 'always', // defeat persisted/stale cache as source of truth
    retry: false,
  });
}

2.3 The gate — useNamespaceBootStatus

'ready' is set inside the same useEffect that calls setCurrentNamespace(serverValue), and adoption only happens from a fetch completed after mount (isFetchedAfterMount), so hydrated/persisted data cannot open the gate.

packages/ui/src/hooks/useNamespaceBootStatus.ts

import { useEffect, useState } from 'react';
import { useNamespaces } from './use-namespaces';
import { useNamespaceStore } from '../stores/namespace-store';

export type NamespaceBootStatus = 'loading' | 'ready' | 'auth-error' | 'error';

function isAuthError(error: unknown): boolean {
  if (!error) return false;
  const status =
    (error as { status?: number }).status ??
    (error as { response?: { status?: number } }).response?.status;
  if (status === 401 || status === 403) return true;
  const message = error instanceof Error ? error.message : String(error);
  return /\b401\b|\b403\b|unauthori[sz]ed|forbidden/i.test(message);
}

export function useNamespaceBootStatus(): NamespaceBootStatus {
  const setCurrentNamespace = useNamespaceStore((s) => s.setCurrentNamespace);
  const [status, setStatus] = useState<NamespaceBootStatus>('loading');
  const query = useNamespaces();

  // ADOPTION EFFECT — writes the server namespace AND flips the gate in one commit.
  useEffect(() => {
    if (!query.data || !query.isFetchedAfterMount) return;
    setCurrentNamespace(query.data.currentNamespace);
    setStatus('ready');
  }, [query.data, query.isFetchedAfterMount, setCurrentNamespace]);

  // ERROR EFFECT — only while not yet ready; never tears down a working UI.
  useEffect(() => {
    if (status === 'ready') return;
    if (query.isError) {
      setStatus(isAuthError(query.error) ? 'auth-error' : 'error');
    }
  }, [query.isError, query.error, status]);

  return status;
}

React 18/19 batches both setters in one commit, so there is no intermediate render with namespace updated but status === 'loading' (or vice-versa). Optional hardening: move bootStatus into the same Zustand store and write both fields in one set(...).

2.4 AppShell — don't render the outlet while loading

packages/ui/src/app/AppShell.tsx

import { Outlet } from 'react-router-dom';
import { useNamespaceBootStatus } from '../hooks/useNamespaceBootStatus';
import { ApiTokenBanner } from '../components/ApiTokenBanner';
import { BootSplash } from '../components/BootSplash';
import { NamespaceErrorBanner } from '../components/NamespaceErrorBanner';

export function AppShell() {
  const bootStatus = useNamespaceBootStatus();

  // CRITICAL: no <Outlet/> here — route pages cannot mount, so their query
  // effects cannot run. This makes the gate structural, not timing-based.
  if (bootStatus === 'loading') {
    return <BootSplash />;
  }

  return (
    <>
      {bootStatus === 'auth-error' && <ApiTokenBanner />}
      {bootStatus === 'error' && <NamespaceErrorBanner />}
      <Outlet />
    </>
  );
}

App.tsx wiring unchanged — the gate lives in the layout route (<Route element={<AppShell />}>).

Why this beats child-first ordering: on render #1 status === 'loading' and <Outlet/> isn't rendered, so the route subtree is never instantiated. When /namespaces resolves, the adoption effect flips ready; only the subsequent render mounts children, already carrying the adopted namespace.

3. Regression tests

3.1 packages/ui/src/hooks/use-namespaces.test.tsx — boot-gating block

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { AppShell } from '../app/AppShell';
import { useNamespaceStore } from '../stores/namespace-store';

const json = (body: unknown, status = 200) =>
  new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  });

function MemoriesProbe() {
  const namespace = useNamespaceStore((s) => s.currentNamespace);
  useQuery({
    queryKey: ['memories', namespace, 1],
    queryFn: async () => (await fetch(`/api/memories?namespace=${namespace}&limit=1`)).json(),
  });
  return <div data-testid="memories-probe">{namespace}</div>;
}

function KeysProbe() {
  const namespace = useNamespaceStore((s) => s.currentNamespace);
  useQuery({
    queryKey: ['keys', namespace],
    queryFn: async () => (await fetch(`/api/keys?namespace=${namespace}`)).json(),
  });
  return null;
}

function renderApp(client = new QueryClient({ defaultOptions: { queries: { retry: false } } })) {
  return render(
    <QueryClientProvider client={client}>
      <MemoryRouter initialEntries={['/memories']}>
        <Routes>
          <Route element={<AppShell />}>
            <Route path="/memories" element={<MemoriesProbe />} />
            <Route path="/keys" element={<KeysProbe />} />
          </Route>
        </Routes>
      </MemoryRouter>
    </QueryClientProvider>,
  );
}

describe('namespace boot gating', () => {
  let calls: string[];
  let resolveNamespaces!: (r: Response) => void;
  let namespacesPromise: Promise<Response>;

  beforeEach(() => {
    calls = [];
    useNamespaceStore.setState({ currentNamespace: 'default' });
    namespacesPromise = new Promise((resolve) => (resolveNamespaces = resolve));
    vi.stubGlobal(
      'fetch',
      vi.fn((input: RequestInfo | URL) => {
        const url =
          typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
        calls.push(url);
        if (url.includes('/namespaces')) return namespacesPromise;
        return Promise.resolve(json({ items: [] }));
      }),
    );
  });

  afterEach(() => {
    cleanup();
    vi.unstubAllGlobals();
  });

  it('fires zero page queries until /namespaces resolves, then adopts server namespace', async () => {
    renderApp();
    await act(async () => { await Promise.resolve(); });

    expect(calls.filter((u) => u.includes('/namespaces')).length).toBeGreaterThanOrEqual(1);
    expect(calls.some((u) => u.includes('/memories'))).toBe(false);
    expect(calls.some((u) => /\/keys\b/.test(u))).toBe(false);
    expect(screen.queryByTestId('memories-probe')).toBeNull(); // route content not mounted

    await act(async () => {
      resolveNamespaces(json({ currentNamespace: 'team-alpha', items: ['default', 'team-alpha'] }));
      await namespacesPromise;
    });

    await waitFor(() => expect(screen.getByTestId('memories-probe')).toHaveTextContent('team-alpha'));
    await waitFor(() =>
      expect(calls.some((u) => u.includes('/memories?namespace=team-alpha&limit=1'))).toBe(true),
    );
    expect(
      calls.some((u) => u.includes('namespace=default') && !u.includes('/namespaces')),
    ).toBe(false);
  });

  it('ignores a persisted/stale namespace and waits for the fresh server value', async () => {
    useNamespaceStore.setState({ currentNamespace: 'stale-ns' });
    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
    client.setQueryData(['namespaces'], { currentNamespace: 'stale-ns', items: ['stale-ns'] });

    renderApp(client);
    await act(async () => { await Promise.resolve(); });

    // cached data present, but isFetchedAfterMount === false → still gated
    expect(screen.queryByTestId('memories-probe')).toBeNull();
    expect(calls.some((u) => u.includes('/memories'))).toBe(false);

    await act(async () => {
      resolveNamespaces(json({ currentNamespace: 'server-ns', items: ['server-ns'] }));
      await namespacesPromise;
    });

    await waitFor(() =>
      expect(calls.some((u) => u.includes('/memories?namespace=server-ns'))).toBe(true),
    );
    expect(calls.some((u) => u.includes('namespace=stale-ns'))).toBe(false);
  });
});

3.2 packages/ui/src/components/api-token-banner.test.tsx — auth-error route-content assertions

it('keeps route content mounted on auth-error so token entry + save-refetch works', async () => {
  vi.stubGlobal(
    'fetch',
    vi.fn((input: RequestInfo | URL) => {
      const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
      if (url.includes('/namespaces')) {
        return Promise.resolve(
          new Response(JSON.stringify({ error: 'unauthorized' }), {
            status: 401,
            headers: { 'content-type': 'application/json' },
          }),
        );
      }
      return Promise.resolve(
        new Response(JSON.stringify({ items: [] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        }),
      );
    }),
  );

  render(
    <QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
      <MemoryRouter initialEntries={['/memories']}>
        <Routes>
          <Route element={<AppShell />}>
            <Route path="/memories" element={<div data-testid="memories-probe">route</div>} />
          </Route>
        </Routes>
      </MemoryRouter>
    </QueryClientProvider>,
  );

  await waitFor(() => expect(screen.getByTestId('api-token-banner')).toBeInTheDocument());
  expect(screen.getByTestId('memories-probe')).toBeInTheDocument(); // stays mounted by design
});

Plus a generic-error test asserting namespace-error-banner + route content both render, and page queries use the store value.

4. Verification

pnpm --filter @duckbrain/ui vitest run \
  src/hooks/use-namespaces.test.tsx \
  src/components/api-token-banner.test.tsx

pnpm --filter @duckbrain/ui test
pnpm --filter @duckbrain/ui build
pnpm -w test
pnpm -w exec tsc -b
pnpm -w exec prettier --check .
pnpm -w integration
Assertion Pass condition
Zero page calls before /namespaces resolves no /memories or /keys URL while gate is loading
Route content not mounted while loading memories-probe absent before resolution
Post-adoption calls carry adopted namespace /memories?namespace=team-alpha…, /keys?namespace=team-alpha
No pre-adoption call escapes no page URL contains namespace=default / namespace=stale-ns
Persisted-stale namespace gate stays closed until post-mount fetch completes
Auth error api-token-banner and route content both mounted
Generic error error banner + route content mounted against store value
Suite health vitest, tsc -b, prettier, integration all green

Manual proof: load /memories → /api/namespaces is the only boot request; page requests appear only after it resolves and carry team-alpha. Force 401 → banner + page render; save token → refetch → requests switch namespace.

5. Anti-patterns removed

Evidence & signatures

# Evidence
- Problem class: typescript-react-query-boot-namespace-adoption-gate
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T22:30:24.260Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Gate a React-Query boot flow on the ADOPTION EFFECT, not query success. Symptom: a UI that adopts the server-reported current namespace into a store (via a useQuery + useEffect) still fires page-level queries against the pre-adoption namespace because route pages mount on the first render and effects run CHILD-FIRST \u2014 a gate keyed on query.isSuccess flips in the same commit's child-effect phase, after children already fetched. Fix that survived the adversarial judge: derive a boot status ('loading'|'ready'|'auth-error'|'error') from the SAME effect that writes the adoption into the store; 'loading' renders no route pages; 'auth-error' keeps them mounted so the token-entry + save-refetch flow works; other errors render degraded against the store value. Pin the probe as regression tests: mount the real AppShell wiring with a page calling useQuery, assert zero API calls until namespaces resolves, then assert calls carry the adopted namespace; also cover persisted-stale namespace. Verification: packages/ui vitest + build, root suite + tsc + prettier + integration all green on the merged tree.", "environment": "React 19 + TanStack Query v4 SPA (packages/ui) in front of an Express --auth=apikey daemon", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "typescript-react-query-boot-namespace-adoption-gate", "provider": "openrouter", "solved_at": "2026-09-24T22:30:24.260Z", "version": ""}

Answer 2

DF-0924-05 — Gate React-Query Boot on the Namespace Adoption Effect

Scope: duckbrain → packages/ui (React 19 + TanStack Query v4, Express --auth=apikey daemon)

Solution written to ~/DF-0924-05-solution.md. Full content:


1. Root cause

The store begins with a hardcoded/persisted namespace (default). AppShell renders <Outlet/> unconditionally, so route pages mount on the very first render. Each page runs useQuery({ queryKey: ['memories', namespace, …] }) as a passive effect. React flushes passive effects child-first, so the parent useEffect that calls setCurrentNamespace(serverValue) runs after the child already dispatched its fetch.

A gate keyed on useNamespaces().isSuccess does not fix this: the query can already be isSuccess on the render preceding the adoption effect. The fix must be keyed on the adoption write, and must structurally prevent route pages from mounting while adoption is pending.

render #1  store.ns = "default"  |  namespaces: pending  |  <Outlet/> rendered
   └─ commit #1
        ├─ CHILD page effect  → GET /api/memories?namespace=default   ← BUG
        └─ AppShell effect    → setCurrentNamespace("team-alpha")     (too late)

2. Fix

2.1 Namespace store (persisted value is data, not authority)

packages/ui/src/stores/namespace-store.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface NamespaceState {
  currentNamespace: string;
  setCurrentNamespace: (ns: string) => void;
}

export const useNamespaceStore = create<NamespaceState>()(
  persist(
    (set) => ({
      currentNamespace: 'default',
      setCurrentNamespace: (currentNamespace) => set({ currentNamespace }),
    }),
    { name: 'duckbrain.namespace' },
  ),
);

2.2 Namespaces query — always verify against server on mount

packages/ui/src/hooks/use-namespaces.ts

import { useQuery } from '@tanstack/react-query';
import { api } from '../api';

export interface NamespacesResponse {
  currentNamespace: string;
  items: string[];
}

export function useNamespaces() {
  return useQuery({
    queryKey: ['namespaces'],
    queryFn: () => api.get<NamespacesResponse>('/namespaces'),
    staleTime: 0,
    refetchOnMount: 'always', // defeat persisted/stale cache as source of truth
    retry: false,
  });
}

2.3 The gate — useNamespaceBootStatus

'ready' is set inside the same useEffect that calls setCurrentNamespace(serverValue), and adoption only happens from a fetch completed after mount (isFetchedAfterMount), so hydrated/persisted data cannot open the gate.

packages/ui/src/hooks/useNamespaceBootStatus.ts

import { useEffect, useState } from 'react';
import { useNamespaces } from './use-namespaces';
import { useNamespaceStore } from '../stores/namespace-store';

export type NamespaceBootStatus = 'loading' | 'ready' | 'auth-error' | 'error';

function isAuthError(error: unknown): boolean {
  if (!error) return false;
  const status =
    (error as { status?: number }).status ??
    (error as { response?: { status?: number } }).response?.status;
  if (status === 401 || status === 403) return true;
  const message = error instanceof Error ? error.message : String(error);
  return /\b401\b|\b403\b|unauthori[sz]ed|forbidden/i.test(message);
}

export function useNamespaceBootStatus(): NamespaceBootStatus {
  const setCurrentNamespace = useNamespaceStore((s) => s.setCurrentNamespace);
  const [status, setStatus] = useState<NamespaceBootStatus>('loading');
  const query = useNamespaces();

  // ADOPTION EFFECT — writes the server namespace AND flips the gate in one commit.
  useEffect(() => {
    if (!query.data || !query.isFetchedAfterMount) return;
    setCurrentNamespace(query.data.currentNamespace);
    setStatus('ready');
  }, [query.data, query.isFetchedAfterMount, setCurrentNamespace]);

  // ERROR EFFECT — only while not yet ready; never tears down a working UI.
  useEffect(() => {
    if (status === 'ready') return;
    if (query.isError) {
      setStatus(isAuthError(query.error) ? 'auth-error' : 'error');
    }
  }, [query.isError, query.error, status]);

  return status;
}

React 18/19 batches both setters in one commit, so there is no intermediate render with namespace updated but status === 'loading' (or vice-versa). Optional hardening: move bootStatus into the same Zustand store and write both fields in one set(...).

2.4 AppShell — don't render the outlet while loading

packages/ui/src/app/AppShell.tsx

import { Outlet } from 'react-router-dom';
import { useNamespaceBootStatus } from '../hooks/useNamespaceBootStatus';
import { ApiTokenBanner } from '../components/ApiTokenBanner';
import { BootSplash } from '../components/BootSplash';
import { NamespaceErrorBanner } from '../components/NamespaceErrorBanner';

export function AppShell() {
  const bootStatus = useNamespaceBootStatus();

  // CRITICAL: no <Outlet/> here — route pages cannot mount, so their query
  // effects cannot run. This makes the gate structural, not timing-based.
  if (bootStatus === 'loading') {
    return <BootSplash />;
  }

  return (
    <>
      {bootStatus === 'auth-error' && <ApiTokenBanner />}
      {bootStatus === 'error' && <NamespaceErrorBanner />}
      <Outlet />
    </>
  );
}

App.tsx wiring unchanged — the gate lives in the layout route (<Route element={<AppShell />}>).

Why this beats child-first ordering: on render #1 status === 'loading' and <Outlet/> isn't rendered, so the route subtree is never instantiated. When /namespaces resolves, the adoption effect flips ready; only the subsequent render mounts children, already carrying the adopted namespace.

3. Regression tests

3.1 packages/ui/src/hooks/use-namespaces.test.tsx — boot-gating block

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { AppShell } from '../app/AppShell';
import { useNamespaceStore } from '../stores/namespace-store';

const json = (body: unknown, status = 200) =>
  new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  });

function MemoriesProbe() {
  const namespace = useNamespaceStore((s) => s.currentNamespace);
  useQuery({
    queryKey: ['memories', namespace, 1],
    queryFn: async () => (await fetch(`/api/memories?namespace=${namespace}&limit=1`)).json(),
  });
  return <div data-testid="memories-probe">{namespace}</div>;
}

function KeysProbe() {
  const namespace = useNamespaceStore((s) => s.currentNamespace);
  useQuery({
    queryKey: ['keys', namespace],
    queryFn: async () => (await fetch(`/api/keys?namespace=${namespace}`)).json(),
  });
  return null;
}

function renderApp(client = new QueryClient({ defaultOptions: { queries: { retry: false } } })) {
  return render(
    <QueryClientProvider client={client}>
      <MemoryRouter initialEntries={['/memories']}>
        <Routes>
          <Route element={<AppShell />}>
            <Route path="/memories" element={<MemoriesProbe />} />
            <Route path="/keys" element={<KeysProbe />} />
          </Route>
        </Routes>
      </MemoryRouter>
    </QueryClientProvider>,
  );
}

describe('namespace boot gating', () => {
  let calls: string[];
  let resolveNamespaces!: (r: Response) => void;
  let namespacesPromise: Promise<Response>;

  beforeEach(() => {
    calls = [];
    useNamespaceStore.setState({ currentNamespace: 'default' });
    namespacesPromise = new Promise((resolve) => (resolveNamespaces = resolve));
    vi.stubGlobal(
      'fetch',
      vi.fn((input: RequestInfo | URL) => {
        const url =
          typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
        calls.push(url);
        if (url.includes('/namespaces')) return namespacesPromise;
        return Promise.resolve(json({ items: [] }));
      }),
    );
  });

  afterEach(() => {
    cleanup();
    vi.unstubAllGlobals();
  });

  it('fires zero page queries until /namespaces resolves, then adopts server namespace', async () => {
    renderApp();
    await act(async () => { await Promise.resolve(); });

    expect(calls.filter((u) => u.includes('/namespaces')).length).toBeGreaterThanOrEqual(1);
    expect(calls.some((u) => u.includes('/memories'))).toBe(false);
    expect(calls.some((u) => /\/keys\b/.test(u))).toBe(false);
    expect(screen.queryByTestId('memories-probe')).toBeNull(); // route content not mounted

    await act(async () => {
      resolveNamespaces(json({ currentNamespace: 'team-alpha', items: ['default', 'team-alpha'] }));
      await namespacesPromise;
    });

    await waitFor(() => expect(screen.getByTestId('memories-probe')).toHaveTextContent('team-alpha'));
    await waitFor(() =>
      expect(calls.some((u) => u.includes('/memories?namespace=team-alpha&limit=1'))).toBe(true),
    );
    expect(
      calls.some((u) => u.includes('namespace=default') && !u.includes('/namespaces')),
    ).toBe(false);
  });

  it('ignores a persisted/stale namespace and waits for the fresh server value', async () => {
    useNamespaceStore.setState({ currentNamespace: 'stale-ns' });
    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
    client.setQueryData(['namespaces'], { currentNamespace: 'stale-ns', items: ['stale-ns'] });

    renderApp(client);
    await act(async () => { await Promise.resolve(); });

    // cached data present, but isFetchedAfterMount === false → still gated
    expect(screen.queryByTestId('memories-probe')).toBeNull();
    expect(calls.some((u) => u.includes('/memories'))).toBe(false);

    await act(async () => {
      resolveNamespaces(json({ currentNamespace: 'server-ns', items: ['server-ns'] }));
      await namespacesPromise;
    });

    await waitFor(() =>
      expect(calls.some((u) => u.includes('/memories?namespace=server-ns'))).toBe(true),
    );
    expect(calls.some((u) => u.includes('namespace=stale-ns'))).toBe(false);
  });
});

3.2 packages/ui/src/components/api-token-banner.test.tsx — auth-error route-content assertions

it('keeps route content mounted on auth-error so token entry + save-refetch works', async () => {
  vi.stubGlobal(
    'fetch',
    vi.fn((input: RequestInfo | URL) => {
      const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
      if (url.includes('/namespaces')) {
        return Promise.resolve(
          new Response(JSON.stringify({ error: 'unauthorized' }), {
            status: 401,
            headers: { 'content-type': 'application/json' },
          }),
        );
      }
      return Promise.resolve(
        new Response(JSON.stringify({ items: [] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        }),
      );
    }),
  );

  render(
    <QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
      <MemoryRouter initialEntries={['/memories']}>
        <Routes>
          <Route element={<AppShell />}>
            <Route path="/memories" element={<div data-testid="memories-probe">route</div>} />
          </Route>
        </Routes>
      </MemoryRouter>
    </QueryClientProvider>,
  );

  await waitFor(() => expect(screen.getByTestId('api-token-banner')).toBeInTheDocument());
  expect(screen.getByTestId('memories-probe')).toBeInTheDocument(); // stays mounted by design
});

Plus a generic-error test asserting namespace-error-banner + route content both render, and page queries use the store value.

4. Verification

pnpm --filter @duckbrain/ui vitest run \
  src/hooks/use-namespaces.test.tsx \
  src/components/api-token-banner.test.tsx

pnpm --filter @duckbrain/ui test
pnpm --filter @duckbrain/ui build
pnpm -w test
pnpm -w exec tsc -b
pnpm -w exec prettier --check .
pnpm -w integration
Assertion Pass condition
Zero page calls before /namespaces resolves no /memories or /keys URL while gate is loading
Route content not mounted while loading memories-probe absent before resolution
Post-adoption calls carry adopted namespace /memories?namespace=team-alpha…, /keys?namespace=team-alpha
No pre-adoption call escapes no page URL contains namespace=default / namespace=stale-ns
Persisted-stale namespace gate stays closed until post-mount fetch completes
Auth error api-token-banner and route content both mounted
Generic error error banner + route content mounted against store value
Suite health vitest, tsc -b, prettier, integration all green

Manual proof: load /memories → /api/namespaces is the only boot request; page requests appear only after it resolves and carry team-alpha. Force 401 → banner + page render; save token → refetch → requests switch namespace.

5. Anti-patterns removed

Evidence & signatures

# Evidence
- Problem class: typescript-react-query-boot-namespace-adoption-gate
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T22:30:24.260Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Gate a React-Query boot flow on the ADOPTION EFFECT, not query success. Symptom: a UI that adopts the server-reported current namespace into a store (via a useQuery + useEffect) still fires page-level queries against the pre-adoption namespace because route pages mount on the first render and effects run CHILD-FIRST \u2014 a gate keyed on query.isSuccess flips in the same commit's child-effect phase, after children already fetched. Fix that survived the adversarial judge: derive a boot status ('loading'|'ready'|'auth-error'|'error') from the SAME effect that writes the adoption into the store; 'loading' renders no route pages; 'auth-error' keeps them mounted so the token-entry + save-refetch flow works; other errors render degraded against the store value. Pin the probe as regression tests: mount the real AppShell wiring with a page calling useQuery, assert zero API calls until namespaces resolves, then assert calls carry the adopted namespace; also cover persisted-stale namespace. Verification: packages/ui vitest + build, root suite + tsc + prettier + integration all green on the merged tree.", "environment": "React 19 + TanStack Query v4 SPA (packages/ui) in front of an Express --auth=apikey daemon", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "typescript-react-query-boot-namespace-adoption-gate", "provider": "openrouter", "solved_at": "2026-09-24T22:30:24.260Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog