/** @jest-environment jsdom */ import { lastJourney, useJourneyTestRuntime } from '../../__test-utils__/test-runtime'; import { configureJourney, getSlowRequests } from '../../config'; import { completeJourney, defineJourney, journeyStep, resetJourney, singleInFlightJourneyStep, } from '../../core'; import { createJourneyRuntime, moduleRuntime } from '../../core/runtime'; import { registerJourneyRuntime, setJourneyDefaultEnabled } from '../../global'; import { sendToConsole } from '../../sinks/console'; import { sendToDatadog } from '../../sinks/datadog'; jest.mock('../../sinks/datadog', () => ({ sendToDatadog: jest.fn(), })); const def = { name: 'runtime-journey', team: 't', group: 'g', service: 's', timeoutMs: 0, }; describe('[journey] resetJourney', () => { const emitMock = jest.fn(); useJourneyTestRuntime(emitMock); describe('when a journey is open', () => { beforeEach(() => { moduleRuntime.startJourney(def); }); const subject = () => resetJourney({ sinks: [emitMock] }); test('clears it without emitting', () => { subject(); expect(emitMock).not.toHaveBeenCalled(); }); test('makes later complete a no-op', () => { subject(); completeJourney(def.name); expect(emitMock).not.toHaveBeenCalled(); }); }); describe('when request timeouts were customized', () => { beforeEach(() => { configureJourney({ slowRequests: { defaultMs: 100, endpoints: [] } }); }); const subject = () => resetJourney({ sinks: [emitMock] }); test('clears this runtime overlay and inherits shared policies', () => { subject(); expect(moduleRuntime.getDebugSnapshot().policyOverlay.slowRequests).toBe(false); expect(moduleRuntime.getSlowRequests().defaultMs).toBe(100); expect(getSlowRequests().defaultMs).toBe(100); }); }); describe('when reset passes custom slowRequests', () => { const subject = () => resetJourney({ sinks: [emitMock], slowRequests: { defaultMs: 250, endpoints: [] }, }); test('applies those timeouts on the module engine', () => { subject(); expect(moduleRuntime.getSlowRequests().defaultMs).toBe(250); }); }); describe('when reset is called with no options', () => { beforeEach(() => { resetJourney({ sinks: [emitMock] }); emitMock.mockClear(); moduleRuntime.startJourney(def); completeJourney(def.name); emitMock.mockClear(); }); const subject = () => resetJourney(); test('leaves sinks empty', () => { subject(); moduleRuntime.startJourney(def); completeJourney(def.name); expect(emitMock).not.toHaveBeenCalled(); }); }); describe('when a step is in flight', () => { let resolveWork!: () => void; let stepPromise: Promise; beforeEach(() => { moduleRuntime.startJourney(def); const hang = new Promise(resolve => { resolveWork = resolve; }); stepPromise = journeyStep(def.name, 'slow', async () => { await hang; }); }); afterEach(async () => { resolveWork(); await stepPromise.catch(() => {}); }); const subject = () => resetJourney({ sinks: [emitMock] }); test('clears the ambient step', () => { expect(singleInFlightJourneyStep()).not.toBeNull(); subject(); expect(singleInFlightJourneyStep()).toBeNull(); }); test('reuses the same ambient stamp while the active step is unchanged', async () => { const first = singleInFlightJourneyStep(); const second = singleInFlightJourneyStep(); expect(first).not.toBeNull(); expect(second).toBe(first); resolveWork(); await stepPromise; expect(singleInFlightJourneyStep()).toBeNull(); }); }); describe('when a journey countdown is armed', () => { beforeEach(() => { jest.useFakeTimers(); moduleRuntime.startJourney({ ...def, timeoutMs: 50 }); }); afterEach(() => { jest.useRealTimers(); }); const subject = () => resetJourney({ sinks: [emitMock] }); test('prevents the timer from emitting', async () => { subject(); emitMock.mockClear(); await jest.advanceTimersByTimeAsync(50); expect(emitMock).not.toHaveBeenCalled(); }); }); }); describe('[journey] createJourneyRuntime', () => { const defaultEmit = jest.fn(); const isolatedEmit = jest.fn(); let runtime: ReturnType; beforeEach(() => { setJourneyDefaultEnabled(true); resetJourney({ sinks: [defaultEmit], slowRequests: { defaultMs: 4_000, endpoints: [] }, }); defaultEmit.mockClear(); isolatedEmit.mockClear(); runtime = createJourneyRuntime({ sinks: [isolatedEmit] }); }); test('auto-assigns a unique runtime-N id when registered without an id', () => { registerJourneyRuntime(runtime); expect(moduleRuntime.id).toMatch(/^runtime-\d+$/); expect(runtime.id).toMatch(/^runtime-\d+$/); expect(runtime.id).not.toBe(moduleRuntime.id); }); test('uses the provided id', () => { const named = createJourneyRuntime({ id: 'job-booking', sinks: [isolatedEmit] }); expect(named.id).toBe('job-booking'); }); afterEach(() => { setJourneyDefaultEnabled(false); localStorage.clear(); resetJourney({ sinks: [sendToDatadog, sendToConsole], slowRequests: { defaultMs: 4_000, endpoints: [] }, }); }); describe('when both runtimes open a journey', () => { beforeEach(() => { runtime.startJourney(def); moduleRuntime.startJourney({ ...def, name: 'default-side' }); }); const subject = () => { runtime.completeJourney(def.name); completeJourney('default-side'); }; test('emits only to the matching sink', () => { subject(); expect(isolatedEmit.mock.calls[0][0].journey.name).toBe(def.name); expect(defaultEmit.mock.calls[0][0].journey.name).toBe('default-side'); }); }); describe('when the isolated runtime has its own slowRequests', () => { beforeEach(() => { runtime = createJourneyRuntime({ sinks: [isolatedEmit], slowRequests: { defaultMs: 10, endpoints: [] }, }); configureJourney({ slowRequests: { defaultMs: 9_000, endpoints: [] } }); }); test('inherits shared host thresholds unless this runtime overlays', () => { expect(runtime.getSlowRequests().defaultMs).toBe(9_000); runtime.setPolicyOverlay({ slowRequests: { defaultMs: 10, endpoints: [] } }); expect(runtime.getSlowRequests().defaultMs).toBe(10); expect(getSlowRequests().defaultMs).toBe(9_000); }); }); describe('when the isolated runtime is reset', () => { beforeEach(() => { runtime.startJourney(def); moduleRuntime.startJourney({ ...def, name: 'default-side' }); }); const subject = () => runtime.reset({ sinks: [isolatedEmit] }); test('does not clear the module engine', () => { subject(); completeJourney('default-side'); expect(defaultEmit).toHaveBeenCalledTimes(1); expect(lastJourney(defaultEmit).name).toBe('default-side'); }); }); describe('when defineJourney is used', () => { const subject = () => defineJourney(def).step('load', step => { step.completeJourney(); }); test('binds to the module runtime', async () => { await subject(); expect(defaultEmit).toHaveBeenCalledTimes(1); expect(lastJourney(defaultEmit).name).toBe(def.name); expect(isolatedEmit).not.toHaveBeenCalled(); }); }); describe('when a throwing sink is registered on the isolated runtime', () => { let second: jest.Mock; beforeEach(() => { second = jest.fn(); runtime.setSinks([ () => { throw new Error('boom'); }, second, ]); runtime.startJourney(def); }); const subject = () => runtime.completeJourney(def.name); test('still invokes later sinks', () => { expect(() => subject()).not.toThrow(); expect(second).toHaveBeenCalledTimes(1); }); }); describe('with create-time options', () => { test('installs the initial sinks', () => { const created = createJourneyRuntime({ sinks: [isolatedEmit] }); created.startJourney(def); created.completeJourney(def.name); expect(isolatedEmit).toHaveBeenCalledTimes(1); }); }); describe('maxSteps bounds', () => { test('clamps invalid and oversized values', () => { moduleRuntime.setMaxSteps(Infinity); expect(moduleRuntime.getMaxSteps()).toBe(50); moduleRuntime.setMaxSteps(0); expect(moduleRuntime.getMaxSteps()).toBe(50); moduleRuntime.setMaxSteps(500); expect(moduleRuntime.getMaxSteps()).toBe(200); moduleRuntime.setMaxSteps(Number.NaN); expect(moduleRuntime.getMaxSteps()).toBe(50); }); }); });