Files
ritmex-bot/tests/strategy-registry.test.ts
discountry 9538ecf265 refactor(strategy): collapse six strategy lists into one registry
Adding a strategy meant editing six places that had to agree: the StrategyId
union and a parallel Set in args.ts, STRATEGY_LABELS and a nine-branch
STRATEGY_FACTORIES in strategy-runner.ts, plus an inline id union and
BASE_STRATEGIES in App.tsx. runEngine's type parameter was additionally bounded
by a union of all nine snapshot types.

They had already drifted: the CLI gated basis on isBasisSupportedExchangeId
while the menu checked only isBasisStrategyEnabled, so the menu offered basis
on exchanges where startStrategy would throw.

- strategy-ids.ts: the id list and alias parsing, dependency-free so CLI arg
  parsing does not pull in every engine.
- registry.ts: one definition per strategy (labels, symbol, engine factory, and
  a single unavailableReason both the menu and the runner consult), keyed by a
  total Record so a new id will not compile until it is defined.
- StrategyEngine/StrategySnapshot interfaces replace the snapshot union;
  runEngine now depends only on the contract. All nine engines already satisfied
  it — no engine changed.
- App.tsx keeps only the id -> Ink view map, also a total Record.

Menu order preserved. 8 new tests, one pinning menu/CLI availability agreement.
234 pass; tsc clean. -242 lines.
2026-07-29 20:36:55 +08:00

70 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
STRATEGY_DEFINITIONS,
availableStrategies,
getStrategyDefinition,
strategyUnavailableReason,
} from "../src/strategy/registry";
import { STRATEGY_IDS, isStrategyId, parseStrategyId } from "../src/strategy/strategy-ids";
describe("strategy ids", () => {
it("resolves canonical ids and documented aliases", () => {
expect(parseStrategyId("trend")).toBe("trend");
expect(parseStrategyId(" GRID ")).toBe("grid");
expect(parseStrategyId("offset")).toBe("offset-maker");
expect(parseStrategyId("offsetmaker")).toBe("offset-maker");
expect(parseStrategyId("makerpoints")).toBe("maker-points");
expect(parseStrategyId("maker_points")).toBe("maker-points");
expect(parseStrategyId("liquidity")).toBe("liquidity-maker");
expect(parseStrategyId("liquidity_maker")).toBe("liquidity-maker");
});
it("rejects unknown names", () => {
expect(parseStrategyId("nope")).toBeNull();
expect(parseStrategyId("")).toBeNull();
expect(isStrategyId("nope")).toBe(false);
});
});
describe("strategy registry", () => {
it("defines every id exactly once, in menu order", () => {
expect(STRATEGY_DEFINITIONS.map((d) => d.id)).toEqual([...STRATEGY_IDS]);
expect(new Set(STRATEGY_DEFINITIONS.map((d) => d.id)).size).toBe(STRATEGY_IDS.length);
});
it("gives every strategy a console label and i18n keys", () => {
for (const definition of STRATEGY_DEFINITIONS) {
expect(definition.consoleLabel).toBeTruthy();
expect(definition.labelKey).toMatch(/^app\.strategy\./);
expect(definition.descriptionKey).toMatch(/^app\.strategy\./);
expect(typeof definition.symbol()).toBe("string");
}
});
it("gates maker-points to StandX", () => {
expect(strategyUnavailableReason("maker-points", "standx")).toBeNull();
expect(strategyUnavailableReason("maker-points", "aster")).toContain("StandX");
});
it("keeps the menu and the CLI on one availability rule", () => {
// The menu shows exactly what startStrategy would accept — the two used to
// disagree, so basis appeared on exchanges where the runner then threw.
for (const exchangeId of ["aster", "standx", "backpack"] as const) {
const shown = availableStrategies(exchangeId).map((d) => d.id);
const runnable = STRATEGY_IDS.filter((id) => strategyUnavailableReason(id, exchangeId) == null);
expect(shown).toEqual(runnable);
}
});
it("hides strategies whose environment gate is closed", () => {
const shown = availableStrategies("backpack").map((d) => d.id);
expect(shown).not.toContain("maker-points");
});
it("exposes an engine factory per strategy", () => {
for (const id of STRATEGY_IDS) {
expect(typeof getStrategyDefinition(id).createEngine).toBe("function");
}
});
});