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>
142 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|