fix(18): WR-01 treat empty/blank TZ as unset in household timezone fallback
The D-06 fallback used row?.value ?? process.env.TZ ?? Intl..., but ??
only short-circuits on null/undefined. A set-but-empty TZ ('' or ' ')
leaked through and yielded an invalid IANA zone that throws inside
Intl.DateTimeFormat({ timeZone }) downstream, silently dropping the
all-day reminder. Extract resolveHouseholdTimezone() which trims and
treats empty/whitespace candidate values (stored value and TZ) as
absent so they fall through to the Intl resolved zone. Adds RED->GREEN
unit tests for empty and whitespace-only TZ.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ea93089b74
commit
d168da71cf
@@ -16,7 +16,7 @@ import { appConfig } from '../db/schema.js';
|
||||
|
||||
/**
|
||||
* Returns the stored household timezone from app_config, or falls back to:
|
||||
* 1. process.env.TZ (if set and non-empty)
|
||||
* 1. process.env.TZ (only if set and non-empty — empty/whitespace is ignored, WR-01)
|
||||
* 2. Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
*
|
||||
* This is the D-05 single source of truth for the server-side all-day "9 AM local"
|
||||
@@ -31,11 +31,33 @@ export async function getHouseholdTimezone(
|
||||
.where(eq(appConfig.key, 'household_timezone'))
|
||||
.limit(1);
|
||||
|
||||
return (
|
||||
row?.value ??
|
||||
process.env.TZ ??
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
);
|
||||
return resolveHouseholdTimezone(row?.value ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the household timezone from an already-fetched stored value, applying
|
||||
* the D-06 fallback chain. Centralizing the policy here (D-05) means callers that
|
||||
* have already read the row — e.g. the GET /config/timezone handler — can reuse it
|
||||
* without a second DB round-trip (IN-01), and there is exactly one place where the
|
||||
* fallback rules live (IN-02).
|
||||
*
|
||||
* WR-01: `??` only short-circuits on null/undefined, so a set-but-empty
|
||||
* `process.env.TZ` (`TZ=` or `TZ=' '`) would otherwise leak through and yield an
|
||||
* invalid IANA zone that throws inside `Intl.DateTimeFormat({ timeZone })` downstream.
|
||||
* Empty/whitespace-only candidate values are treated as absent so they fall through.
|
||||
*/
|
||||
export function resolveHouseholdTimezone(storedValue: string | null): string {
|
||||
const stored = storedValue?.trim();
|
||||
if (stored) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
const envTz = process.env.TZ?.trim();
|
||||
if (envTz) {
|
||||
return envTz;
|
||||
}
|
||||
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user