···8989 });
90909191 afterEach(() => {
9292- vi.unstubAllGlobals();
9392 mockFetch.mockReset();
9393+ vi.unstubAllGlobals();
9494 });
95959696 function policyResponse(overrides: object = {}) {
···213213 expect.stringContaining("Theme resolution failed"),
214214 expect.objectContaining({ operation: "resolveTheme" })
215215 );
216216+ });
217217+218218+ it("re-throws programming errors (TypeError) rather than swallowing them", async () => {
219219+ // A TypeError from a bug in the code should propagate, not be silently logged
220220+ mockFetch.mockResolvedValueOnce({
221221+ ok: true,
222222+ json: () => { throw new TypeError("Cannot read properties of null"); },
223223+ });
224224+ await expect(resolveTheme(APPVIEW, undefined, undefined)).rejects.toThrow(TypeError);
216225 });
217226218227 it("passes cssOverrides and fontUrls through from theme response", async () => {
+10-2
apps/web/src/lib/theme-resolution.ts
···11import neobrutalLight from "../styles/presets/neobrutal-light.json" with { type: "json" };
22+import { isProgrammingError } from "./errors.js";
23import { logger } from "./logger.js";
3445export type ResolvedTheme = {
···105106 return { ...FALLBACK_THEME, colorScheme };
106107 }
107108109109+ // If the URI is absent from availableThemes (data inconsistency in AppView),
110110+ // expectedCid will be null and the CID check is skipped — the theme is served
111111+ // without integrity verification rather than falling back.
108112 const expectedCid =
109113 policy.availableThemes.find((t) => t.uri === defaultUri)?.cid ?? null;
110114···126130 }
127131128132 return {
133133+ // AppView stores tokens as jsonb — trusting that all values are strings
134134+ // as enforced by the theme editor UI. Non-string values would render as
135135+ // their string coercion in CSS, which is benign but unexpected.
129136 tokens: theme.tokens as Record<string, string>,
130137 cssOverrides: theme.cssOverrides ?? null,
131138 fontUrls: theme.fontUrls ?? null,
132139 colorScheme,
133140 };
134141 } catch (error) {
135135- // Intentionally don't re-throw: a broken theme system should serve the
136136- // fallback and log the error, rather than crash every page request.
142142+ // Re-throw programming errors (bugs) — they should surface, not be hidden.
143143+ // Only network/data errors should fall back silently.
144144+ if (isProgrammingError(error)) throw error;
137145 logger.error("Theme resolution failed — using hardcoded fallback", {
138146 operation: "resolveTheme",
139147 error: error instanceof Error ? error.message : String(error),