Skip to content

Commit 170dae2

Browse files
Pigbibicodex
andcommitted
Fix runtime target mapping for account key casing mismatches
Co-Authored-By: Codex <noreply@openai.com>
1 parent 564cfa8 commit 170dae2

3 files changed

Lines changed: 203 additions & 60 deletions

File tree

tests/test_cash_financing.js

Lines changed: 170 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,40 @@ function makeCurrentStrategies(platform, overrides = {}) {
158158

159159
function currentEntryForAccount(state, platform, account) {
160160
const byPlatform = state.currentStrategies[platform] || {};
161+
const normalizeAccountLookupKey = (value) => String(value || "").trim().toLowerCase();
161162
const keys = [account?.key, account?.target_name, account?.label].filter(Boolean).map(String);
163+
164+
const candidates = [...new Set(keys.map(normalizeAccountLookupKey).filter(Boolean))];
165+
162166
for (const key of keys) {
163167
const entry = byPlatform[key];
164-
if (entry && typeof entry === "object") return entry;
168+
if (entry && typeof entry === "object") {
169+
if (!entry.strategy_profile) {
170+
return { ...entry, strategy_profile: account?.default_strategy_profile || "", source: "worker+account_defaults" };
171+
}
172+
return entry;
173+
}
165174
}
166-
return null;
175+
176+
for (const [rawKey, entry] of Object.entries(byPlatform)) {
177+
if (!entry || typeof entry !== "object") continue;
178+
if (candidates.includes(normalizeAccountLookupKey(rawKey))) {
179+
if (!entry.strategy_profile) {
180+
return {
181+
...entry,
182+
strategy_profile: account?.default_strategy_profile || "",
183+
source: "worker+account_defaults",
184+
};
185+
}
186+
return entry;
187+
}
188+
}
189+
190+
return {
191+
runtime_target_enabled: true,
192+
strategy_profile: account?.default_strategy_profile || account?.strategy_profile || "",
193+
source: "account_defaults",
194+
};
167195
}
168196

169197
function currentReservePolicyForAccount(state, platform, account) {
@@ -813,12 +841,12 @@ console.log("\n=== 9. syncRuntimeTargetForAccount (解析为具体值) ===\n");
813841
assert(form.runtimeTargetMode === "disabled", "9d: runtime_target='FALSE' → 'disabled'");
814842
}
815843

816-
// 9e: no entry → stays "current"
844+
// 9e: no entry → use default enabled状态
817845
{
818846
const state = { currentStrategies: {} };
819847
const form = { runtimeTargetMode: "current", runtimeTargetTouched: false };
820848
syncRuntimeTargetForAccount(state, form, "ibkr", makeAccount("preview"));
821-
assert(form.runtimeTargetMode === "current", "9c: no entry → 'current'");
849+
assert(form.runtimeTargetMode === "enabled", "9e: no entry → default enabled");
822850
}
823851

824852
// 9f: touched → skip
@@ -937,7 +965,99 @@ console.log("\n=== 10. syncIncomeLayerForAccount (解析为具体值) ===\n");
937965
}
938966

