/** * R-011 回归测试:tabs 统一管理数据层(R-027 策略 tab 静态化) * 独立内存 mock scope(settings 读写),验证: * 1. 归一化迁移:旧布尔对象 tabs → 有序数组(内置保留原显隐,策略接续追加) * 2. 迁移:旧隐藏策略 → visible=true(Q3) * 3. getTabs:策略行 name 由内置常量派生(改名场景已退役) * 4. updateTabs:整表更新(顺序 + 显隐) * 5. 策略 tab 静态化(R-027):getStrategies 恒 2 项;normalizeTabs 恒 5 条; * 存量自建策略 tab 条目被丢弃;addStrategy/removeStrategy/renameStrategy 等退役导出不存在 * 纯内存,不触真实数据(技术约束-011) */ import { getTabs, updateTabs, normalizeTabs, getStrategies } from '../src/settings.js'; import * as settingsMod from '../src/settings.js'; let pass = 0, fail = 0; function assert(cond, msg) { if (cond) { pass++; console.log(" ✅ " + msg); } else { fail++; console.log(" ❌ " + msg); } } /** 内存 mock scope:get/update 读改写对象 */ function makeScope(initial) { const store = structuredClone(initial ?? {}); return { get: () => structuredClone(store), update: async (patch) => { Object.assign(store, structuredClone(patch)); }, _store: store, }; } // ---------- 1. 旧格式归一化(布尔对象 tabs + 策略带 visible/order) ---------- console.log("\n[1] 旧格式归一化迁移"); const oldScope = makeScope({ strategies: [ { id: 'grid-supermarket', name: '网格超市', visible: true, order: 5 }, { id: 'manual-t', name: '手动做T', visible: false, order: 9 }, ], tabs: { allPositions: true, tradeRecords: false, watchlist: true }, }); const oldTabs = normalizeTabs(oldScope); assert(oldTabs.length === 5, "归一化后共 5 个条目(3 内置 + 2 策略)"); assert(oldTabs[0].kind === 'builtin' && oldTabs[0].refKey === 'allPositions', '内置 allPositions 在前'); assert(oldTabs[1].refKey === 'tradeRecords' && oldTabs[1].visible === false, '内置 tradeRecords 保留旧显隐=false'); assert(oldTabs[2].refKey === 'watchlist' && oldTabs[2].visible === true, '内置 watchlist 保留旧显隐=true'); const manualTab = oldTabs.find((t) => t.kind === 'strategy' && t.refId === 'manual-t'); assert(manualTab && manualTab.visible === true, "旧隐藏策略 manual-t 迁移后 visible=true(Q3)"); assert(manualTab && manualTab.name === '手动做T', '策略行 name 来自内置常量'); assert(oldTabs[3].order === 3 && oldTabs[4].order === 4, '策略按 order 接续(order 连续)'); // ---------- 2. 已归一化数组直接返回 ---------- console.log("\n[2] 已归一化数组直接返回"); const arrScope = makeScope({ tabs: [ { id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 1 }, { id: 'tab-watchlist', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: true, order: 0 }, ] }); const arrTabs = normalizeTabs(arrScope); assert(arrTabs[0].id === 'tab-watchlist' && arrTabs[1].id === 'tab-all-positions', '数组按 order 排序'); assert(arrTabs.length === 5, '缺失条目补齐(恒 5 条)'); // ---------- 3. getTabs:策略行 name 由常量派生(改名已退役) ---------- console.log("\n[3] getTabs 策略行 name"); const joinScope = makeScope({ strategies: [{ id: 'manual-t', name: '手动做T新名' }], // 存量改名残留:不再参与 tabs: [ { id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 }, { id: 'tab-strategy-manual-t', kind: 'strategy', refId: 'manual-t', name: '旧名', visible: true, order: 1 }, ], }); const joined = getTabs(joinScope); assert(joined.find((t) => t.id === 'tab-strategy-manual-t').name === '手动做T', '策略行 name 恒为内置常量(存量行 name 被覆盖)'); // ---------- 4. updateTabs 整表更新 ---------- console.log("\n[4] updateTabs 整表更新"); const updScope = makeScope({ tabs: [ { id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 }, { id: 'tab-watchlist', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: true, order: 1 }, ] }); await updateTabs(updScope, [ { id: 'tab-watchlist', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: false, order: 0 }, { id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 1 }, ]); const updTabs = getTabs(updScope); assert(updTabs[0].id === 'tab-watchlist' && updTabs[0].visible === false, '整表更新:顺序与显隐生效'); assert(updTabs.length === 5, '归一化后恒 5 条(缺失策略行补齐)'); // ---------- 5. 策略 tab 静态化(R-027) ---------- console.log("\n[5] 策略 tab 静态化(R-027)"); const statScope = makeScope({ strategies: [ { id: 'grid-supermarket', name: '网格超市' }, { id: 'manual-t', name: '手动做T' }, { id: 'long-term', name: '长线持有' }, // R-027 前可增:存量自建策略 ], tabs: [ { 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-long-term', kind: 'strategy', refId: 'long-term', name: '长线持有', visible: true, order: 0 }, // 存量自建策略 tab ], }); const strat = getStrategies(statScope); assert(strat.length === 2, 'getStrategies 恒 2 项(自建策略被忽略)'); assert(!strat.some((s) => s.id === 'long-term'), '自建策略不出现在读取结果'); const statTabs = normalizeTabs(statScope); assert(statTabs.length === 5, 'normalizeTabs 恒 5 条(3 内置 + 2 策略)'); assert(!statTabs.some((t) => t.refId === 'long-term'), '存量自建策略 tab 条目被丢弃'); assert(statTabs.filter((t) => t.kind === 'builtin').length === 3 && statTabs.filter((t) => t.kind === 'strategy').length === 2, '条目构成 = 3 内置 + 2 策略'); assert(!('addStrategy' in settingsMod), '退役导出 addStrategy 不存在'); assert(!('removeStrategy' in settingsMod), '退役导出 removeStrategy 不存在'); assert(!('renameStrategy' in settingsMod), '退役导出 renameStrategy 不存在'); assert(!('updateStrategies' in settingsMod), '退役导出 updateStrategies 不存在'); assert(!('appendStrategyTab' in settingsMod) && !('removeStrategyTab' in settingsMod), '退役导出 append/removeStrategyTab 不存在'); // ---------- 汇总 ---------- console.log("\n结果: " + pass + " 通过 / " + fail + " 失败"); if (fail > 0) process.exit(1);