Skip to content

Commit ae502a7

Browse files
committed
test(perps): fix integration CI after rebase
1 parent 0d4985f commit ae502a7

8 files changed

Lines changed: 205 additions & 101 deletions

File tree

app/components/UI/Perps/hooks/usePerpsFlipPosition.integration.test.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
/**
2-
* Integration test — perps "reverse position" flow.
2+
* Integration test — perps validation contract for flip-shaped market orders.
33
*
44
* Sits next to `usePerpsFlipPosition.test.ts` (a unit test that mocks
55
* `usePerpsTrading` wholesale and never reaches the provider). This file
6-
* exercises the REAL `HyperLiquidProvider.validateOrder` against the exact
7-
* `OrderParams` shape `TradingService.flipPosition` constructs today.
6+
* exercises the REAL `HyperLiquidProvider.validateOrder` against a
7+
* flip-shaped market order without caller-provided price data.
88
*
9-
* The bug: market reverse orders aren't given `currentPrice`. Validator's
10-
* fallback only fires for `orderType === 'limit'`. Result: aborts with
11-
* `ORDER_PRICE_REQUIRED`. See `app/controllers/perps/providers/HyperLiquidProvider.ts`
12-
* line ~6745–6770 (validateOrder) and `services/TradingService.ts` line
13-
* ~1918 (flipPosition).
9+
* Direct validation still requires either `currentPrice` or `usdAmount`.
10+
* The full TradingService/provider place-order path fetches live price data
11+
* before validation, so the user-facing flip flow is covered separately by
12+
* `orderLifecycleFlow.integration.test.ts`.
1413
*
1514
* Setup is delegated to the perps integration harness — see
1615
* `tests/integration/harnesses/perps.ts` (rules + factory) and
@@ -23,14 +22,13 @@
2322
import { buildPerpsIntegrationHarness } from '../../../../../tests/integration/harnesses/perps';
2423
import { PERPS_ERROR_CODES } from '@metamask/perps-controller';
2524

26-
describe('Perps reverse position — integration', () => {
27-
describe('reproduces the production bug', () => {
28-
it('rejects a flip-position market order with ORDER_PRICE_REQUIRED', async () => {
25+
describe('Perps reverse position validation — integration', () => {
26+
describe('direct provider validation', () => {
27+
it('rejects a market order without price context', async () => {
2928
const { provider } = buildPerpsIntegrationHarness();
3029

31-
// Exact OrderParams shape that `TradingService.flipPosition` constructs
32-
// when reversing a 1-BTC long: 2x size, market, opposite side,
33-
// no currentPrice, no usdAmount.
30+
// Flip-shaped order params: 2x size, market, opposite side,
31+
// no caller-provided currentPrice, no usdAmount.
3432
const result = await provider.validateOrder({
3533
symbol: 'BTC',
3634
isBuy: false,
@@ -43,7 +41,7 @@ describe('Perps reverse position — integration', () => {
4341
});
4442
});
4543

46-
describe('confirms the bug surface', () => {
44+
describe('valid price contexts', () => {
4745
it('passes when usdAmount is provided (one possible fix shape)', async () => {
4846
const { provider } = buildPerpsIntegrationHarness();
4947

app/components/UI/Perps/integration/componentFlow.integration.test.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
*/
99

1010
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
11+
import { type Position } from '@metamask/perps-controller';
1112
import React from 'react';
1213

1314
import { buildPerpsComponentHarness } from '../../../../../tests/integration/harnesses/perps-component';
14-
import { PerpsFlipPositionConfirmSheetSelectorsIDs , PerpsOrderViewSelectorsIDs } from '../Perps.testIds';
15+
import {
16+
PerpsFlipPositionConfirmSheetSelectorsIDs,
17+
PerpsOrderViewSelectorsIDs,
18+
} from '../Perps.testIds';
1519
import PerpsFlipPositionConfirmSheet from '../components/PerpsFlipPositionConfirmSheet';
1620
import PerpsOrderView from '../Views/PerpsOrderView/PerpsOrderView';
1721

@@ -81,13 +85,12 @@ describe('Perps component flows — integration', () => {
8185
});
8286