939967
// ============================================================
940-
// 11. syncOptionOverlayForAccount (解析为具体值)
968+
// 11. currentEntryForAccount 映射健壮性 ===
969+
// ============================================================
970+
971+
console.log("\n=== 11. currentEntryForAccount 映射健壮性 ===\n");
972+
973+
// 11a: 按原 key 命中
974+
{
975+
const state = {
976+
currentStrategies: {
977+
ibkr: {
978+
preview: { runtime_target_enabled: true },
979+
},
980+
},
981+
};
982+
const account = makeAccount("preview");
983+
const entry = currentEntryForAccount(state, "ibkr", account);
984+
assert(entry && entry.runtime_target_enabled === true, "11a: exact key should hit current entry");
985+
}
986+
987+
// 11b: key 大小写不一致也能命中
988+
{
989+
const state = {
990+
currentStrategies: {
991+
longbridge: {
992+
sg: { runtime_target_enabled: false },
993+
},
994+
},
995+
};
996+
const account = {
997+
key: "SG",
998+
target_name: "sg",
999+
label: "SG",
1000+
};
1001+
const entry = currentEntryForAccount(state, "longbridge", account);
1002+
assert(entry && entry.runtime_target_enabled === false, "11b: case-insensitive key should match normalized");
1003+
}
1004+
1005+
// 11c: key 前后空白也能命中
1006+
{
1007+
const state = {
1008+
currentStrategies: {
1009+
longbridge: {
1010+
sg: { runtime_target_enabled: false },
1011+
},
1012+
},
1013+
};
1014+
const account = {
1015+
key: " sg ",
1016+
target_name: "SG",
1017+
label: "longbridge sg",
1018+
};
1019+
const entry = currentEntryForAccount(state, "longbridge", account);
1020+
assert(entry && entry.runtime_target_enabled === false, "11c: whitespace-trimmed key should match");
1021+
}
1022+
1023+
// 11d: 无 key 命中时回退到默认账号配置
1024+
{
1025+
const state = {
1026+
currentStrategies: {
1027+
longbridge: {},
1028+
},
1029+
};
1030+
const account = {
1031+
key: "sg",
1032+
target_name: "sg",
1033+
label: "sg",
1034+
default_strategy_profile: "tqqq_growth_income",
1035+
};
1036+
const entry = currentEntryForAccount(state, "longbridge", account);
1037+
assert(Boolean(entry), "11d: missing entry should return synthesized defaults");
1038+
assert(entry.runtime_target_enabled === true, "11d: missing entry should default runtime_target_enabled=enabled");
1039+
}
1040+
1041+
// 11e: 当前条目存在 strategy_profile 时不改写策略
1042+
{
1043+
const state = {
1044+
currentStrategies: {
1045+
longbridge: {
1046+
sg: { strategy_profile: "soxl_soxx_trend_income", runtime_target_enabled: false },
1047+
},
1048+
},
1049+
};
1050+
const account = {
1051+
key: "SG",
1052+
target_name: "SG",
1053+
label: "sg",
1054+
};
1055+
const entry = currentEntryForAccount(state, "longbridge", account);
1056+
assert(entry?.strategy_profile === "soxl_soxx_trend_income", "11e: existing strategy_profile should be preserved");
1057+
}
1058+
1059+
// ============================================================
1060+
// 12. syncOptionOverlayForAccount (解析为具体值)
9411061
// ============================================================
9421062

9431063
function optionOverlaySupported(profile) { return profile !== "no_overlay"; }
@@ -973,54 +1093,54 @@ function syncOptionOverlayForAccount(state, form, platform, account) {
9731093
}
9741094
}
9751095

976-
console.log("\n=== 11. syncOptionOverlayForAccount (解析为具体值) ===\n");
1096+
console.log("\n=== 12. syncOptionOverlayForAccount (解析为具体值) ===\n");
9771097

978-
// 11a: option_overlay_enabled: true → enabled
1098+
// 12a: option_overlay_enabled: true → enabled
9791099
{
9801100
const state = { currentStrategies: makeCurrentStrategies("ibkr", { extra: { option_overlay_enabled: true } }) };
9811101
const form = { optionOverlayMode: "current", optionOverlayTouched: false, strategy: "some_profile" };
9821102
syncOptionOverlayForAccount(state, form, "ibkr", makeAccount("preview"));
983-
assert(form.optionOverlayMode === "enabled", "11a: option overlay enabled → 'enabled'");
1103+
assert(form.optionOverlayMode === "enabled", "12a: option overlay enabled → 'enabled'");
9841104
}
9851105

986-
// 11b: option_overlay_enabled: false → disabled
1106+
// 12b: option_overlay_enabled: false → disabled
9871107
{
9881108
const state = { currentStrategies: makeCurrentStrategies("ibkr", { extra: { option_overlay_enabled: false } }) };
9891109
const form = { optionOverlayMode: "current", optionOverlayTouched: false, strategy: "some_profile" };
9901110
syncOptionOverlayForAccount(state, form, "ibkr", makeAccount("preview"));
991-
assert(form.optionOverlayMode === "disabled", "11b: option overlay disabled → 'disabled'");
1111+
assert(form.optionOverlayMode === "disabled", "12b: option overlay disabled → 'disabled'");
9921112
}
9931113

