/** * R-011 API 层集成测试:handleStrategy tabs 域(R-027 策略静态化更新) * 独立内存 mock settings scope + mock manager,验证: * 1. tabs 端点:返回归一化统一数组(恒 5 条:3 内置 + 2 策略) * 2. tabs/update:整表更新(顺序 + 显隐) * 3. strategies/add:已退役(方法表不含 + 调用抛 not-found) * 4. strategies/remove:已退役(方法表不含 + 调用抛 not-found) * 5. strategies/update:已退役(方法表不含 + 调用抛 not-found) * 6. strategies/schema-update:单策略字段定义写入(读回一致) * 7. 端点表不含 strategies/move(已废弃) * 纯内存,不触真实数据(技术约束-011) */ import { handleStrategy, STRATEGY_METHODS } from '../src/api/strategies.js'; let pass = 0, fail = 0; function assert(cond, msg) { if (cond) { pass++; console.log(" ✅ " + msg); } else { fail++; console.log(" ❌ " + msg); } } /** 内存 mock scope */ function makeScope(initial) { const store = structuredClone(initial ?? {}); return { get: () => structuredClone(store), update: async (patch) => { Object.assign(store, structuredClone(patch)); }, }; } /** 断言某端点已退役:STRATEGY_METHODS 不含 + 调用抛 code=not-found */ async function assertRetired(method, args, runtime) { assert(!STRATEGY_METHODS.has(method), "STRATEGY_METHODS 不含 " + method); try { await handleStrategy(method, args, runtime); assert(false, method + " 应抛 not-found(未抛错)"); } catch (e) { assert(e.code === "not-found", method + " 抛 code=not-found"); } } const scope = makeScope({ strategies: [ { id: "grid-supermarket", name: "网格超市" }, { id: "manual-t", name: "手动做T" }, ], tabs: [ { id: "tab-all-positions", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 0 }, { id: "tab-trade-records", kind: "builtin", refKey: "tradeRecords", name: "交易记录", visible: true, order: 1 }, { id: "tab-watchlist", kind: "builtin", refKey: "watchlist", name: "关注列表", visible: true, order: 2 }, { id: "tab-strategy-grid-supermarket", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: true, order: 3 }, { id: "tab-strategy-manual-t", kind: "strategy", refId: "manual-t", name: "手动做T", visible: true, order: 4 }, ], }); const runtime = { manager: {}, settings: scope }; // 1. tabs 端点 console.log("\n[1] tabs 端点"); const strat1 = await handleStrategy("strategies", {}, runtime); assert(Array.isArray(strat1) && strat1.length === 2, "strategies 端点恒 2 项(内置)"); const tabs1 = await handleStrategy("tabs", {}, runtime); assert(Array.isArray(tabs1) && tabs1.length === 5, "tabs 恒 5 条(3 内置 + 2 策略)"); assert(tabs1.filter((t) => t.kind === "builtin").length === 3 && tabs1.filter((t) => t.kind === "strategy").length === 2, "内置 3 + 策略 2 混排"); assert(tabs1.some((t) => t.refId === "grid-supermarket") && tabs1.some((t) => t.refId === "manual-t"), "策略 tab 恒在(不可增删)"); // 2. tabs/update 整表更新 console.log("\n[2] tabs/update 整表更新"); const afterUpdate = await handleStrategy("tabs/update", { tabs: [ { id: "tab-strategy-manual-t", kind: "strategy", refId: "manual-t", name: "手动做T", visible: false, order: 0 }, { id: "tab-all-positions", kind: "builtin", refKey: "allPositions", name: "全部持仓", visible: true, order: 1 }, { id: "tab-watchlist", kind: "builtin", refKey: "watchlist", name: "关注列表", visible: true, order: 2 }, { id: "tab-strategy-grid-supermarket", kind: "strategy", refId: "grid-supermarket", name: "网格超市", visible: true, order: 3 }, { id: "tab-trade-records", kind: "builtin", refKey: "tradeRecords", name: "交易记录", visible: true, order: 4 }, ] }, runtime); assert(afterUpdate[0].id === "tab-strategy-manual-t" && afterUpdate[0].visible === false, "手动做T 排最前且隐藏(顺序+显隐生效)"); assert(afterUpdate[1].id === "tab-all-positions", "内置 tab 排第二位"); assert(afterUpdate.length === 5, "整表更新后恒 5 条"); // 3. strategies/add 已退役 console.log("\n[3] strategies/add 退役"); await assertRetired("strategies/add", { name: "长线持有" }, runtime); // 4. strategies/remove 已退役 console.log("\n[4] strategies/remove 退役"); await assertRetired("strategies/remove", { strategyId: "grid-supermarket" }, runtime); // 5. strategies/update 已退役 console.log("\n[5] strategies/update 退役"); await assertRetired("strategies/update", { strategies: [] }, runtime); // 6. strategies/schema-update 单策略写入(R-027 唯一写路径) console.log("\n[6] strategies/schema-update"); const saved = await handleStrategy("strategies/schema-update", { strategyId: "grid-supermarket", configSchema: [{ key: "gridGap", label: "网格间距", type: "text", def: "3%" }], }, runtime); assert(saved.configSchema[0].label === "网格间距", "schema-update 返回更新后的策略对象"); const stratAfter = await handleStrategy("strategies", {}, runtime); assert(stratAfter.find((s) => s.id === "grid-supermarket").configSchema[0].label === "网格间距", "读回一致(写入 strategyFields)"); assert(stratAfter.find((s) => s.id === "manual-t").configSchema.length === 0, "只影响目标策略(manual-t 无定义)"); // 7. 端点表不含 strategies/move console.log("\n[7] strategies/move 已废弃"); assert(!STRATEGY_METHODS.has("strategies/move"), "STRATEGY_METHODS 不含 strategies/move"); console.log("\n结果: " + pass + " 通过 / " + fail + " 失败"); if (fail > 0) process.exit(1);