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); }); });