test: harden daemon lifecycle and e2e reliability across app/cli/server

This commit is contained in:
Mohamed Boudra
2026-02-25 10:21:46 +07:00
parent 094ce49d4f
commit a060ae1256
98 changed files with 5716 additions and 1771 deletions
@@ -1,5 +1,5 @@
import type { ComponentType, ReactElement } from "react";
import type { StyleProp, ViewStyle } from "react-native";
import type { ComponentType, ReactElement, RefObject } from "react";
import type { FlatList, ScrollView, StyleProp, View, ViewStyle } from "react-native";
import type { StreamItem } from "@/types/stream";
type EdgeSlot = "header" | "footer";
@@ -11,44 +11,6 @@ export type MaintainVisibleContentPositionConfig = Readonly<{
autoscrollToTopThreshold: number;
}>;
type StreamRenderStrategyBase = {
kind: "inverted_stream" | "forward_stream";
flatListInverted: boolean;
edgeSlot: EdgeSlot;
overlayScrollbarInverted: boolean;
maintainVisibleContentPosition?: MaintainVisibleContentPositionConfig;
assistantTurnTraversalStep: AssistantTurnTraversalStep;
disableParentScrollOnInlineDetailsExpansion: boolean;
};
export type InvertedStreamRenderStrategy = StreamRenderStrategyBase & {
kind: "inverted_stream";
flatListInverted: true;
edgeSlot: "header";
overlayScrollbarInverted: true;
maintainVisibleContentPosition: MaintainVisibleContentPositionConfig;
assistantTurnTraversalStep: 1;
disableParentScrollOnInlineDetailsExpansion: false;
};
export type ForwardStreamRenderStrategy = StreamRenderStrategyBase & {
kind: "forward_stream";
flatListInverted: false;
edgeSlot: "footer";
overlayScrollbarInverted: false;
maintainVisibleContentPosition?: undefined;
assistantTurnTraversalStep: -1;
};
export type StreamRenderStrategy =
| InvertedStreamRenderStrategy
| ForwardStreamRenderStrategy;
export type ResolveStreamRenderStrategyInput = {
platform: string;
isMobileBreakpoint: boolean;
};
export type StreamViewportMetrics = {
contentHeight: number;
viewportHeight: number;
@@ -66,65 +28,269 @@ export type StreamEdgeSlotProps = {
ListFooterComponentStyle?: StyleProp<ViewStyle>;
};
export type StreamRenderRefs = {
flatListRef: RefObject<FlatList<StreamItem> | null>;
scrollViewRef: RefObject<ScrollView | null>;
bottomAnchorRef: RefObject<View | null>;
};
export type ResolveStreamRenderStrategyInput = {
platform: string;
isMobileBreakpoint: boolean;
};
export interface StreamRenderStrategy {
orderTail: (streamItems: StreamItem[]) => StreamItem[];
orderHead: (streamHead: StreamItem[]) => StreamItem[];
getNeighborIndex: (index: number, relation: NeighborRelation) => number;
getNeighborItem: (
items: StreamItem[],
index: number,
relation: NeighborRelation
) => StreamItem | undefined;
collectAssistantTurnContent: (items: StreamItem[], startIndex: number) => string;
isNearBottom: (input: StreamNearBottomInput) => boolean;
getBottomOffset: (metrics: StreamViewportMetrics) => number;
getEdgeSlotProps: (
component: ReactElement | ComponentType<any> | null,
gapSize: number
) => StreamEdgeSlotProps;
getMaintainVisibleContentPosition: () =>
| MaintainVisibleContentPositionConfig
| undefined;
getFlatListInverted: () => boolean;
getOverlayScrollbarInverted: () => boolean;
shouldDisableParentScrollOnInlineDetailsExpansion: () => boolean;
shouldAnchorBottomOnContentSizeChange: () => boolean;
shouldAnimateManualScrollToBottom: () => boolean;
shouldUseVirtualizedList: () => boolean;
scrollToBottom: (params: {
refs: StreamRenderRefs;
metrics: StreamViewportMetrics;
animated: boolean;
}) => void;
scrollToOffset: (params: {
refs: StreamRenderRefs;
offset: number;
animated: boolean;
}) => void;
}
type StreamRenderStrategyConfig = {
orderTailReverse: boolean;
orderHeadReverse: boolean;
assistantTurnTraversalStep: AssistantTurnTraversalStep;
edgeSlot: EdgeSlot;
flatListInverted: boolean;
overlayScrollbarInverted: boolean;
maintainVisibleContentPosition?: MaintainVisibleContentPositionConfig;
disableParentScrollOnInlineDetailsExpansion: boolean;
anchorBottomOnContentSizeChange: boolean;
animateManualScrollToBottom: boolean;
useVirtualizedList: boolean;
isNearBottom: (input: StreamNearBottomInput) => boolean;
getBottomOffset: (metrics: StreamViewportMetrics) => number;
scrollToBottom: (params: {
refs: StreamRenderRefs;
metrics: StreamViewportMetrics;
animated: boolean;
}) => void;
scrollToOffset: (params: {
refs: StreamRenderRefs;
offset: number;
animated: boolean;
}) => void;
};
const DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION: MaintainVisibleContentPositionConfig =
Object.freeze({
minIndexForVisible: 0,
autoscrollToTopThreshold: 0,
});
const INVERTED_STREAM_STRATEGY: InvertedStreamRenderStrategy = {
kind: "inverted_stream",
flatListInverted: true,
edgeSlot: "header",
overlayScrollbarInverted: true,
maintainVisibleContentPosition: DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION,
assistantTurnTraversalStep: 1,
disableParentScrollOnInlineDetailsExpansion: false,
};
function scrollAnchorIntoView(params: {
refs: StreamRenderRefs;
animated: boolean;
}): boolean {
const anchorHandle = params.refs.bottomAnchorRef.current as
| ({ getNativeRef?: () => unknown; scrollIntoView?: (options?: unknown) => void } &
object)
| null;
if (!anchorHandle) {
return false;
}
const FORWARD_STREAM_STRATEGY_DESKTOP: ForwardStreamRenderStrategy = {
kind: "forward_stream",
flatListInverted: false,
edgeSlot: "footer",
overlayScrollbarInverted: false,
assistantTurnTraversalStep: -1,
disableParentScrollOnInlineDetailsExpansion: true,
};
const maybeNative =
typeof anchorHandle.getNativeRef === "function"
? anchorHandle.getNativeRef()
: anchorHandle;
const FORWARD_STREAM_STRATEGY_MOBILE: ForwardStreamRenderStrategy = {
...FORWARD_STREAM_STRATEGY_DESKTOP,
disableParentScrollOnInlineDetailsExpansion: false,
};
const domElement = maybeNative as { scrollIntoView?: (options?: unknown) => void };
if (typeof domElement.scrollIntoView !== "function") {
return false;
}
domElement.scrollIntoView({
block: "end",
behavior: params.animated ? "smooth" : "auto",
});
return true;
}
function createStreamRenderStrategy(
config: StreamRenderStrategyConfig
): StreamRenderStrategy {
return {
orderTail: (streamItems) =>
config.orderTailReverse ? [...streamItems].reverse() : streamItems,
orderHead: (streamHead) =>
config.orderHeadReverse ? [...streamHead].reverse() : streamHead,
getNeighborIndex: (index, relation) =>
relation === "above"
? index + config.assistantTurnTraversalStep
: index - config.assistantTurnTraversalStep,
getNeighborItem: (items, index, relation) => {
const neighborIndex =
relation === "above"
? index + config.assistantTurnTraversalStep
: index - config.assistantTurnTraversalStep;
if (neighborIndex < 0 || neighborIndex >= items.length) {
return undefined;
}
return items[neighborIndex];
},
collectAssistantTurnContent: (items, startIndex) => {
const messages: string[] = [];
for (
let index = startIndex;
index >= 0 && index < items.length;
index += config.assistantTurnTraversalStep
) {
const currentItem = items[index];
if (currentItem.kind === "user_message") {
break;
}
if (currentItem.kind === "assistant_message") {
messages.push(currentItem.text);
}
}
return messages.reverse().join("\n\n");
},
isNearBottom: (input) => config.isNearBottom(input),
getBottomOffset: (metrics) => config.getBottomOffset(metrics),
getEdgeSlotProps: (component, gapSize) => {
if (config.edgeSlot === "header") {
return {
ListHeaderComponent: component,
ListHeaderComponentStyle: { marginBottom: gapSize },
};
}
return {
ListFooterComponent: component,
ListFooterComponentStyle: { marginTop: gapSize },
};
},
getMaintainVisibleContentPosition: () => config.maintainVisibleContentPosition,
getFlatListInverted: () => config.flatListInverted,
getOverlayScrollbarInverted: () => config.overlayScrollbarInverted,
shouldDisableParentScrollOnInlineDetailsExpansion: () =>
config.disableParentScrollOnInlineDetailsExpansion,
shouldAnchorBottomOnContentSizeChange: () =>
config.anchorBottomOnContentSizeChange,
shouldAnimateManualScrollToBottom: () => config.animateManualScrollToBottom,
shouldUseVirtualizedList: () => config.useVirtualizedList,
scrollToBottom: (params) => config.scrollToBottom(params),
scrollToOffset: (params) => config.scrollToOffset(params),
};
}
function createInvertedStreamStrategy(): StreamRenderStrategy {
return createStreamRenderStrategy({
orderTailReverse: true,
orderHeadReverse: true,
assistantTurnTraversalStep: 1,
edgeSlot: "header",
flatListInverted: true,
overlayScrollbarInverted: true,
maintainVisibleContentPosition: DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION,
disableParentScrollOnInlineDetailsExpansion: false,
anchorBottomOnContentSizeChange: false,
animateManualScrollToBottom: true,
useVirtualizedList: true,
isNearBottom: (input) => input.offsetY <= input.threshold,
getBottomOffset: () => 0,
scrollToBottom: ({ refs, animated }) => {
refs.flatListRef.current?.scrollToOffset({
offset: 0,
animated,
});
},
scrollToOffset: ({ refs, offset, animated }) => {
refs.flatListRef.current?.scrollToOffset({ offset, animated });
},
});
}
function createForwardStreamStrategy(): StreamRenderStrategy {
return createStreamRenderStrategy({
orderTailReverse: false,
orderHeadReverse: false,
assistantTurnTraversalStep: -1,
edgeSlot: "footer",
flatListInverted: false,
overlayScrollbarInverted: false,
maintainVisibleContentPosition: undefined,
disableParentScrollOnInlineDetailsExpansion: false,
anchorBottomOnContentSizeChange: true,
animateManualScrollToBottom: false,
useVirtualizedList: false,
isNearBottom: (inputMetrics) => {
const distanceFromBottom = Math.max(
0,
inputMetrics.contentHeight -
(inputMetrics.offsetY + inputMetrics.viewportHeight)
);
return distanceFromBottom <= inputMetrics.threshold;
},
getBottomOffset: (metrics) =>
Math.max(0, metrics.contentHeight - metrics.viewportHeight),
scrollToBottom: ({ refs, metrics, animated }) => {
if (scrollAnchorIntoView({ refs, animated })) {
return;
}
refs.scrollViewRef.current?.scrollToEnd?.({ animated });
refs.scrollViewRef.current?.scrollTo?.({
y: Math.max(0, metrics.contentHeight - metrics.viewportHeight),
animated,
});
},
scrollToOffset: ({ refs, offset, animated }) => {
refs.scrollViewRef.current?.scrollTo({ y: offset, animated });
},
});
}
export function resolveStreamRenderStrategy(
input: ResolveStreamRenderStrategyInput
): StreamRenderStrategy {
if (input.platform === "web") {
return input.isMobileBreakpoint
? FORWARD_STREAM_STRATEGY_MOBILE
: FORWARD_STREAM_STRATEGY_DESKTOP;
return createForwardStreamStrategy();
}
return INVERTED_STREAM_STRATEGY;
return createInvertedStreamStrategy();
}
export function orderTailForStreamRenderStrategy(params: {
strategy: StreamRenderStrategy;
streamItems: StreamItem[];
}): StreamItem[] {
const { strategy, streamItems } = params;
return strategy.kind === "inverted_stream"
? [...streamItems].reverse()
: streamItems;
return params.strategy.orderTail(params.streamItems);
}
export function orderHeadForStreamRenderStrategy(params: {
strategy: StreamRenderStrategy;
streamHead: StreamItem[];
}): StreamItem[] {
const { strategy, streamHead } = params;
return strategy.kind === "inverted_stream"
? [...streamHead].reverse()
: streamHead;
return params.strategy.orderHead(params.streamHead);
}
export function getStreamNeighborIndex(params: {
@@ -132,11 +298,7 @@ export function getStreamNeighborIndex(params: {
index: number;
relation: NeighborRelation;
}): number {
const { strategy, index, relation } = params;
if (strategy.kind === "inverted_stream") {
return relation === "above" ? index + 1 : index - 1;
}
return relation === "above" ? index - 1 : index + 1;
return params.strategy.getNeighborIndex(params.index, params.relation);
}
export function getStreamNeighborItem(params: {
@@ -145,11 +307,11 @@ export function getStreamNeighborItem(params: {
index: number;
relation: NeighborRelation;
}): StreamItem | undefined {
const nextIndex = getStreamNeighborIndex(params);
if (nextIndex < 0 || nextIndex >= params.items.length) {
return undefined;
}
return params.items[nextIndex];
return params.strategy.getNeighborItem(
params.items,
params.index,
params.relation
);
}
export function collectAssistantTurnContentForStreamRenderStrategy(params: {
@@ -157,49 +319,32 @@ export function collectAssistantTurnContentForStreamRenderStrategy(params: {
items: StreamItem[];
startIndex: number;
}): string {
const { strategy, items, startIndex } = params;
const messages: string[] = [];
for (
let index = startIndex;
index >= 0 && index < items.length;
index += strategy.assistantTurnTraversalStep
) {
const currentItem = items[index];
if (currentItem.kind === "user_message") {
break;
}
if (currentItem.kind === "assistant_message") {
messages.push(currentItem.text);
}
}
return messages.reverse().join("\n\n");
return params.strategy.collectAssistantTurnContent(
params.items,
params.startIndex
);
}
export function isNearBottomForStreamRenderStrategy(
params: StreamNearBottomInput & { strategy: StreamRenderStrategy }
): boolean {
const { strategy, threshold, offsetY } = params;
if (strategy.kind === "inverted_stream") {
return offsetY <= threshold;
}
const distanceFromBottom = Math.max(
0,
params.contentHeight - (offsetY + params.viewportHeight)
);
return distanceFromBottom <= threshold;
return params.strategy.isNearBottom({
offsetY: params.offsetY,
threshold: params.threshold,
contentHeight: params.contentHeight,
viewportHeight: params.viewportHeight,
});
}
export function getBottomOffsetForStreamRenderStrategy(params: StreamViewportMetrics & {
strategy: StreamRenderStrategy;
}): number {
const { strategy } = params;
if (strategy.kind === "inverted_stream") {
return 0;
export function getBottomOffsetForStreamRenderStrategy(
params: StreamViewportMetrics & {
strategy: StreamRenderStrategy;
}
return Math.max(0, params.contentHeight - params.viewportHeight);
): number {
return params.strategy.getBottomOffset({
contentHeight: params.contentHeight,
viewportHeight: params.viewportHeight,
});
}
export function getStreamEdgeSlotProps(params: {
@@ -207,15 +352,5 @@ export function getStreamEdgeSlotProps(params: {
component: ReactElement | ComponentType<any> | null;
gapSize: number;
}): StreamEdgeSlotProps {
const { strategy, component, gapSize } = params;
if (strategy.edgeSlot === "header") {
return {
ListHeaderComponent: component,
ListHeaderComponentStyle: { marginBottom: gapSize },
};
}
return {
ListFooterComponent: component,
ListFooterComponentStyle: { marginTop: gapSize },
};
return params.strategy.getEdgeSlotProps(params.component, params.gapSize);
}