994-
// 11c: no entry + supported → enabled (default)
1114+
// 12c: no entry + supported → enabled (default)
9951115
{
9961116
const state = { currentStrategies: {} };
9971117
const form = { optionOverlayMode: "current", optionOverlayTouched: false, strategy: "some_profile" };
9981118
syncOptionOverlayForAccount(state, form, "ibkr", makeAccount("preview"));
999-
assert(form.optionOverlayMode === "enabled", "11c: no entry + supported → 'enabled'");
1119+
assert(form.optionOverlayMode === "enabled", "12c: no entry + supported → 'enabled'");
10001120
}
10011121

1002-
// 11d: not supported → disabled
1122+
// 12d: not supported → disabled
10031123
{
10041124
const state = { currentStrategies: makeCurrentStrategies("ibkr", { extra: { option_overlay_enabled: true } }) };
10051125
const form = { optionOverlayMode: "current", optionOverlayTouched: false, strategy: "no_overlay" };
10061126
syncOptionOverlayForAccount(state, form, "ibkr", makeAccount("preview"));
1007-
assert(form.optionOverlayMode === "disabled", "11d: not supported → 'disabled'");
1127+
assert(form.optionOverlayMode === "disabled", "12d: not supported → 'disabled'");
10081128
}
10091129

1010-
// 11e: account has explicit mode → use it
1130+
// 12e: account has explicit mode → use it
10111131
{
10121132
const form = { optionOverlayMode: "current", optionOverlayTouched: false, strategy: "some_profile" };
10131133
syncOptionOverlayForAccount({}, form, "ibkr", { key: "preview", option_overlay_mode: "disabled" });
1014-
assert(form.optionOverlayMode === "disabled", "11e: account explicit 'disabled' → 'disabled'");
1134+
assert(form.optionOverlayMode === "disabled", "12e: account explicit 'disabled' → 'disabled'");
10151135
}
10161136

10171137
// ============================================================
1018-
// 12. 互斥 UI 不再禁用选项 (回归测试)
1138+
// 13. 互斥 UI 不再禁用选项 (回归测试)
10191139
// ============================================================
10201140

1021-
console.log("\n=== 12. 互斥 UI 不再禁用选项 ===\n");
1141+
console.log("\n=== 13. 互斥 UI 不再禁用选项 ===\n");
10221142

