/** * @module tests/unit/aggregation/materialize-failure-loud * @description Aggregation (Pattern C): a failed materialization must not be * swallowed. `scheduleMaterialize`'s debounced `materializeOne(key).catch(() => {})` * silently discarded the error, leaving the materialized Measurement entity stale * with no signal. It now emits a loud warning (the value is still rebuildable via * backfill-on-query, so it stays non-fatal — but visible). Load-bearing assertion: * prodLog.warn fires instead of an empty catch. */ import { describe, it, expect, vi, afterEach } from 'vitest' import { AggregateMaterializer } from '../../../src/aggregation/materializer.js' import { prodLog } from '../../../src/utils/logger.js' import { NounType } from '../../../src/types/graphTypes.js' describe('Aggregation — swallowed materialize failure is surfaced', () => { afterEach(() => { vi.restoreAllMocks() vi.useRealTimers() }) it('logs loudly when materializeOne rejects', async () => { vi.useFakeTimers() const warn = vi.spyOn(prodLog, 'warn').mockImplementation(() => {}) // Brain access whose add()/update() always reject → doMaterialize throws. const brain = { add: vi.fn().mockRejectedValue(new Error('storage down')), update: vi.fn().mockRejectedValue(new Error('storage down')) } const m = new AggregateMaterializer(brain as any, 10) const def: any = { name: 'sales', source: { type: NounType.Document }, groupBy: ['region'], metrics: { total: { op: 'count' } }, materialize: { debounceMs: 10 } } m.scheduleMaterialize( 'sales', def, { region: 'us' }, { groupKey: { region: 'us' }, metrics: { total: { count: 1 } } } as any ) await vi.advanceTimersByTimeAsync(20) // fire the debounce timer await Promise.resolve() expect(warn).toHaveBeenCalled() expect(String(warn.mock.calls[0][0])).toMatch(/Failed to materialize aggregate group/) }) })