8387
describe('reversing a position from a rendered confirmation sheet', () => {
84-
it('surfaces ORDER_PRICE_REQUIRED from a rendered flip button press', async () => {
88+
it('submits the flip market order from a rendered button press', async () => {
8589
// Arrange
8690
const perps = buildPerpsComponentHarness();
8791
try {
8892
perps.harness.setupTradingReady();
89-
const openLongBTC = {
90-
coin: 'BTC',
93+
const openLongBTC: Position = {
9194
symbol: 'BTC',
9295
size: '0.1',
9396
entryPrice: '50000',
@@ -99,6 +102,8 @@ describe('Perps component flows — integration', () => {
99102
maxLeverage: 50,
100103
returnOnEquity: '0',
101104
cumulativeFunding: { allTime: '0', sinceOpen: '0', sinceChange: '0' },
105+
takeProfitCount: 0,
106+
stopLossCount: 0,
102107
};
103108
perps.renderWithFlow(
104109
<PerpsFlipPositionConfirmSheet position={openLongBTC} />,
@@ -113,20 +118,28 @@ describe('Perps component flows — integration', () => {
113118

114119
// Assert
115120
await waitFor(() => {
116-
expect(perps.mocks.showToast).toHaveBeenCalledWith(
117-
expect.objectContaining({
118-
labelOptions: expect.arrayContaining([
119-
expect.objectContaining({
120-
label: 'Order failed',
121-
}),
122-
]),
123-
}),
124-
);
121+
expect(perps.mocks.exchangeClient.order).toHaveBeenCalledTimes(1);
125122
});
126-
expect(perps.mocks.exchangeClient.order).not.toHaveBeenCalled();
127-
expect(
128-
screen.getByTestId(PerpsFlipPositionConfirmSheetSelectorsIDs.SHEET),
129-
).toBeOnTheScreen();
123+
expect(perps.mocks.exchangeClient.order).toHaveBeenCalledWith(
124+
expect.objectContaining({
125+
orders: [
126+
expect.objectContaining({
127+
a: 0,
128+
b: false,
129+
t: { limit: { tif: 'FrontendMarket' } },
130+
}),
131+
],
132+
}),
133+
);
134+
expect(perps.mocks.showToast).toHaveBeenCalledWith(
135+
expect.objectContaining({
136+
labelOptions: expect.arrayContaining([
137+
expect.objectContaining({
138+
label: 'Order filled',
139+
}),
140+
]),
141+
}),
142+
);
130143
} finally {
131144
perps.teardown();
132145
}

app/components/UI/Perps/integration/orderLifecycleFlow.integration.test.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
import { renderHook, act } from '@testing-library/react-native';
23+
import { type OrderResult, type Position } from '@metamask/perps-controller';
2324

2425
import { buildPerpsFlowHarness } from '../../../../../tests/integration/harnesses/perps-flow';
2526
import { usePerpsTrading } from '../hooks/usePerpsTrading';
@@ -73,22 +74,20 @@ describe('Perps order lifecycle — FLOW integration', () => {
7374
* → result.current.flipPosition({ position })
7475
* → Engine.context.PerpsController.flipPosition (shim)
7576
* → real TradingService.flipPosition
76-
* → constructs OrderParams with size=2x, no currentPrice
77+
* → constructs OrderParams with size=2x
7778
* → real provider.placeOrder(orderParams)
78-
* → real validateOrder
79-
* → returns ORDER_PRICE_REQUIRED (production bug)
79+
* → fetches live price for validation
80+
* → mocked SDK exchange.order
8081
*
81-
* Shape A could only catch the bug by calling `provider.validateOrder`
82-
* directly. Shape B catches it through the actual user-facing chain —
83-
* proving the TradingService → provider seam is genuinely covered.
82+
* Shape B catches the actual user-facing chain, proving the
83+
* TradingService → provider seam is genuinely covered.
8484
*/
85-
describe('reversing a position via the hook chain (production bug)', () => {
86-
it('reproduces the ORDER_PRICE_REQUIRED bug end-to-end', async () => {
85+
describe('reversing a position via the hook chain', () => {
86+
it('places the flip market order end-to-end', async () => {
8787
// Arrange
8888
const { harness } = buildPerpsFlowHarness();
8989
harness.setupTradingReady();
90-
const openLongBTC = {
91-
coin: 'BTC',
90+
const openLongBTC: Position = {
9291
symbol: 'BTC',
9392
size: '0.1', // positive = long; flipPosition will compute 2x = 0.2
9493
entryPrice: '50000',
@@ -100,30 +99,39 @@ describe('Perps order lifecycle — FLOW integration', () => {
10099
maxLeverage: 50,
101100
returnOnEquity: '0',
102101
cumulativeFunding: { allTime: '0', sinceOpen: '0', sinceChange: '0' },
102+
takeProfitCount: 0,
103+
stopLossCount: 0,
103104
};
104105
const { result } = renderHook(() => usePerpsTrading());
105106

106107
// Act — through the real hook → real TradingService → real provider chain
107-
let flipResult: Awaited<
108-
ReturnType<typeof result.current.flipPosition>
109-
> | null = null;
108+
let flipResult: OrderResult | null = null;
110109
await act(async () => {
111110
flipResult = await result.current.flipPosition({
112-
position: openLongBTC as never,
111+
symbol: 'BTC',
112+
position: openLongBTC,
113113
});
114114
});
115115

116-
// Assert — flipPosition returns a failed result with the right error
117-
// code. The bug is reachable because TradingService constructs the
118-
// OrderParams without `currentPrice`, and the real provider's
119-
// `validateOrder` rejects market orders without a price.
116+
// Assert — flipPosition succeeds because the provider fetches the
117+
// current market price before running order validation.
120118
expect(flipResult).not.toBeNull();
121-
expect(flipResult).toMatchObject({ success: false });
122-
expect((flipResult as { error: string }).error).toMatch(
123-
/ORDER_PRICE_REQUIRED/,
119+
if (!flipResult) {
120+
throw new Error('Expected flipPosition to return a result');
121+
}
122+
expect(flipResult).toMatchObject({ success: true });
123+
expect(harness.mocks.exchangeClient.order).toHaveBeenCalledTimes(1);
124+
expect(harness.mocks.exchangeClient.order).toHaveBeenCalledWith(
125+
expect.objectContaining({
126+
orders: [
127+
expect.objectContaining({
128+
a: 0,
129+
b: false,
130+
t: { limit: { tif: 'FrontendMarket' } },
131+
}),
132+
],
133+
}),
124134
);
125-
// SDK was never called — validation aborted the flow before placement.
126-
expect(harness.mocks.exchangeClient.order).not.toHaveBeenCalled();
127135
});
128136
});
129137
});

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const config = {
7474
'.*/e2e/.*\\.spec\\.(ts|js)$',
7575
'.*/e2e/pages/',
7676
'.*/e2e/selectors/',
77+
'.*\\.integration\\.test\\.(ts|tsx)$',
7778
'.*\\.view\\.test\\.(ts|tsx)$',
7879
],
7980
coverageReporters: ['text-summary', 'lcov'],

tests/integration/harnesses/perps-component.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ jest.mock('react-native-gesture-handler', () => {
4545
Simultaneous: jest.fn((...gestures) => gestures),
4646
},
4747
PanGestureHandler: ReactActual.forwardRef(
48-
({ children }: { children: React.ReactNode }, _ref) => children,
48+
({ children }: { children: React.ReactNode }, _ref: React.Ref<unknown>) =>
49+
children,
4950
),
5051
};
5152
});
@@ -184,11 +185,15 @@ import {
184185
PerpsStreamProvider,
185186
type PerpsStreamManager,
186187
} from '../../../app/components/UI/Perps/providers/PerpsStreamManager';
188+
import type {
189+
ToastOptions,
190+
ToastRef,
191+
} from '../../../app/component-library/components/Toast/Toast.types';
187192
import { AccessRestrictedProvider } from '../../../app/components/UI/Compliance';
188193
import Routes from '../../../app/constants/navigation/Routes';
189194
import { initialStatePerps } from '../../component-view/presets/perpsStatePreset';
190195

191-
type ToastOptionsForHarness = Record<string, unknown>;
196+
type ToastOptionsForHarness = ToastOptions;
192197

193198
interface PerpsComponentRenderOptions {
194199
stateOverrides?: DeepPartial<RootState>;
@@ -319,7 +324,9 @@ export function buildPerpsComponentHarness(
319324
const flowHarness = buildPerpsFlowHarness(options);
320325
const showToast = jest.fn<void, [ToastOptionsForHarness]>();
321326
const closeToast = jest.fn<void, []>();
322-
const toastRef = { current: { showToast, closeToast } };
327+
const toastRef: React.RefObject<ToastRef | null> = {
328+
current: { showToast, closeToast },
329+
};
323330

324331
const renderWithFlow = (
325332
component: React.ReactElement,

tests/integration/harnesses/perps-flow.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
* etc.) and exercise the chain ALL the way from hook → Engine →
88
* shim controller → real TradingService → real provider → mocked SDK.
99
* Catches bugs that live in the hook → controller wiring AND inside
10-
* TradingService (e.g. the perps reverse-position bug, where TradingService
11-
* constructs `OrderParams` without `currentPrice` and the provider rejects).
10+
* TradingService, including multi-step flows where service-generated
11+
* `OrderParams` must stay compatible with provider validation.
1212
*
1313
* Both harnesses coexist intentionally:
1414
* - `perps.ts` → Shape A. Fast, sharp failure isolation. Best for
@@ -23,8 +23,7 @@
2323
* - `TradingService` — instantiated with the harness's mocked platform deps
2424
* - `HyperLiquidProvider` (instantiated through the inner perps harness)
2525
* - All in-memory state transitions, validation, asset mapping
26-
* - The seam between TradingService and provider — where the perps
27-
* reverse-position bug lives
26+
* - The seam between TradingService and provider
2827
*
2928
* MOCKED (the I/O boundary + non-perps app surface):
3029
* - SDK clients, wallet, subscription cache, readiness cache (via inner harness)
@@ -115,8 +114,8 @@ import {
115114
type PerpsIntegrationHarness,
116115
type PerpsHarnessOptions,
117116
} from './perps';
118-
import { TradingService } from '../../../app/controllers/perps/services/TradingService';
119-
import type { ServiceContext } from '../../../app/controllers/perps/services/ServiceContext';
117+
import { TradingService } from '@metamask/perps-controller/services/TradingService';
118+
import type { ServiceContext } from '@metamask/perps-controller';
120119
import { createMockInfrastructure } from '../../../app/components/UI/Perps/__mocks__/serviceMocks';
121120

122121
export interface PerpsFlowHarness {
@@ -196,6 +195,7 @@ export function buildPerpsFlowHarness(
196195
provider: harness.provider,
197196
params,
198197
context: buildServiceContext('closePosition'),
198+
reportOrderToDataLake,
199199
}),
200200
flipPosition: (params: { position: unknown }) =>
201201
tradingService.flipPosition({

0 commit comments

Comments
 (0)