diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 2071762..c96d86e 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { NAV_ITEMS, + appendHistoryTail, buildDefaultFlowFilters, classifierToneForFamily, deriveAlertDirection, @@ -9,6 +10,7 @@ import { formatOptionContractLabel, flushPausableTapeData, getAlertWindowAnchorTs, + getLiveHistoryRetentionCap, getOptionTableSnapshot, getLiveFeedStatus, getLiveManifest, @@ -240,6 +242,53 @@ describe("live tape pausable helpers", () => { }); }); +describe("live tape history helpers", () => { + it("appends older scoped rows behind the hot live head", () => { + const liveHead = Array.from({ length: 100 }, (_, idx) => + makeItem(`hot-${idx}`, 200 - idx, 2_000 - idx) + ); + const older = [makeItem("older-1", 99, 999), makeItem("older-2", 98, 998)]; + + const next = appendHistoryTail([], older, liveHead, 5000); + + expect(next.map((item) => item.trace_id)).toEqual(["older-1", "older-2"]); + }); + + it("skips duplicates already present in the live head", () => { + const liveHead = [makeItem("latest", 3, 300), makeItem("duplicate", 2, 200)]; + const older = [makeItem("duplicate", 2, 200), makeItem("older", 1, 100)]; + + const next = appendHistoryTail([], older, liveHead, 5000); + + expect(next.map((item) => item.trace_id)).toEqual(["older"]); + }); + + it("trims the history tail to the soft cap", () => { + const current = [makeItem("existing", 4, 400)]; + const older = [makeItem("older-1", 3, 300), makeItem("older-2", 2, 200)]; + + const next = appendHistoryTail(current, older, [], 2); + + expect(next.map((item) => item.trace_id)).toEqual(["existing", "older-1"]); + }); + + it("keeps scoped option and equity history on the normal retention cap", () => { + expect( + getLiveHistoryRetentionCap({ + channel: "options", + underlying_ids: ["AAPL"], + option_contract_id: "AAPL-2025-01-17-200-C" + } as any) + ).toBeGreaterThan(0); + expect( + getLiveHistoryRetentionCap({ + channel: "equities", + underlying_ids: ["AAPL"] + } as any) + ).toBeGreaterThan(0); + }); +}); + describe("options display formatters", () => { it("formats dashed option contracts as ticker strike expiry", () => { expect(formatOptionContractLabel("SPY-2025-01-17-450-C")).toEqual({ diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 7a66d5b..d20be39 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -510,7 +510,7 @@ const EMPTY_PAUSABLE_TAPE = { dropped: 0 }; -const appendHistoryTail = ( +export const appendHistoryTail = ( current: T[], incoming: T[], liveHead: T[], @@ -541,6 +541,16 @@ const appendHistoryTail = ( return cap > 0 ? appended.slice(0, cap) : appended; }; +export const getLiveHistoryRetentionCap = (subscription: LiveSubscription): number => { + switch (subscription.channel) { + case "options": + case "equities": + return LIVE_HISTORY_SOFT_CAP; + default: + return LIVE_HISTORY_SOFT_CAP; + } +}; + export const getLiveFeedStatus = ( sourceStatus: WsStatus, freshestTs: number | null, @@ -2924,21 +2934,13 @@ const useLiveSession = ( switch (subscription.channel) { case "options": - mergeOlder( - setOptionsHistory, - options, - subscription.underlying_ids?.length || subscription.option_contract_id ? 0 : LIVE_HISTORY_SOFT_CAP - ); + mergeOlder(setOptionsHistory, options, getLiveHistoryRetentionCap(subscription)); break; case "nbbo": mergeOlder(setNbboHistory, nbbo); break; case "equities": - mergeOlder( - setEquitiesHistory, - equities, - subscription.underlying_ids?.length ? 0 : LIVE_HISTORY_SOFT_CAP - ); + mergeOlder(setEquitiesHistory, equities, getLiveHistoryRetentionCap(subscription)); break; case "equity-quotes": break;