Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/web/__tests__/flow-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
NODE_WIDTH,
SELF_LOOP_HEIGHT,
SELF_LOOP_WIDTH,
orthogonalizeRoutePoints,
} from '../src/lib/flow-layout.ts'

const orderFlow = {
Expand Down Expand Up @@ -285,6 +286,54 @@ describe('buildOrthogonalPath', () => {
})

describe('inferPortAssignmentsFromRoutes', () => {
it('treats sub-pixel y differences as flat (e.g. claude-router → mongodb in orion-main)', () => {
// After shiftRoutesAfterSnap, two endpoints that should share y can
// drift by ~1 ULP due to float arithmetic. Without epsilon-tolerant
// orthogonalization this triggers a phantom vertical corner; that 3rd
// point makes the final segment a tiny vertical, and
// inferPortAssignmentsFromRoutes then runs targetHandleFromSegment on
// (0, ~3e-15), where |dx|=0 < |dy|=3e-15, so it falls into the
// vertical branch and picks 'bottom' instead of 'left'. Regression:
// the road visibly dove past the database into empty space.
//
// Run through orthogonalizeRoutePoints first so we exercise the same
// pipeline computeElkLayout uses (orthogonalize → infer); without that
// step the test would already pass on broken code because the raw
// 2-point route biases the horizontal-dominant branch.
const topology = buildFlowTopology({
flow: {
nodes: [
{ id: 'a', label: 'A', type: 'service' },
{ id: 'b', label: 'B', type: 'database' },
],
steps: [{ from: 'a', to: 'b' }],
},
} as Flow)

const edge = topology.displayEdges[0]
const positions = new Map([
['a', { x: 0, y: 0 }],
['b', { x: 200, y: 0 }],
])
// The two y values differ at the last bit of float precision.
const rawNoisyRoute = [
{ x: 100, y: 80.00000000000001 },
{ x: 200, y: 80 },
]
const orthogonalized = orthogonalizeRoutePoints(rawNoisyRoute)
// With the epsilon fix, orthogonalize keeps the 2-point route and
// doesn't fabricate a corner. (Without the fix, it would expand to
// three points with a phantom near-zero vertical at the end.)
expect(orthogonalized).toHaveLength(2)

const assignments = inferPortAssignmentsFromRoutes(
topology,
positions,
new Map([[edge.id, orthogonalized]])
)
expect(assignments.get(edge.id)).toMatchObject({ source: 'right', target: 'left' })
})

Comment thread
coderabbitai[bot] marked this conversation as resolved.
it('chooses the closest routed side from the first and last orthogonal segments', () => {
const assignments = inferPortAssignmentsFromRoutes(
buildFlowTopology(orderFlow),
Expand Down
15 changes: 13 additions & 2 deletions packages/web/src/lib/flow-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,15 @@ function chooseOrthogonalCorner(
return { x: next.x, y: current.y }
}

function orthogonalizeRoutePoints(points: RoutePoint[]): RoutePoint[] {
// Sub-pixel epsilon for orthogonality checks. After shiftRoutesAfterSnap,
// two points that should share a coordinate can drift by ~1 ULP (3e-15) due
// to float arithmetic. Without this tolerance, orthogonalize inserts a
// phantom corner and inferPortAssignmentsFromRoutes then treats a tiny dy
// as the dominant axis — picking 'bottom' for what should be a flat
// 'left' entry. (Seen on claude-router → mongodb in the orion main flow.)
const ORTHOGONAL_EPSILON = 0.5

export function orthogonalizeRoutePoints(points: RoutePoint[]): RoutePoint[] {
const route: RoutePoint[] = []

for (let index = 0; index < points.length; index++) {
Expand All @@ -363,7 +371,10 @@ function orthogonalizeRoutePoints(points: RoutePoint[]): RoutePoint[] {
continue
}

if (current.x !== point.x && current.y !== point.y) {
if (
Math.abs(current.x - point.x) > ORTHOGONAL_EPSILON &&
Math.abs(current.y - point.y) > ORTHOGONAL_EPSILON
) {
const corner = chooseOrthogonalCorner(
route[route.length - 2],
current,
Expand Down
Loading