From def7156cdba5e7aebaf0481dcd56d2cc59990280 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 18:03:02 +0000 Subject: [PATCH 1/4] Fix CI: safe settings feature selector and resilient color scale The syncPolymerRunColorMap$ effect (added in PR #58) uses getRunColorMap which transitively depends on the settings feature selector via getColorPalette. In the karma bundle test, when the settings feature state is not registered in a particular test's MockStore, selectSettingsState returns undefined, causing 'TypeError: Cannot read property settings of undefined' in the memoized selector chain. Fix 1 - settings_selectors.ts: Wrap the bare createFeatureSelector with a createSelector that falls back to initialState when the feature state is undefined. This prevents crashes in any test that evaluates settings-dependent selectors without registering the settings feature. Fix 2 - colorScale.ts: The readColorMap() function threw when window.__tbRunColorMap was not set. During tests and before the first NgRx effect fires, this map is absent. Changed to return null gracefully, skip domain population when the map is absent, and return a neutral fallback color (#808080) from getColor instead of throwing when a run is not in the domain. Co-authored-by: Samuel --- .../components/tf_color_scale/colorScale.ts | 26 ++++++++----------- .../settings/_redux/settings_selectors.ts | 9 +++++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tensorbored/components/tf_color_scale/colorScale.ts b/tensorbored/components/tf_color_scale/colorScale.ts index 9b105263a04..5c81ade0a36 100644 --- a/tensorbored/components/tf_color_scale/colorScale.ts +++ b/tensorbored/components/tf_color_scale/colorScale.ts @@ -20,15 +20,11 @@ import {runsStore} from '../tf_backend/runsStore'; /** * Read the run-name → hex-color map from NgRx (exposed on window by * RunsEffects). This is the single source of truth used by time-series. + * Returns null when the map has not been seeded yet (e.g. during tests + * or before the first NgRx effect fires). */ -function readColorMap(): Record { - const live = (window as any).__tbRunColorMap as - | Record - | undefined; - if (!live) { - throw new Error('Missing run color map on window.__tbRunColorMap'); - } - return live; +function readColorMap(): Record | null { + return ((window as any).__tbRunColorMap as Record) ?? null; } export class ColorScale { @@ -36,25 +32,25 @@ export class ColorScale { public setDomain(strings: string[]): this { this.identifiers = d3.map(); - // Module-level initialization runs before the NgRx bridge seeds - // window.__tbRunColorMap. During that phase the run domain is empty. - // Keep strict behavior for non-empty domains only. if (strings.length === 0) { return this; } const stored = readColorMap(); + if (!stored) { + return this; + } strings.forEach((s) => { - if (stored[s] === undefined) { - throw new Error(`Missing run color for "${s}" in shared color map`); + const color = stored[s]; + if (color !== undefined) { + this.identifiers.set(s, color); } - this.identifiers.set(s, stored[s]); }); return this; } public getColor(s: string): string { if (!this.identifiers.has(s)) { - throw new Error(`String ${s} was not in the domain.`); + return '#808080'; } return this.identifiers.get(s) as string; } diff --git a/tensorbored/webapp/settings/_redux/settings_selectors.ts b/tensorbored/webapp/settings/_redux/settings_selectors.ts index 82ad9e287af..d279bef5f17 100644 --- a/tensorbored/webapp/settings/_redux/settings_selectors.ts +++ b/tensorbored/webapp/settings/_redux/settings_selectors.ts @@ -15,11 +15,16 @@ limitations under the License. import {createFeatureSelector, createSelector} from '@ngrx/store'; import {DataLoadState} from '../../types/data'; import {ColorPalette} from '../../util/colors'; -import {SettingsState, SETTINGS_FEATURE_KEY} from './settings_types'; +import {SettingsState, SETTINGS_FEATURE_KEY, initialState} from './settings_types'; -const selectSettingsState = +const selectSettingsStateRaw = createFeatureSelector(SETTINGS_FEATURE_KEY); +const selectSettingsState = createSelector( + selectSettingsStateRaw, + (state): SettingsState => state ?? initialState +); + export const getSettingsLoadState = createSelector( selectSettingsState, (state: SettingsState): DataLoadState => { From 795f6025bf2730529961dd282454942ae638f7c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 18:05:04 +0000 Subject: [PATCH 2/4] colorScale: restore palette fallback and keep getColor throwing Revert the overly-defensive approach. When window.__tbRunColorMap is not yet seeded (race between runsStore listener and the NgRx effect), fall back to the original static palette assignment so the domain is always fully populated and getColor throws for actual programming errors. When the shared color map IS available but a run is missing, log console.error so the problem is visible. Co-authored-by: Samuel --- .../components/tf_color_scale/colorScale.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/tensorbored/components/tf_color_scale/colorScale.ts b/tensorbored/components/tf_color_scale/colorScale.ts index 5c81ade0a36..f2eb498bc31 100644 --- a/tensorbored/components/tf_color_scale/colorScale.ts +++ b/tensorbored/components/tf_color_scale/colorScale.ts @@ -16,12 +16,12 @@ import * as d3 from 'd3'; import {BaseStore} from '../tf_backend/baseStore'; import {experimentsStore} from '../tf_backend/experimentsStore'; import {runsStore} from '../tf_backend/runsStore'; +import {standard} from './palettes'; /** - * Read the run-name → hex-color map from NgRx (exposed on window by - * RunsEffects). This is the single source of truth used by time-series. - * Returns null when the map has not been seeded yet (e.g. during tests - * or before the first NgRx effect fires). + * Read the run-name → hex-color map seeded on window by the NgRx + * RunsEffects syncPolymerRunColorMap$ effect. Returns null before the + * effect has fired for the first time. */ function readColorMap(): Record | null { return ((window as any).__tbRunColorMap as Record) ?? null; @@ -30,27 +30,37 @@ function readColorMap(): Record | null { export class ColorScale { private identifiers = d3.map(); + constructor(private readonly palette: string[] = standard) {} + public setDomain(strings: string[]): this { this.identifiers = d3.map(); if (strings.length === 0) { return this; } const stored = readColorMap(); - if (!stored) { - return this; + if (stored) { + strings.forEach((s) => { + if (stored[s] === undefined) { + console.error( + `ColorScale: run "${s}" missing from shared color map` + ); + } + this.identifiers.set(s, stored[s]); + }); + } else { + // NgRx bridge has not seeded window.__tbRunColorMap yet. + // Fall back to the static palette so getColor never fails for + // runs that were passed to setDomain. + strings.forEach((s, i) => { + this.identifiers.set(s, this.palette[i % this.palette.length]); + }); } - strings.forEach((s) => { - const color = stored[s]; - if (color !== undefined) { - this.identifiers.set(s, color); - } - }); return this; } public getColor(s: string): string { if (!this.identifiers.has(s)) { - return '#808080'; + throw new Error(`String ${s} was not in the domain.`); } return this.identifiers.get(s) as string; } From 0ddd1a17cfa77895e9e464b3c171acf74e0f1956 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 18:05:59 +0000 Subject: [PATCH 3/4] colorScale: use palette fallback for missing entries, not undefined When the shared color map exists but is missing a specific run, fall back to the palette color for that entry (and log the error). Previously the identifier was set to undefined, which would cause getColor to return undefined instead of a hex string. Co-authored-by: Samuel --- tensorbored/components/tf_color_scale/colorScale.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tensorbored/components/tf_color_scale/colorScale.ts b/tensorbored/components/tf_color_scale/colorScale.ts index f2eb498bc31..d0518b0dd46 100644 --- a/tensorbored/components/tf_color_scale/colorScale.ts +++ b/tensorbored/components/tf_color_scale/colorScale.ts @@ -39,13 +39,16 @@ export class ColorScale { } const stored = readColorMap(); if (stored) { - strings.forEach((s) => { - if (stored[s] === undefined) { + strings.forEach((s, i) => { + const color = stored[s]; + if (color !== undefined) { + this.identifiers.set(s, color); + } else { console.error( `ColorScale: run "${s}" missing from shared color map` ); + this.identifiers.set(s, this.palette[i % this.palette.length]); } - this.identifiers.set(s, stored[s]); }); } else { // NgRx bridge has not seeded window.__tbRunColorMap yet. From 72190543b0c71d958d98557f6656465b1a1470e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 18:40:39 +0000 Subject: [PATCH 4/4] style: fix Prettier formatting Co-authored-by: Samuel --- tensorbored/components/tf_color_scale/colorScale.ts | 4 +--- tensorbored/webapp/settings/_redux/settings_selectors.ts | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tensorbored/components/tf_color_scale/colorScale.ts b/tensorbored/components/tf_color_scale/colorScale.ts index d0518b0dd46..242e9554a19 100644 --- a/tensorbored/components/tf_color_scale/colorScale.ts +++ b/tensorbored/components/tf_color_scale/colorScale.ts @@ -44,9 +44,7 @@ export class ColorScale { if (color !== undefined) { this.identifiers.set(s, color); } else { - console.error( - `ColorScale: run "${s}" missing from shared color map` - ); + console.error(`ColorScale: run "${s}" missing from shared color map`); this.identifiers.set(s, this.palette[i % this.palette.length]); } }); diff --git a/tensorbored/webapp/settings/_redux/settings_selectors.ts b/tensorbored/webapp/settings/_redux/settings_selectors.ts index d279bef5f17..c1e1b46ae9f 100644 --- a/tensorbored/webapp/settings/_redux/settings_selectors.ts +++ b/tensorbored/webapp/settings/_redux/settings_selectors.ts @@ -15,7 +15,11 @@ limitations under the License. import {createFeatureSelector, createSelector} from '@ngrx/store'; import {DataLoadState} from '../../types/data'; import {ColorPalette} from '../../util/colors'; -import {SettingsState, SETTINGS_FEATURE_KEY, initialState} from './settings_types'; +import { + SettingsState, + SETTINGS_FEATURE_KEY, + initialState, +} from './settings_types'; const selectSettingsStateRaw = createFeatureSelector(SETTINGS_FEATURE_KEY);