diff --git a/libs/shared/interfaces/src/lib/stalker-item.normalizer.spec.ts b/libs/shared/interfaces/src/lib/stalker-item.normalizer.spec.ts new file mode 100644 index 000000000..d3e4bb8ac --- /dev/null +++ b/libs/shared/interfaces/src/lib/stalker-item.normalizer.spec.ts @@ -0,0 +1,27 @@ +import { normalizeStalkerDate } from './stalker-item.normalizer'; + +describe('normalizeStalkerDate', () => { + it('normalizes SQLite UTC timestamps without relying on engine-specific parsing', () => { + const originalParse = Date.parse; + const parseSpy = jest + .spyOn(Date, 'parse') + .mockImplementation((value: string) => { + if (value === '2026-04-21 22:49:02') { + return Number.NaN; + } + + return originalParse(value); + }); + + expect(normalizeStalkerDate('2026-04-21 22:49:02')).toBe( + '2026-04-21T22:49:02.000Z' + ); + expect(parseSpy).not.toHaveBeenCalledWith('2026-04-21 22:49:02'); + }); + + it('preserves fractional seconds in SQLite UTC timestamps', () => { + expect(normalizeStalkerDate('2026-04-21 22:49:02.7')).toBe( + '2026-04-21T22:49:02.700Z' + ); + }); +}); diff --git a/libs/shared/interfaces/src/lib/stalker-item.normalizer.ts b/libs/shared/interfaces/src/lib/stalker-item.normalizer.ts index 5b61c256c..881306ae3 100644 --- a/libs/shared/interfaces/src/lib/stalker-item.normalizer.ts +++ b/libs/shared/interfaces/src/lib/stalker-item.normalizer.ts @@ -3,6 +3,9 @@ import { StalkerPortalItem, } from './stalker-portal-item.interface'; +const SQLITE_UTC_TIMESTAMP_PATTERN = + /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/; + /** * Extract a stable ID from an untyped Stalker portal item. * @@ -93,6 +96,10 @@ export function normalizeStalkerDate(value: unknown): string { return new Date(ms).toISOString(); } } + const sqliteUtcIso = normalizeSqliteUtcTimestamp(trimmed); + if (sqliteUtcIso) { + return sqliteUtcIso; + } const parsed = Date.parse(trimmed); if (!Number.isNaN(parsed)) { return new Date(parsed).toISOString(); @@ -117,3 +124,34 @@ export function stalkerItemMatchesId( const rawId = extractStalkerItemId(item, fallbackPrefix, fallbackIndex); return rawId === targetId; } + +function normalizeSqliteUtcTimestamp(value: string): string | null { + const match = SQLITE_UTC_TIMESTAMP_PATTERN.exec(value); + if (!match) { + return null; + } + + const [ + , + year, + month, + day, + hours, + minutes, + seconds, + milliseconds = '0', + ] = match; + + const normalizedMs = milliseconds.padEnd(3, '0').slice(0, 3); + return new Date( + Date.UTC( + Number(year), + Number(month) - 1, + Number(day), + Number(hours), + Number(minutes), + Number(seconds), + Number(normalizedMs) + ) + ).toISOString(); +}