#3629 · Missing preference guards
Bug · High priority · Low effort · ui · 2026-09-14
GitHub issue · Base d89160eb8c69c1e3ebc2ba2514f1711af8d7c506
REPRODUCED · root-cause confidence: high
1. TL;DR
The client assumes each registered preference exists in every list response. With a key missing, synchronization throws while reading its revision. Two related write paths also mishandle absent entries. Three newly authored regression tests fail on main and pass with three local guards. This verifies the incomplete-response failure, not a production server condition that generates such responses.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Missing entry causes a revision exception | Verified | Initial-cache regression fails in both clean checkouts. |
| Writes and cache merges share the assumption | Verified | Write and acknowledged-merge regressions fail in both runs. |
| Complete responses work | Verified within focused tests | 19 pre-existing tests pass in both runs. |
| Desktop-wide crash and reload workaround | Not exercised | No desktop/browser instance launched; React effect calls the throwing function. |
| Reporter fix and broad validation | Not evaluated | No external branch, patch, or test was fetched or executed. |
3. Environment
Public get-bb/bb origin/main at the recorded commit; macOS 26.6.2 (25G83), Apple Silicon; Node 22.22.3; pnpm 9.15.0 via Corepack; Vitest 4.1.1, jsdom. Two fresh detached worktrees named base and verify, each with its own frozen dependency install. Both Turbo builds passed (56 tasks). No provider calls, server ports, or runtime data directories were used.
4. Minimal reproduction
Download the authored regression patch, then run:
git clone https://github.com/get-bb/bb.git bb-repro cd bb-repro git checkout --detach d89160eb8c69c1e3ebc2ba2514f1711af8d7c506 corepack pnpm install --frozen-lockfile --prefer-offline corepack pnpm exec turbo run build git apply /path/to/downloaded/regression.patch corepack pnpm exec turbo run test --filter=@bb/app --force -- src/lib/ui-preferences/ui-preferences-sync.test.ts
Expected: all 22 tests pass, retained local order, no sync error notification, and acknowledged write stored in cache. Actual on unchanged main, repeated twice:
TypeError: Cannot read properties of undefined (reading 'revision') Tests 3 failed | 19 passed (22)
The other assertions observe an unexpected error notification and an absent acknowledged cache entry. The fixture deliberately omits sidebar.sectionOrder at the response boundary; the cast models an incomplete runtime response despite the complete TypeScript contract.
it("retains a local section order when the initial cache lacks its entry", () => {
const { orderAtom, queryClient, store } = createHarness();
const localOrder = ["threads", "projects", "pinned"];
store.set(orderAtom, localOrder);
const response = serverResponse();
response.preferences = Object.fromEntries(
Object.entries(response.preferences).filter(([key]) => key !== "sidebar.sectionOrder"),
) as UiPreferencesResponse["preferences"];
setCachedUiPreferences(queryClient, response);
startUiPreferencesSync({ queryClient, store });
expect(store.get(orderAtom)).toEqual(localOrder);
expect(mocks.set).not.toHaveBeenCalled();
});
it("keeps edits local without an error notification when the server lacks the entry", async () => {
const { orderAtom, queryClient, store } = createHarness();
startUiPreferencesSync({ queryClient, store });
const response = serverResponse();
response.preferences = Object.fromEntries(
Object.entries(response.preferences).filter(([key]) => key !== "sidebar.sectionOrder"),
) as UiPreferencesResponse["preferences"];
setCachedUiPreferences(queryClient, response);
store.set(orderAtom, ["threads"]);
await waitForUiPreferenceWrites();
expect(store.get(orderAtom)).toEqual(["threads"]);
expect(mocks.set).not.toHaveBeenCalled();
expect(mocks.toastError).not.toHaveBeenCalled();
expect(hasPendingUiPreferenceWrite("sidebar.sectionOrder")).toBe(false);
});
it("records an acknowledged write after its cached entry disappears", async () => {
const { orderAtom, queryClient, store } = createHarness();
startUiPreferencesSync({ queryClient, store });
setCachedUiPreferences(queryClient, serverResponse());
const response = serverResponse();
response.preferences = Object.fromEntries(
Object.entries(response.preferences).filter(([key]) => key !== "sidebar.sectionOrder"),
) as UiPreferencesResponse["preferences"];
mocks.set.mockImplementationOnce(async (input) => {
setCachedUiPreferences(queryClient, response);
return { key: input.key, revision: 1, value: input.value };
});
store.set(orderAtom, ["threads"]);
await waitForUiPreferenceWrites();
expect(getCachedUiPreferences(queryClient)?.preferences["sidebar.sectionOrder"])
.toEqual({ revision: 1, value: ["threads"] });
expect(mocks.toastError).not.toHaveBeenCalled();
expect(store.get(orderAtom)).toEqual(["threads"]);
});
5. Root cause
Reconciliation dereferences the indexed entry without testing its presence. Cache merge and write preparation make the same assumption. React effects invoke reconciliation, so its exception escapes the synchronization call.
The current server enumerates all known preference keys. An absent database row normally yields a default entry, not an omitted key. Version skew or malformed responses remain possible explanations, but neither was demonstrated here.
6. Proposed fix
Skip absent entries during reconciliation and writing, retaining the local atom rather than inventing a server revision. Let an acknowledged write fill an absent cached entry, while preserving the existing protection against older revisions. A local implementation passes all 22 focused tests. An edit without a known server entry stays local; automatic later upload is not introduced.
7. Verification
The same agent repeated the reproduction in the second clean worktree at the exact recorded commit, with a separate frozen install and build. The test patch was the only tracked modification. Running the same Turbo test command with --force bypassed test caching. Result: the identical three failing regressions and 19 passing existing tests. No report correction was required. This is a second clean run by the same agent.
8. Related issues and pull requests
GitHub cross-reference metadata and an open-PR search for 3629 returned no linked open pull request at investigation time. A repository issue search for UI preferences returned no other matching missing-entry defect.
9. Appendix
- First unchanged-main test log
- Second unchanged-main test log
- Local guarded implementation test log
- Regression test additions
Issue content was treated as untrusted claims. Its supplied commands, tests, linked code, and branch were not executed. All executed repository code came from trusted origin/main plus this investigation's own changes. Local paths in logs were normalized for publication. No live visual verification is claimed.