evOo-local/packages/core/src/trading-bot-status.test.ts
cynarAI de2a64f070 fix(web): read sidebar trading mode from the bot status API (#80)
The Live Trading control stored its mode in localStorage, so the shell could
claim live money while the bot was in dry-run. Show the Tradingbot status
when TRADING_BOT_URL is set, and an unknown state when it is not.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 22:10:18 +02:00

142 lines
5.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
coerceTradingBotStatusView,
interpretTradingBotProxyResponse,
mapTradingBotStatus,
parseTradingBotOrigin,
resolveTradingBotOrigin,
sanitizeTradingBotError,
tradingBotBackoffMs,
tradingBotStatusUrl,
} from "./trading-bot-status.js";
describe("parseTradingBotOrigin", () => {
it("treats an empty value as unset and keeps the origin without a path", () => {
expect(parseTradingBotOrigin(undefined)).toEqual({ kind: "unset" });
expect(parseTradingBotOrigin(" ")).toEqual({ kind: "unset" });
expect(parseTradingBotOrigin(" http://127.0.0.1:8000/bot/ ")).toEqual({
kind: "origin",
origin: "http://127.0.0.1:8000",
});
expect(tradingBotStatusUrl("http://127.0.0.1:8000")).toBe(
"http://127.0.0.1:8000/bot/api/status",
);
});
it("rejects credentials and non-http schemes without echoing the value", () => {
expect(parseTradingBotOrigin("http://user:secret@127.0.0.1:8000").kind).toBe("invalid");
expect(parseTradingBotOrigin("file:///tmp/status").kind).toBe("invalid");
expect(parseTradingBotOrigin("not a url").kind).toBe("invalid");
expect(resolveTradingBotOrigin("")).toBeUndefined();
expect(() => resolveTradingBotOrigin("javascript:alert(1)")).toThrow(
/^Invalid TRADING_BOT_URL$/,
);
});
});
describe("mapTradingBotStatus", () => {
it("maps live, paper, and dry-run badges without defaulting to live", () => {
expect(mapTradingBotStatus({ mode: "LIVE" })).toMatchObject({ mode: "live", kind: "live" });
expect(mapTradingBotStatus({ badge: "DRY RUN" })).toMatchObject({
mode: "paper",
kind: "dry_run",
});
expect(mapTradingBotStatus({ mode: "Paper Sim" })).toMatchObject({
mode: "paper",
kind: "paper",
});
expect(mapTradingBotStatus({ dry_run: true })).toMatchObject({ kind: "dry_run" });
expect(mapTradingBotStatus({ live: true })).toMatchObject({ kind: "live" });
expect(mapTradingBotStatus({ status: "ok" })).toBeNull();
expect(mapTradingBotStatus({})).toBeNull();
expect(mapTradingBotStatus(null)).toBeNull();
expect(mapTradingBotStatus({ live: false })).toBeNull();
});
it("refuses a body that claims live and dry-run together", () => {
expect(mapTradingBotStatus({ mode: "live", dry_run: true })).toBeNull();
expect(mapTradingBotStatus({ live: true, paper: true })).toBeNull();
});
it("reads a nested mode and safe subtitle fields", () => {
expect(
mapTradingBotStatus({
data: { mode: "DRY RUN", adapters: ["a", "b", "c"], last_error: "adapter timeout" },
}),
).toEqual({
mode: "paper",
kind: "dry_run",
adapterCount: 3,
lastError: "adapter timeout",
});
expect(mapTradingBotStatus({ mode: "paper", adapter_count: "4" })?.adapterCount).toBe(4);
});
it("drops secret-like errors and still reports the mode", () => {
expect(
mapTradingBotStatus({ mode: "paper", last_error: "Bearer sk-test-secret" })?.lastError,
).toBeNull();
expect(
mapTradingBotStatus({ mode: "live", error: "failed http://10.0.0.8/status" })?.lastError,
).toBeNull();
expect(sanitizeTradingBotError("ok")).toBeNull();
const long = "x".repeat(120);
expect(sanitizeTradingBotError(long)?.endsWith("…")).toBe(true);
expect(sanitizeTradingBotError(long)?.length).toBe(80);
});
});
describe("interpretTradingBotProxyResponse", () => {
it("keeps unconfigured, offline, unknown, and a live read distinct", () => {
expect(interpretTradingBotProxyResponse(200, { configured: false })).toEqual({
state: "unconfigured",
});
expect(interpretTradingBotProxyResponse(502, { configured: true, reachable: false })).toEqual({
state: "offline",
});
expect(
interpretTradingBotProxyResponse(200, { configured: true, reachable: true, status: null }),
).toEqual({ state: "unknown" });
expect(interpretTradingBotProxyResponse(401, { error: "Unauthorized" })).toEqual({
state: "offline",
});
expect(
interpretTradingBotProxyResponse(200, {
configured: true,
reachable: true,
status: { mode: "live", kind: "live", adapterCount: 1, lastError: null },
}),
).toEqual({
state: "ready",
status: { mode: "live", kind: "live", adapterCount: 1, lastError: null },
});
});
it("strips an unsafe error from an otherwise valid proxy status", () => {
const snapshot = interpretTradingBotProxyResponse(200, {
configured: true,
reachable: true,
status: {
mode: "paper",
kind: "dry_run",
adapterCount: null,
lastError: "token=super-secret",
},
});
expect(snapshot).toEqual({
state: "ready",
status: { mode: "paper", kind: "dry_run", adapterCount: null, lastError: null },
});
expect(coerceTradingBotStatusView({ mode: "live", kind: "paper" })).toBeNull();
});
});
describe("tradingBotBackoffMs", () => {
it("doubles the interval and then caps it", () => {
expect(tradingBotBackoffMs(20_000, 1, 120_000)).toBe(20_000);
expect(tradingBotBackoffMs(20_000, 2, 120_000)).toBe(40_000);
expect(tradingBotBackoffMs(20_000, 3, 120_000)).toBe(80_000);
expect(tradingBotBackoffMs(20_000, 4, 120_000)).toBe(120_000);
});
});