Files
ritmex-bot/tests/isolated-margin-guard.test.ts
T
discountry b9331c516a i18n(core): route order-coordinator and token/margin guards through t()
These files bypassed the i18n table entirely, so a user running LANG=en still
got Chinese order logs. Migrating them surfaced structural duplication too: the
five order paths each spelled out their own 'quantity invalid' string, now one
key parameterised by an order-kind label.

Also fixes a type that carried display text as its domain: TrendLabel was
'做多' | '做空' | '无信号', so the engine's snapshot value *was* the Chinese
string and English rendering depended on matching it. Now 'long' | 'short' |
'none', translated at the edge.

Order-coordinator tests asserted the Chinese literals, which is exactly what
made the gap invisible; they now assert the resolved key so they hold in either
language.

39 new translation keys. 271 pass; tsc and oxlint clean.
2026-07-29 21:26:57 +08:00

128 lines
4.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { IsolatedMarginGuard } from "../src/strategy/common/isolated-margin-guard";
import type { AccountSnapshot } from "../src/exchanges/types";
import { t } from "../src/i18n";
const SYMBOL = "BTC-USD";
function snapshotWithMode(mode: string | null): AccountSnapshot {
return {
positions: [{ symbol: SYMBOL, ...(mode ? { marginType: mode } : {}) }],
} as unknown as AccountSnapshot;
}
function makeGuard(options: {
enabled?: boolean;
initialMode?: string | null;
/** Modes the account reports on successive polls. */
polledModes?: Array<string | null>;
changeMarginMode?: (params: { symbol: string; marginMode: "isolated" | "cross" }) => Promise<void>;
omitCapabilities?: boolean;
} = {}) {
const logs: Array<[string, string]> = [];
let current = snapshotWithMode("initialMode" in options ? options.initialMode! : "cross");
const polled = [...(options.polledModes ?? [])];
const queryAccountSnapshot = vi.fn(async () => snapshotWithMode(polled.shift() ?? "cross"));
const changeMarginMode = vi.fn(options.changeMarginMode ?? (async () => {}));
const guard = new IsolatedMarginGuard({
symbol: SYMBOL,
enabled: options.enabled ?? true,
log: (type, detail) => logs.push([type, detail]),
currentSnapshot: () => current,
changeMarginMode: options.omitCapabilities ? undefined : changeMarginMode,
queryAccountSnapshot: options.omitCapabilities ? undefined : queryAccountSnapshot,
applySnapshot: (next) => {
current = next;
},
// No real waiting in tests.
sleep: async () => {},
});
return { guard, logs, changeMarginMode, queryAccountSnapshot };
}
describe("IsolatedMarginGuard", () => {
it("is inert on venues without a per-symbol margin mode", async () => {
const { guard, changeMarginMode } = makeGuard({ enabled: false });
expect(await guard.ensureIsolated()).toBe(true);
expect(guard.currentMode()).toBeNull();
expect(changeMarginMode).not.toHaveBeenCalled();
});
it("does nothing when already isolated", async () => {
const { guard, changeMarginMode } = makeGuard({ initialMode: "isolated" });
expect(await guard.ensureIsolated()).toBe(true);
expect(changeMarginMode).not.toHaveBeenCalled();
});
it("normalises the reported mode", async () => {
const { guard } = makeGuard({ initialMode: " ISOLATED " });
expect(guard.currentMode()).toBe("isolated");
});
it("reports an unknown mode as null", async () => {
const { guard } = makeGuard({ initialMode: null });
expect(guard.currentMode()).toBeNull();
});
it("switches and confirms through a snapshot poll", async () => {
const { guard, logs, changeMarginMode } = makeGuard({
initialMode: "cross",
polledModes: ["cross", "isolated"],
});
expect(await guard.ensureIsolated()).toBe(true);
expect(changeMarginMode).toHaveBeenCalledWith({ symbol: SYMBOL, marginMode: "isolated" });
expect(logs.some(([, detail]) => detail === t("log.margin.switched"))).toBe(true);
});
it("gives up after the confirm attempts run out", async () => {
const { guard, logs, queryAccountSnapshot } = makeGuard({ polledModes: [] });
expect(await guard.ensureIsolated()).toBe(false);
expect(queryAccountSnapshot).toHaveBeenCalledTimes(10);
expect(logs.some(([type]) => type === "warn")).toBe(true);
});
it("reports failure when the venue rejects the change", async () => {
const { guard, logs } = makeGuard({
changeMarginMode: async () => {
throw new Error("rejected");
},
});
expect(await guard.ensureIsolated()).toBe(false);
expect(logs.some(([type]) => type === "error")).toBe(true);
});
it("returns false when the adapter cannot change margin mode", async () => {
const { guard } = makeGuard({ omitCapabilities: true });
expect(await guard.ensureIsolated()).toBe(false);
});
it("shares one in-flight switch across concurrent ticks", async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const { guard, changeMarginMode } = makeGuard({
polledModes: ["isolated"],
changeMarginMode: async () => {
await gate;
},
});
const first = guard.ensureIsolated();
// A tick arriving mid-switch must not fire a second change request.
const second = await guard.ensureIsolated();
expect(second).toBe(false);
release();
expect(await first).toBe(true);
expect(changeMarginMode).toHaveBeenCalledTimes(1);
});
it("allows a fresh attempt after the previous one settles", async () => {
const { guard, changeMarginMode } = makeGuard({ polledModes: [] });
expect(await guard.ensureIsolated()).toBe(false);
expect(await guard.ensureIsolated()).toBe(false);
expect(changeMarginMode).toHaveBeenCalledTimes(2);
});
});