1023-
// 12a: 预留现金覆盖活跃时,select cash-only=disabled 不应因 option.disabled 被阻挡
1143+
// 13a: 预留现金覆盖活跃时,select cash-only=disabled 不应因 option.disabled 被阻挡
10241144
// (此测试验证 reconcileExecutionCashPolicy 可处理用户选择,无需前置禁用)
10251145
{
10261146
const form = {
@@ -1036,13 +1156,13 @@ console.log("\n=== 12. 互斥 UI 不再禁用选项 ===\n");
10361156
form.cashOnlyExecutionTouched = true;
10371157
reconcileExecutionCashPolicy(form, "margin");
10381158
// 预留现金应被清除
1039-
assert(form.reservePolicyMode === "none", "12a: margin=yes → reserve cleared");
1040-
assert(form.cashOnlyExecutionMode === "disabled", "12a: margin stays 'disabled'");
1159+
assert(form.reservePolicyMode === "none", "13a: margin=yes → reserve cleared");
1160+
assert(form.cashOnlyExecutionMode === "disabled", "13a: margin stays 'disabled'");
10411161
// 不再冲突
1042-
assert(executionCashPolicyConflict(form) === false, "12a: no conflict after reconciliation");
1162+
assert(executionCashPolicyConflict(form) === false, "13a: no conflict after reconciliation");
10431163
}
10441164

1045-
// 12b: margin 启用时,reserve 下拉框不应 disabled,用户可选 ratio
1165+
// 13b: margin 启用时,reserve 下拉框不应 disabled,用户可选 ratio
10461166
{
10471167
const form = {
10481168
cashOnlyExecutionMode: "disabled", // margin enabled
@@ -1058,18 +1178,18 @@ console.log("\n=== 12. 互斥 UI 不再禁用选项 ===\n");
10581178
form.reservedCashTouched = true;
10591179
reconcileExecutionCashPolicy(form, "reserve");
10601180
// 融资应被禁用
1061-
assert(form.cashOnlyExecutionMode === "enabled", "12b: reserve=ratio → margin disabled");
1062-
assert(form.reservePolicyMode === "ratio", "12b: reserve stays 'ratio'");
1063-
assert(executionCashPolicyConflict(form) === false, "12b: no conflict after reconciliation");
1181+
assert(form.cashOnlyExecutionMode === "enabled", "13b: reserve=ratio → margin disabled");
1182+
assert(form.reservePolicyMode === "ratio", "13b: reserve stays 'ratio'");
1183+
assert(executionCashPolicyConflict(form) === false, "13b: no conflict after reconciliation");
10641184
}
10651185

10661186
// ============================================================
1067-
// 13. 融资切换 save/restore 预留现金配置
1187+
// 14. 融资切换 save/restore 预留现金配置
10681188
// ============================================================
10691189

1070-
console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
1190+
console.log("\n=== 14. Save/Restore 预留现金配置 ===\n");
10711191

1072-
// 13a: 用户开融资 → 值保留 → 关融资 → 值恢复
1192+
// 14a: 用户开融资 → 值保留 → 关融资 → 值恢复
10731193
{
10741194
const form = {
10751195
cashOnlyExecutionMode: "enabled", // 当前:不允许融资
@@ -1083,20 +1203,20 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
10831203
form.cashOnlyExecutionMode = "disabled";
10841204
form.cashOnlyExecutionTouched = true;
10851205
reconcileExecutionCashPolicy(form, "margin");
1086-
assert(form.reservePolicyMode === "none", "13a-step1: reserve cleared to 'none'");
1087-
assert(form.reservedCashRatio === "0.05", "13a-step1: ratio value preserved");
1088-
assert(form._prevReserve.mode === "ratio", "13a-step1: prev mode saved");
1089-
assert(form._prevReserve.ratio === "0.05", "13a-step1: prev ratio saved");
1206+
assert(form.reservePolicyMode === "none", "14a-step1: reserve cleared to 'none'");
1207+
assert(form.reservedCashRatio === "0.05", "14a-step1: ratio value preserved");
1208+
assert(form._prevReserve.mode === "ratio", "14a-step1: prev mode saved");
1209+
assert(form._prevReserve.ratio === "0.05", "14a-step1: prev ratio saved");
10901210

10911211
// 用户选"允许融资: 否" → 恢复
10921212
form.cashOnlyExecutionMode = "enabled";
10931213
restoreReserveAfterMarginDisabled(form);
1094-
assert(form.reservePolicyMode === "ratio", "13a-step2: reserve restored to 'ratio'");
1095-
assert(form.reservedCashRatio === "0.05", "13a-step2: ratio restored");
1096-
assert(form._prevReserve === undefined, "13a-step2: _prevReserve cleaned up");
1214+
assert(form.reservePolicyMode === "ratio", "14a-step2: reserve restored to 'ratio'");
1215+
assert(form.reservedCashRatio === "0.05", "14a-step2: ratio restored");
1216+
assert(form._prevReserve === undefined, "14a-step2: _prevReserve cleaned up");
10971217
}
10981218

1099-
// 13b: 用户开融资 → 关融资 → 手动改 reserve → 再开再关不复原旧值
1219+
// 14b: 用户开融资 → 关融资 → 手动改 reserve → 再开再关不复原旧值
11001220
{
11011221
const form = {
11021222
cashOnlyExecutionMode: "enabled",
@@ -1109,13 +1229,13 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
11091229
// 开融资
11101230
form.cashOnlyExecutionMode = "disabled";
11111231
reconcileExecutionCashPolicy(form, "margin");
1112-
assert(form.reservePolicyMode === "none", "13b-step1: cleared");
1113-
assert(form._prevReserve.mode === "max", "13b-step1: prev 'max' saved");
1232+
assert(form.reservePolicyMode === "none", "14b-step1: cleared");
1233+
assert(form._prevReserve.mode === "max", "14b-step1: prev 'max' saved");
11141234

11151235
// 关融资 → 恢复
11161236
form.cashOnlyExecutionMode = "enabled";
11171237
restoreReserveAfterMarginDisabled(form);
1118-
assert(form.reservePolicyMode === "max", "13b-step2: restored to 'max'");
1238+
assert(form.reservePolicyMode === "max", "14b-step2: restored to 'max'");
11191239

11201240
// 用户手动改为 floor
11211241
form.reservePolicyMode = "floor";
@@ -1126,17 +1246,17 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
11261246
// 再开融资
11271247
form.cashOnlyExecutionMode = "disabled";
11281248
reconcileExecutionCashPolicy(form, "margin");
1129-
assert(form._prevReserve.mode === "floor", "13b-step3: prev 'floor' saved");
1130-
assert(form._prevReserve.floor === "5000", "13b-step3: prev floor saved");
1249+
assert(form._prevReserve.mode === "floor", "14b-step3: prev 'floor' saved");
1250+
assert(form._prevReserve.floor === "5000", "14b-step3: prev floor saved");
11311251

11321252
// 再关融资 → 恢复 floor
11331253
form.cashOnlyExecutionMode = "enabled";
11341254
restoreReserveAfterMarginDisabled(form);
1135-
assert(form.reservePolicyMode === "floor", "13b-step4: restored to 'floor'");
1136-
assert(form.minReservedCashUsd === "5000", "13b-step4: floor restored");
1255+
assert(form.reservePolicyMode === "floor", "14b-step4: restored to 'floor'");
1256+
assert(form.minReservedCashUsd === "5000", "14b-step4: floor restored");
11371257
}
11381258

1139-
// 13c: reserve=none 时开融资不应保存 _prevReserve
1259+
// 14c: reserve=none 时开融资不应保存 _prevReserve
11401260
{
11411261
const form = {
11421262
cashOnlyExecutionMode: "enabled",
@@ -1147,10 +1267,10 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
11471267
};
11481268
form.cashOnlyExecutionMode = "disabled";
11491269
reconcileExecutionCashPolicy(form, "margin");
1150-
assert(form._prevReserve === undefined, "13c: reserve=none → no _prevReserve saved");
1270+
assert(form._prevReserve === undefined, "14c: reserve=none → no _prevReserve saved");
11511271
}
11521272

1153-
// 13d: margin 切到 "enabled" 但无 _prevReserve → 不动
1273+
// 14d: margin 切到 "enabled" 但无 _prevReserve → 不动
11541274
{
11551275
const form = {
11561276
cashOnlyExecutionMode: "disabled",
@@ -1161,10 +1281,10 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
11611281
};
11621282
form.cashOnlyExecutionMode = "enabled";
11631283
restoreReserveAfterMarginDisabled(form);
1164-
assert(form.reservePolicyMode === "none", "13d: no _prevReserve → stays 'none'");
1284+
assert(form.reservePolicyMode === "none", "14d: no _prevReserve → stays 'none'");
11651285
}
11661286

1167-
// 13e: account switch 清除 _prevReserve
1287+
// 14e: account switch 清除 _prevReserve
11681288
{
11691289
const form = {
11701290
cashOnlyExecutionMode: "disabled",
@@ -1174,7 +1294,7 @@ console.log("\n=== 13. Save/Restore 预留现金配置 ===\n");
11741294
_prevReserve: { mode: "max", floor: "10000", ratio: "0.05" },
11751295
};
11761296
delete form._prevReserve; // simulate account switch
1177-
assert(form._prevReserve === undefined, "13e: account switch clears _prevReserve");
1297+
assert(form._prevReserve === undefined, "14e: account switch clears _prevReserve");
11781298
}
11791299

11801300
// ============================================================

0 commit comments

Comments
 (0)