Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

369 lines
9.0 KiB
TypeScript

/**
* Smoke tests for `handleHyperliquidRoute` and `createHyperliquidInfoClient`:
* markets/funding/status/positions/orders route payloads, non-GET 501
* execution-disabled responses, and position/order normalization math
* (mark price, distance-to-liquidation, effective leverage). Fetch is mocked
* against fixed responses — no live Hyperliquid API calls.
*/
import type http from "node:http";
import { describe, expect, it, vi } from "vitest";
import {
createHyperliquidInfoClient,
type HyperliquidFetch,
handleHyperliquidRoute,
} from "../src/routes";
function responseRecorder() {
const res = {
status: 0,
headers: {} as Record<string, string>,
body: "",
headersSent: false,
statusCode: 0,
setHeader(key: string, value: string) {
this.headers[key.toLowerCase()] = value;
},
writeHead(status: number, headers?: Record<string, string>) {
this.status = status;
this.statusCode = status;
this.headers = headers ?? {};
},
end(body?: string) {
this.status = this.statusCode;
this.body = body ?? "";
this.headersSent = true;
},
};
return res;
}
function fixedNow() {
return new Date("2026-05-01T12:00:00.000Z");
}
describe("Hyperliquid route and info client behavior", () => {
it("fetches markets through the Info API and returns parsed route payloads", async () => {
const fetchImpl = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) => {
expect(init).toMatchObject({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "meta" }),
});
return Response.json({
universe: [
{ name: "BTC", szDecimals: 5, maxLeverage: 50 },
{
name: "ETH",
szDecimals: 4,
onlyIsolated: true,
isDelisted: false,
},
],
});
},
) as HyperliquidFetch;
const res = responseRecorder();
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
res as unknown as http.ServerResponse,
"/api/hyperliquid/markets",
"GET",
{ fetchImpl, now: fixedNow },
),
).resolves.toBe(true);
expect(fetchImpl).toHaveBeenCalledWith(
"https://api.hyperliquid.xyz/info",
expect.any(Object),
);
expect(res.status).toBe(200);
expect(JSON.parse(res.body)).toEqual({
markets: [
{
name: "BTC",
index: 0,
szDecimals: 5,
maxLeverage: 50,
onlyIsolated: false,
isDelisted: false,
},
{
name: "ETH",
index: 1,
szDecimals: 4,
maxLeverage: null,
onlyIsolated: true,
isDelisted: false,
},
],
source: "hyperliquid-info-meta",
fetchedAt: "2026-05-01T12:00:00.000Z",
});
});
it("fetches current funding rates through metaAndAssetCtxs", async () => {
const fetchImpl = vi.fn(
async (_input: string | URL | Request, init?: RequestInit) => {
expect(init).toMatchObject({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "metaAndAssetCtxs" }),
});
return Response.json([
{
universe: [
{ name: "BTC", szDecimals: 5, maxLeverage: 50 },
{ name: "ETH", szDecimals: 4, maxLeverage: 50 },
],
},
[
{
funding: "0.0000125",
premium: "0.00031774",
markPx: "14.3161",
oraclePx: "14.32",
openInterest: "688.11",
},
{
funding: "-0.000001",
premium: "-0.00002",
markPx: "6.0436",
oraclePx: "6.0457",
openInterest: "1882.55",
},
],
]);
},
) as HyperliquidFetch;
const res = responseRecorder();
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
res as unknown as http.ServerResponse,
"/api/hyperliquid/funding",
"GET",
{ fetchImpl, now: fixedNow },
),
).resolves.toBe(true);
expect(res.status).toBe(200);
expect(JSON.parse(res.body)).toEqual({
rates: [
{
coin: "BTC",
index: 0,
funding: "0.0000125",
premium: "0.00031774",
markPx: "14.3161",
oraclePx: "14.32",
openInterest: "688.11",
},
{
coin: "ETH",
index: 1,
funding: "-0.000001",
premium: "-0.00002",
markPx: "6.0436",
oraclePx: "6.0457",
openInterest: "1882.55",
},
],
source: "hyperliquid-info-meta-and-asset-ctxs",
fetchedAt: "2026-05-01T12:00:00.000Z",
});
});
it("returns status without fetch and rejects non-GET methods as execution-disabled", async () => {
const statusRes = responseRecorder();
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
statusRes as unknown as http.ServerResponse,
"/api/hyperliquid/status",
"GET",
{ env: {} as NodeJS.ProcessEnv },
),
).resolves.toBe(true);
expect(statusRes.status).toBe(200);
expect(JSON.parse(statusRes.body)).toMatchObject({
publicReadReady: true,
signerReady: false,
executionReady: false,
credentialMode: "none",
readiness: {
accountReads: false,
execution: false,
},
});
const postRes = responseRecorder();
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
postRes as unknown as http.ServerResponse,
"/api/hyperliquid/orders",
"POST",
{ env: {} as NodeJS.ProcessEnv },
),
).resolves.toBe(true);
expect(postRes.status).toBe(501);
expect(JSON.parse(postRes.body)).toMatchObject({
executionReady: false,
credentialMode: "none",
});
});
it("returns blocked positions when no account is configured and false for unrelated routes", async () => {
const res = responseRecorder();
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
res as unknown as http.ServerResponse,
"/api/hyperliquid/positions",
"GET",
{
env: {} as NodeJS.ProcessEnv,
fetchImpl: vi.fn() as unknown as HyperliquidFetch,
},
),
).resolves.toBe(true);
expect(res.status).toBe(200);
expect(JSON.parse(res.body)).toMatchObject({
accountAddress: null,
positions: [],
fetchedAt: null,
});
await expect(
handleHyperliquidRoute(
{} as http.IncomingMessage,
responseRecorder() as unknown as http.ServerResponse,
"/api/not-hyperliquid",
"GET",
),
).resolves.toBe(false);
});
it("normalizes positions and orders through the info client and throws on malformed payloads", async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
Response.json({
assetPositions: [
{
position: {
coin: "ETH",
szi: "1.25",
entryPx: "3000",
// positionValue 4000 / size 1.25 => mark 3200, deliberately
// above the 3000 entry so the derived mark (not entry) drives
// the distance-to-liquidation calc.
positionValue: "4000",
unrealizedPnl: "12.5",
returnOnEquity: "0.05",
liquidationPx: "2400",
marginUsed: "1000",
leverage: { type: "cross", value: 3 },
},
},
],
marginSummary: {
accountValue: "5000",
totalNtlPos: "4000",
totalMarginUsed: "1000",
totalRawUsd: "5012.5",
},
withdrawable: "4000",
}),
)
.mockResolvedValueOnce(
Response.json([
{
coin: "BTC",
side: "B",
limitPx: "65000",
sz: "0.1",
oid: 123,
timestamp: 171000,
reduceOnly: true,
orderType: "Limit",
tif: "Gtc",
},
]),
)
.mockResolvedValueOnce(
Response.json({ notUniverse: [] }),
) as unknown as vi.MockedFunction<HyperliquidFetch>;
const client = createHyperliquidInfoClient({ fetchImpl });
const account = "0x0000000000000000000000000000000000000001";
await expect(client.getPositions(account)).resolves.toEqual({
positions: [
{
coin: "ETH",
size: "1.25",
entryPx: "3000",
positionValue: "4000",
unrealizedPnl: "12.5",
returnOnEquity: "0.05",
liquidationPx: "2400",
marginUsed: "1000",
leverageType: "cross",
leverageValue: 3,
// mark = 4000 / 1.25 = 3200; (3200 - 2400) / 3200 = 25% to liq.
markPx: "3200",
distanceToLiquidationPct: 25,
},
],
summary: {
accountValue: "5000",
totalNotionalPosition: "4000",
totalMarginUsed: "1000",
totalRawUsd: "5012.5",
withdrawable: "4000",
totalUnrealizedPnl: "12.50",
// effective leverage = totalNtlPos 4000 / accountValue 5000 = 0.8.
effectiveLeverage: 0.8,
},
});
await expect(client.getOpenOrders(account)).resolves.toEqual([
{
coin: "BTC",
side: "B",
limitPx: "65000",
size: "0.1",
oid: 123,
timestamp: 171000,
reduceOnly: true,
orderType: "Limit",
tif: "Gtc",
cloid: null,
},
]);
await expect(client.getMarkets()).rejects.toThrow(
"Hyperliquid meta response missing universe",
);
expect(fetchImpl).toHaveBeenNthCalledWith(
1,
"https://api.hyperliquid.xyz/info",
expect.objectContaining({
body: JSON.stringify({ type: "clearinghouseState", user: account }),
}),
);
expect(fetchImpl).toHaveBeenNthCalledWith(
2,
"https://api.hyperliquid.xyz/info",
expect.objectContaining({
body: JSON.stringify({ type: "openOrders", user: account }),
}),
);
});
});