// Checks for kept rules — which of an account type's rule sets an account // trades on (lib/kept-rules.ts). // // npm run check // // lib/kept-rules.ts has only `import type`s, so Node strips its types and // loads it directly; .mjs keeps this file out of Next's type check. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { RULE_FIELDS, IDENTITY_FIELDS, PROJECTION_FIELDS, normalizeAccountType, pickKeptRules, overlayKept, ruleDiffs, stampRulesDate, evidenceRulesDate, lowersRulesDate, sortableIso, chicagoDay, chicagoDayStart, evidenceDay, openingEvidence, identityStillMatches, pickedDayRulesDate, unchangedDaySaves, } from '../lib/kept-rules.ts'; const LIVE = { id: 'live-x', firmId: 'firm-1', label: '50K Eval', prefix: 'TDFY', accountSize: 50_000, profitTarget: 2500, consistency: 0.4, minDayPnL: 150, minTradingDays: 3, maxLoss: 2000, maxPositionSize: 5, targetSameEquity: false, splitEnabled: true, minLegs: 2, firstDayStop: true, minDayStop: false, combineCmeHolidays: false, withdrawalStages: [{ profit: 2000, consistency: 0.4, minTradingDays: 5, drain: false, minDayPnL: 0 }], payoutable: true, trailingDrawdown: 2000, expectedPayout: 1000, withdrawalPct: 0.5, payoutCap: 3000, profitSplit: 0.9, dailyLossLimit: 1000, stagesSameDay: false, tradesEveryDay: false, updatedAt: '2026-10-18T14:05:00.000Z', }; /** `obj` without `keys`. */ const omit = (obj, keys) => Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k))); /** A kept set as the master builds it: the live type, the old rules on top. */ function keptOf(rules, window, id = 'kept-1') { return { ...LIVE, ...rules, id, keptFrom: LIVE.id, createdAt: '2026-01-01T00:00:00.000Z', openedAt: window.openedAt, closedAt: window.closedAt, }; } // ── field classes ─────────────────────────────────────────────────────────── test('field classes: the spec\'s lists, identical to the master\'s, and disjoint', () => { assert.deepEqual([...RULE_FIELDS], [ 'profitTarget', 'consistency', 'minDayPnL', 'minTradingDays', 'maxLoss', 'maxPositionSize', 'targetSameEquity', 'splitEnabled', 'minLegs', 'firstDayStop', 'minDayStop', 'combineCmeHolidays', 'withdrawalStages', 'payoutable', 'trailingDrawdown', 'dailyLossLimit', 'stagesSameDay', ]); assert.deepEqual([...IDENTITY_FIELDS], ['prefix', 'accountSize']); assert.deepEqual([...PROJECTION_FIELDS], ['expectedPayout', 'withdrawalPct', 'payoutCap', 'profitSplit']); const all = [...RULE_FIELDS, ...IDENTITY_FIELDS, ...PROJECTION_FIELDS, 'label']; assert.equal(new Set(all).size, all.length); // Every field of a catalog account type is in exactly one class, or is // bookkeeping the live type always owns. const bookkeeping = ['id', 'firmId', 'updatedAt', 'tradesEveryDay']; assert.deepEqual(Object.keys(LIVE).filter((k) => !all.includes(k) && !bookkeeping.includes(k)), []); }); // ── normalizeAccountType: exactly the old validatePayload callback ────────── /** lib/firm-catalog.ts validatePayload's per-type callback before the move * (git show HEAD:lib/firm-catalog.ts), verbatim but for the types. */ function legacyNormalize(t, j, tradesEveryDay) { const nonNegative = (value) => (typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0); const isStages = (value) => Array.isArray(value) && value.every((s) => typeof s === 'object' && s !== null && typeof s.profit === 'number' && typeof s.consistency === 'number' && typeof s.minTradingDays === 'number'); if (typeof t !== 'object' || t === null) throw new Error(`account type ${j} is not an object`); const type = t; for (const key of ['id', 'firmId', 'label', 'prefix', 'updatedAt']) { if (typeof type[key] !== 'string') throw new Error(`account type ${j} is missing ${key}`); } for (const key of [ 'accountSize', 'profitTarget', 'consistency', 'minDayPnL', 'minTradingDays', 'maxLoss', 'maxPositionSize', ]) { if (typeof type[key] !== 'number') throw new Error(`account type ${j} is missing ${key}`); } if (!isStages(type.withdrawalStages)) throw new Error(`account type ${j} has bad withdrawalStages`); return { ...type, splitEnabled: type.splitEnabled !== false, combineCmeHolidays: type.combineCmeHolidays === true && !tradesEveryDay, minLegs: typeof type.minLegs === 'number' ? type.minLegs : 0, firstDayStop: type.firstDayStop === true, minDayStop: type.minDayStop === true, dailyLossLimit: nonNegative(type.dailyLossLimit), stagesSameDay: type.stagesSameDay === true, tradesEveryDay, }; } function outcome(fn) { try { return { ok: JSON.stringify(fn()) }; } catch (err) { return { error: err.message }; } } test('normalizeAccountType is the old per-type validation, byte for byte (values, key order, errors)', () => { const wire = omit(LIVE, ['tradesEveryDay']); const variants = [ wire, // An old master: none of the lenient fields. omit(wire, ['splitEnabled', 'minLegs', 'firstDayStop', 'minDayStop', 'combineCmeHolidays', 'dailyLossLimit']), { ...wire, splitEnabled: false, combineCmeHolidays: true, minLegs: '3', firstDayStop: 'yes', dailyLossLimit: -5 }, { ...wire, dailyLossLimit: NaN, minDayStop: true, extra: { nested: [1, 2] } }, { ...wire, withdrawalStages: [] }, // Malformed: each must throw the same message. null, 'type', 42, { ...wire, id: 7 }, { ...wire, label: undefined }, { ...wire, updatedAt: null }, { ...wire, accountSize: '50000' }, { ...wire, maxPositionSize: undefined }, { ...wire, withdrawalStages: [{ profit: 1, consistency: 0.5 }] }, { ...wire, withdrawalStages: 'none' }, { ...wire, withdrawalStages: [null] }, ]; let errors = 0; for (const tradesEveryDay of [false, true]) { variants.forEach((v, j) => { const legacy = outcome(() => legacyNormalize(v, j, tradesEveryDay)); const now = outcome(() => normalizeAccountType(v, { tradesEveryDay, where: `account type ${j}` })); assert.deepEqual(now, legacy, `variant ${j}, every day ${tradesEveryDay}`); if (legacy.error) errors++; }); } assert.equal(errors, 2 * 11); // The kept-set wording only changes the name in the message. assert.throws(() => normalizeAccountType({ ...wire, prefix: 1 }, { tradesEveryDay: false, where: 'kept rules 0 of Tradeify' }), { message: 'kept rules 0 of Tradeify is missing prefix' }); }); // ── pickKeptRules ─────────────────────────────────────────────────────────── const G1 = { id: 'g1', openedAt: null, closedAt: '2026-10-02T15:00:00.000Z' }; const G2 = { id: 'g2', openedAt: '2026-10-02T15:00:00.000Z', closedAt: '2026-10-18T14:05:00.000Z' }; const G3 = { id: 'g3', openedAt: '2026-10-18T14:05:00.000Z', closedAt: '2026-10-18T19:30:00.000Z' }; const GENERATIONS = [G1, G2, G3]; test('pickKeptRules: the window holding the date, [openedAt, closedAt)', () => { const pick = (d, sets = GENERATIONS) => pickKeptRules(sets, d)?.id ?? null; // No lower bound on the first generation. assert.equal(pick('2020-01-01T00:00:00.000Z'), 'g1'); assert.equal(pick('2026-10-02T14:59:59.999Z'), 'g1'); // A date exactly at a cutoff is on the newer side: closedAt excluded, // openedAt included. assert.equal(pick('2026-10-02T15:00:00.000Z'), 'g2'); assert.equal(pick('2026-10-18T14:04:59.999Z'), 'g2'); assert.equal(pick('2026-10-18T14:05:00.000Z'), 'g3'); assert.equal(pick('2026-10-18T19:29:59.999Z'), 'g3'); // From the last cutoff on: the current rules. assert.equal(pick('2026-10-18T19:30:00.000Z'), null); assert.equal(pick('2027-01-01T00:00:00.000Z'), null); // Compared as times, not strings: other ISO spellings of the same instant. assert.equal(pick('2026-10-02T10:00:00-05:00'), 'g2'); assert.equal(pick('2026-10-02T09:59:59.999-05:00'), 'g1'); // Order of the list doesn't matter for disjoint windows. assert.equal(pick('2026-10-10T00:00:00.000Z', [G3, G1, G2]), 'g2'); // Returns the set itself (the generic carries through). assert.equal(pickKeptRules(GENERATIONS, '2026-10-10T00:00:00.000Z'), G2); }); test('pickKeptRules: no date, no sets, or an unreadable date — the current rules', () => { assert.equal(pickKeptRules(GENERATIONS, null), null); assert.equal(pickKeptRules(GENERATIONS, ''), null); assert.equal(pickKeptRules(GENERATIONS, 'not a date'), null); assert.equal(pickKeptRules(undefined, '2026-10-10T00:00:00.000Z'), null); assert.equal(pickKeptRules([], '2026-10-10T00:00:00.000Z'), null); // An unreadable bound never matches (the catalog parse rejects them anyway). assert.equal(pickKeptRules([{ id: 'bad', openedAt: null, closedAt: 'soon' }], '2020-01-01T00:00:00.000Z'), null); assert.equal(pickKeptRules([{ id: 'bad', openedAt: 'once', closedAt: '2030-01-01T00:00:00.000Z' }], '2026-01-01T00:00:00.000Z'), null); // An empty window (openedAt == closedAt) holds nothing. assert.equal(pickKeptRules([{ id: 'empty', openedAt: G2.closedAt, closedAt: G2.closedAt }], G2.closedAt), null); }); // ── overlayKept ───────────────────────────────────────────────────────────── test('overlayKept: the kept rules on top; identity, label, projections and bookkeeping stay live', () => { const oldRules = { profitTarget: 3000, consistency: 0.5, minDayPnL: 200, minTradingDays: 5, maxLoss: 2500, maxPositionSize: 3, targetSameEquity: true, splitEnabled: false, minLegs: 0, firstDayStop: false, minDayStop: true, combineCmeHolidays: true, withdrawalStages: [], payoutable: false, trailingDrawdown: 2500, dailyLossLimit: 0, stagesSameDay: false, }; // A master that sent other identity/projection values on the kept set: // they must not leak through. const kept = keptOf({ ...oldRules, label: 'OLD', prefix: 'OLD', accountSize: 1, expectedPayout: 1, withdrawalPct: 1, payoutCap: 1, profitSplit: 1, firmId: 'other', updatedAt: '1999-01-01T00:00:00.000Z', tradesEveryDay: true }, G2); const liveBefore = structuredClone(LIVE); const keptBefore = structuredClone(kept); const eff = overlayKept(LIVE, kept); for (const field of RULE_FIELDS) assert.deepEqual(eff[field], oldRules[field], field); for (const field of ['id', 'firmId', 'label', 'prefix', 'accountSize', 'updatedAt', 'tradesEveryDay', ...PROJECTION_FIELDS]) { assert.deepEqual(eff[field], LIVE[field], field); } assert.deepEqual(eff.keptRules, { id: 'kept-1', openedAt: G2.openedAt, closedAt: G2.closedAt }); // Nothing of the kept set's own bookkeeping comes along. for (const key of ['keptFrom', 'openedAt', 'closedAt', 'createdAt']) assert.equal(key in eff, false, key); assert.deepEqual(Object.keys(eff).sort(), [...Object.keys(LIVE), 'keptRules'].sort()); // Inputs untouched. assert.deepEqual(LIVE, liveBefore); assert.deepEqual(kept, keptBefore); }); test('overlayKept: a rule field the kept set lacks keeps the live value', () => { const kept = keptOf({ consistency: 0.5 }, G1); delete kept.payoutable; kept.trailingDrawdown = undefined; const eff = overlayKept(LIVE, kept); assert.equal(eff.consistency, 0.5); assert.equal(eff.payoutable, LIVE.payoutable); assert.equal(eff.trailingDrawdown, LIVE.trailingDrawdown); }); // ── ruleDiffs ─────────────────────────────────────────────────────────────── test('ruleDiffs: the rule fields that differ, raw values both sides', () => { assert.deepEqual(ruleDiffs(LIVE, LIVE), []); const eff = overlayKept(LIVE, keptOf({ consistency: 0.5, profitTarget: 3000 }, G1)); assert.deepEqual(ruleDiffs(eff, LIVE), [ { field: 'profitTarget', kept: 3000, now: 2500 }, { field: 'consistency', kept: 0.5, now: 0.4 }, ]); // Non-rule fields never count. assert.deepEqual(ruleDiffs({ ...LIVE, label: 'x', prefix: 'y', profitSplit: 0.1 }, LIVE), []); const stages = [{ profit: 3000, consistency: 0.4, minTradingDays: 5, drain: false, minDayPnL: 0 }]; assert.deepEqual(ruleDiffs({ ...LIVE, withdrawalStages: stages }, LIVE), [ { field: 'withdrawalStages', kept: stages, now: LIVE.withdrawalStages }, ]); }); test('ruleDiffs: stored JSON from before a field existed is not a change', () => { // A stage without drain / minDayPnL is what parseAccountTypeInput fills // with drain false, minDayPnL 0 (5 for a drain stage); key order is free. const bare = { ...LIVE, withdrawalStages: [{ minTradingDays: 5, consistency: 0.4, profit: 2000 }] }; assert.deepEqual(ruleDiffs(bare, LIVE), []); const drainFilled = { ...LIVE, withdrawalStages: [{ profit: 0, consistency: 0, minTradingDays: 5, drain: true, minDayPnL: 5 }] }; const drainBare = { ...LIVE, withdrawalStages: [{ profit: 0, consistency: 0, minTradingDays: 5, drain: true }] }; assert.deepEqual(ruleDiffs(drainBare, drainFilled), []); // A per-stage payout override is a rule. assert.equal(ruleDiffs({ ...LIVE, withdrawalStages: [{ ...LIVE.withdrawalStages[0], payoutCap: 1500 }] }, LIVE).length, 1); // An absent optional flag or amount is its default. const noOptionals = omit(LIVE, ['payoutable', 'trailingDrawdown']); assert.deepEqual(ruleDiffs(noOptionals, { ...LIVE, payoutable: false, trailingDrawdown: 0 }), []); assert.deepEqual(ruleDiffs({ ...LIVE, splitEnabled: undefined }, LIVE), []); assert.deepEqual(ruleDiffs({ ...LIVE, splitEnabled: undefined }, { ...LIVE, splitEnabled: false }).map((d) => d.field), ['splitEnabled']); }); // ── rules dates ───────────────────────────────────────────────────────────── test('stampRulesDate: the latest of this clock, the master\'s and the catalog\'s last change', () => { const now = Date.parse('2026-10-18T14:00:00.000Z'); assert.equal(stampRulesDate(now, null, null), '2026-10-18T14:00:00.000Z'); // This clock 5 minutes behind the master's. assert.equal(stampRulesDate(now, 5 * 60_000, null), '2026-10-18T14:05:00.000Z'); // This clock ahead: its own time stands. assert.equal(stampRulesDate(now, -5 * 60_000, null), '2026-10-18T14:00:00.000Z'); // The catalog changed after both: no new account dates before it. assert.equal(stampRulesDate(now, 60_000, '2026-10-18T14:07:30.000Z'), '2026-10-18T14:07:30.000Z'); assert.equal(stampRulesDate(now, null, '2026-10-01T00:00:00.000Z'), '2026-10-18T14:00:00.000Z'); // Unreadable inputs are ignored. assert.equal(stampRulesDate(now, NaN, 'never'), '2026-10-18T14:00:00.000Z'); assert.equal(stampRulesDate(now, null, ''), '2026-10-18T14:00:00.000Z'); }); test('stampRulesDate: a machine whose clock is behind never dates a new account inside the set just closed', () => { // The owner saves "new accounts only" at 14:05 master time; this machine's // clock reads 14:01 when it first sees a brand-new account at 14:05. const cutoff = '2026-10-18T14:05:00.000Z'; const kept = [{ id: 'g', openedAt: null, closedAt: cutoff }]; const local = Date.parse('2026-10-18T14:01:00.000Z'); // Unclamped, the account would land in the old rules. assert.equal(pickKeptRules(kept, new Date(local).toISOString())?.id, 'g'); // With the master's offset, or just the catalog's updatedAt, it doesn't. assert.equal(pickKeptRules(kept, stampRulesDate(local, 4 * 60_000, null)), null); assert.equal(pickKeptRules(kept, stampRulesDate(local, null, cutoff)), null); // A machine whose master offset isn't known yet still clamps to the catalog. assert.equal(pickKeptRules(kept, stampRulesDate(local, null, '2026-10-18T14:05:00.250Z')), null); }); test('evidenceRulesDate: the end of the first fund day in Chicago — CDT and CST', () => { // CDT (UTC−5): 23:59:59.999 CDT is 04:59:59.999Z the next day. assert.equal(evidenceRulesDate('2026-10-18'), '2026-10-19T04:59:59.999Z'); assert.equal(evidenceRulesDate('2026-07-04'), '2026-07-05T04:59:59.999Z'); // CST (UTC−6): 05:59:59.999Z the next day. assert.equal(evidenceRulesDate('2026-01-15'), '2026-01-16T05:59:59.999Z'); assert.equal(evidenceRulesDate('2026-12-31'), '2027-01-01T05:59:59.999Z'); assert.equal(evidenceRulesDate('2028-02-29'), '2028-03-01T05:59:59.999Z'); for (const bad of [null, undefined, '', '2026-10-18T00:00:00Z', '18/10/2026', '2026-1-8', '2026-02-31', '2026-13-01', 20261018]) { assert.equal(evidenceRulesDate(bad), null, String(bad)); } }); test('evidenceRulesDate: DST boundary days end on the right side of the change', () => { // 2026: CST→CDT at 2 AM on Sunday Mar 8, CDT→CST at 2 AM on Sunday Nov 1. assert.equal(evidenceRulesDate('2026-03-07'), '2026-03-08T05:59:59.999Z'); // last CST day assert.equal(evidenceRulesDate('2026-03-08'), '2026-03-09T04:59:59.999Z'); // springs forward: ends in CDT assert.equal(evidenceRulesDate('2026-03-09'), '2026-03-10T04:59:59.999Z'); assert.equal(evidenceRulesDate('2026-10-31'), '2026-11-01T04:59:59.999Z'); // last CDT day assert.equal(evidenceRulesDate('2026-11-01'), '2026-11-02T05:59:59.999Z'); // falls back: ends in CST assert.equal(evidenceRulesDate('2026-11-02'), '2026-11-03T05:59:59.999Z'); // The day before each boundary and the boundary day are 23 h / 25 h long // in Chicago, measured end to end. const len = (a, b) => (Date.parse(evidenceRulesDate(b)) - Date.parse(evidenceRulesDate(a))) / 3_600_000; assert.equal(len('2026-03-07', '2026-03-08'), 23); assert.equal(len('2026-10-31', '2026-11-01'), 25); assert.equal(len('2026-06-01', '2026-06-02'), 24); }); test('evidenceRulesDate: every day of 2026–2027 ends 1 ms before the next one starts, in Chicago', () => { for (let ms = Date.UTC(2026, 0, 1); ms < Date.UTC(2028, 0, 1); ms += 86_400_000) { const day = new Date(ms).toISOString().slice(0, 10); const next = new Date(ms + 86_400_000).toISOString().slice(0, 10); const end = evidenceRulesDate(day); assert.equal(Date.parse(chicagoDayStart(next)) - Date.parse(end), 1, day); // Both read back as their own Chicago day, and the end as evidence. assert.equal(chicagoDay(end), day); assert.equal(chicagoDay(chicagoDayStart(next)), next); assert.equal(evidenceDay(end), day); // Under either offset the end is 04:59:59.999Z or 05:59:59.999Z the next UTC day. assert.match(end, new RegExp(`^${next}T0[45]:59:59\\.999Z$`)); } }); test('evidenceRulesDate: an account opened on the day of a change — even that evening — counts as after it', () => { // A change at 14:05 CDT on Oct 18: an account first funded that day is // after it; one funded the day before keeps the old rules. const afternoon = [{ id: 'g', openedAt: null, closedAt: '2026-10-18T19:05:00.000Z' }]; assert.equal(pickKeptRules(afternoon, evidenceRulesDate('2026-10-18')), null); assert.equal(pickKeptRules(afternoon, evidenceRulesDate('2026-10-17'))?.id, 'g'); // A change at 8 PM CDT (01:00Z the next UTC day): a UTC end of day // (Oct 18 23:59:59.999Z = 6:59 PM CDT) would have put an account that // opened that evening before it. const evening = [{ id: 'g', openedAt: null, closedAt: '2026-10-19T01:00:00.000Z' }]; assert.equal(pickKeptRules(evening, evidenceRulesDate('2026-10-18')), null); assert.equal(pickKeptRules(evening, evidenceRulesDate('2026-10-17'))?.id, 'g'); // Same under CST: a change at 11:30 PM CST Jan 15 (05:30Z Jan 16). const winter = [{ id: 'g', openedAt: null, closedAt: '2026-01-16T05:30:00.000Z' }]; assert.equal(pickKeptRules(winter, evidenceRulesDate('2026-01-15')), null); assert.equal(pickKeptRules(winter, evidenceRulesDate('2026-01-14'))?.id, 'g'); }); test('chicagoDayStart / chicagoDay / evidenceDay', () => { assert.equal(chicagoDayStart('2026-10-18'), '2026-10-18T05:00:00.000Z'); assert.equal(chicagoDayStart('2026-01-15'), '2026-01-15T06:00:00.000Z'); assert.equal(chicagoDayStart('2026-03-08'), '2026-03-08T06:00:00.000Z'); // midnight is still CST assert.equal(chicagoDayStart('2026-11-01'), '2026-11-01T05:00:00.000Z'); // midnight is still CDT assert.equal(chicagoDayStart('2026-02-31'), null); assert.equal(chicagoDayStart('1999-12-31'), null); // before 2000: not a rules date assert.equal(chicagoDay('2026-10-19T04:59:59.999Z'), '2026-10-18'); assert.equal(chicagoDay('2026-10-19T05:00:00.000Z'), '2026-10-19'); assert.equal(chicagoDay('nope'), null); // Only the exact end of a Chicago day reads as evidence. assert.equal(evidenceDay('2026-10-19T04:59:59.999Z'), '2026-10-18'); assert.equal(evidenceDay('2026-10-19T04:59:59.998Z'), null); assert.equal(evidenceDay('2026-10-18T23:59:59.999Z'), null); // the old UTC shape assert.equal(evidenceDay('2026-10-18T19:05:00.000Z'), null); }); test('sortableIso: plain four-digit-year ISO from 2000 on, else null', () => { assert.equal(sortableIso(Date.parse('2026-10-18T14:05:00Z')), '2026-10-18T14:05:00.000Z'); assert.equal(sortableIso(Date.UTC(2000, 0, 1)), '2000-01-01T00:00:00.000Z'); assert.equal(sortableIso(Date.UTC(2000, 0, 1) - 1), null); assert.equal(sortableIso(0), null); assert.equal(sortableIso(Date.UTC(9999, 11, 31, 23, 59, 59, 999)), '9999-12-31T23:59:59.999Z'); assert.equal(sortableIso(Date.parse('+010000-01-01T00:00:00Z')), null); // would print as +010000-… assert.equal(sortableIso(8.64e15 + 1), null); assert.equal(sortableIso(NaN), null); assert.equal(sortableIso(Infinity), null); }); test('openingEvidence: a fund day ends in Chicago; the registration fallback is the instant itself', () => { // A positive fund row's day: the end of it in Chicago, whatever the registration. assert.equal(openingEvidence('2026-10-02', Date.parse('2026-09-01T00:00:00Z')), '2026-10-03T04:59:59.999Z'); // Positive rows present but unreadable: no evidence (never the registration). assert.equal(openingEvidence('2026-02-31', Date.parse('2026-09-01T00:00:00Z')), null); // No positive rows: the registration instant, exactly. assert.equal(openingEvidence(undefined, Date.parse('2026-10-19T02:00:00Z')), '2026-10-19T02:00:00.000Z'); // Unreadable or implausible registrations: none. for (const bad of [undefined, null, NaN, 0, 5, -1, Infinity, Date.parse('1999-12-31T23:00:00Z'), 9e15]) { assert.equal(openingEvidence(undefined, bad), null, String(bad)); } }); test('openingEvidence: a DXtrade account registered after an evening cutoff is never lowered before it', () => { // "New accounts only" saved at 8 PM CDT Oct 18 (01:00Z Oct 19); a DX // account registered at 9 PM CDT (02:00Z) with no DEPOSIT row. const kept = [{ id: 'g', openedAt: null, closedAt: '2026-10-19T01:00:00.000Z' }]; const stamped = '2026-10-19T02:00:05.000Z'; const evidence = openingEvidence(undefined, Date.parse('2026-10-19T02:00:00Z')); const lowered = lowersRulesDate({ rules_date: stamped, rules_date_manual: 0 }, evidence) ? evidence : stamped; assert.equal(pickKeptRules(kept, lowered), null); // And at 9 PM CDT Oct 20 against an 8 PM cutoff that day. const kept2 = [{ id: 'g', openedAt: null, closedAt: '2026-10-21T01:00:00.000Z' }]; assert.equal(pickKeptRules(kept2, openingEvidence(undefined, Date.parse('2026-10-21T02:00:00Z'))), null); // One registered before the cutoff, first seen after it, goes back to the old rules. const before = openingEvidence(undefined, Date.parse('2026-10-19T00:30:00Z')); assert.equal(lowersRulesDate({ rules_date: stamped, rules_date_manual: 0 }, before), true); assert.equal(pickKeptRules(kept, before)?.id, 'g'); }); test('identityStillMatches: prefix and the cached size against the live type', () => { const live = { prefix: 'TDFY', accountSize: 50_000 }; assert.equal(identityStillMatches('TDFY1234', '50000', live), true); assert.equal(identityStillMatches('TDFY1234', '50000.004', live), true); // A size edit on the type (50K → 100K, or → $0-start): no longer its size. assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, accountSize: 100_000 }), false); assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, accountSize: 0 }), false); // A prefix edit. assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, prefix: 'TDFX' }), false); // A $0-start type caches 0 when it matches. assert.equal(identityStillMatches('XFA77', '0', { prefix: 'XFA', accountSize: 0 }), true); assert.equal(identityStillMatches('XFA77', '150000', { prefix: 'XFA', accountSize: 0 }), false); // Nothing cached, or junk: not provably the same account. assert.equal(identityStillMatches('TDFY1234', null, live), false); assert.equal(identityStillMatches('TDFY1234', 'abc', live), false); assert.equal(identityStillMatches('TDFY1234', '', { ...live, accountSize: 0 }), false); }); test('pickedDayRulesDate: today is now; another day its start, never before Chicago\'s', () => { const nowMs = Date.parse('2026-10-18T19:30:00.000Z'); const at = (localMidnightMs, today = '2026-10-18') => ({ today, nowMs, localMidnightMs }); // Today: now, whatever the zone. assert.equal(pickedDayRulesDate('2026-10-18', at(Date.parse('2026-10-18T05:00:00Z'))), '2026-10-18T19:30:00.000Z'); // Chicago browser: its own midnight. assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse('2026-10-15T05:00:00Z'))), '2026-10-15T05:00:00.000Z'); // Pacific browser: local midnight is later than Chicago's — stands. assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse('2026-10-15T07:00:00Z'))), '2026-10-15T07:00:00.000Z'); // London (BST), Eastern and Karachi browsers: raised to Chicago's midnight. for (const local of ['2026-10-14T23:00:00Z', '2026-10-15T04:00:00Z', '2026-10-14T19:00:00Z']) { assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse(local))), '2026-10-15T05:00:00.000Z', local); } assert.equal(pickedDayRulesDate('2026-02-31', at(0)), null); assert.equal(pickedDayRulesDate('2026-10-15', at(NaN)), null); }); test('pickedDayRulesDate: a picked day never lands inside the evidence of the day before', () => { // A browser at UTC+1 picks Oct 15 for a set closing at Chicago midnight // of Oct 15: an account first funded Oct 14 stays on the old rules, the // hand-dated one on Oct 15 does not. const kept = [{ id: 'g', openedAt: null, closedAt: chicagoDayStart('2026-10-15') }]; const picked = pickedDayRulesDate('2026-10-15', { today: '2026-10-18', nowMs: Date.parse('2026-10-18T12:00:00Z'), localMidnightMs: Date.parse('2026-10-14T23:00:00Z'), }); assert.equal(pickKeptRules(kept, evidenceRulesDate('2026-10-14'))?.id, 'g'); assert.equal(pickKeptRules(kept, picked), null); for (let ms = Date.UTC(2026, 0, 2); ms < Date.UTC(2027, 0, 1); ms += 86_400_000) { const day = new Date(ms).toISOString().slice(0, 10); const prev = new Date(ms - 86_400_000).toISOString().slice(0, 10); for (const offsetH of [-10, -8, -6, -5, -4, 0, 1, 5, 9, 14]) { const localMidnightMs = Date.parse(`${day}T00:00:00Z`) - offsetH * 3_600_000; const stored = pickedDayRulesDate(day, { today: '2030-01-01', nowMs: 0, localMidnightMs }); assert.ok(stored > evidenceRulesDate(prev), `${day} at UTC${offsetH}`); } } }); test('unchangedDaySaves: an untouched day saves only when it moves the account to other rules', () => { // A change at 2:05 PM CDT today (19:05Z) with the default cutoff "now". const kept = [{ id: 'g', openedAt: null, closedAt: '2026-10-18T19:05:00.000Z' }]; const at = { today: '2026-10-18', nowMs: Date.parse('2026-10-18T19:30:00Z'), localMidnightMs: Date.parse('2026-10-18T05:00:00Z') }; // Dated this morning (old rules): ✓ on today stores now → current rules. const now = pickedDayRulesDate('2026-10-18', at); assert.equal(unchangedDaySaves(kept, '2026-10-18T15:00:00.000Z', now), true); assert.equal(pickKeptRules(kept, now), null); // Already after the change: nothing to save. assert.equal(unchangedDaySaves(kept, '2026-10-18T19:10:00.000Z', now), false); // A past day's start lands where the date already is: nothing to save. const oct2 = pickedDayRulesDate('2026-10-02', at); assert.equal(unchangedDaySaves(kept, evidenceRulesDate('2026-10-02'), oct2), false); // Windows not known (still loading, no firm): never. assert.equal(unchangedDaySaves(null, '2026-10-18T15:00:00.000Z', now), false); // The editor opened on Oct 18 (current rules) and a poll since lowered the // date to Oct 2: an untouched ✓ compares against the opened date, so it // doesn't pin Oct 18 by hand over the evidence. const sets = [{ id: 'g2', openedAt: null, closedAt: '2026-10-10T00:00:00.000Z' }]; const oct18 = pickedDayRulesDate('2026-10-18', { ...at, today: '2026-10-19' }); assert.equal(unchangedDaySaves(sets, '2026-10-18T15:00:00.000Z', oct18), false); }); test('lowersRulesDate: only earlier, only auto dates — the test lowerAutoRulesDate runs in SQL', () => { const evidence = '2026-10-01T23:59:59.999Z'; assert.equal(lowersRulesDate({ rules_date: '2026-10-18T14:00:00.000Z', rules_date_manual: 0 }, evidence), true); assert.equal(lowersRulesDate({ rules_date: null, rules_date_manual: 0 }, evidence), true); assert.equal(lowersRulesDate({ rules_date: evidence, rules_date_manual: 0 }, evidence), false); assert.equal(lowersRulesDate({ rules_date: '2026-09-01T00:00:00.000Z', rules_date_manual: 0 }, evidence), false); // Set by hand: never moved. assert.equal(lowersRulesDate({ rules_date: '2026-10-18T14:00:00.000Z', rules_date_manual: 1 }, evidence), false); assert.equal(lowersRulesDate({ rules_date: null, rules_date_manual: 1 }, evidence), false); // String order is time order for the two formats that are ever stored. assert.equal('2026-10-01T23:59:59.999Z' < '2026-10-02T00:00:00.000Z', true); assert.equal(evidenceRulesDate('2026-10-02') > new Date(Date.parse('2026-10-02T23:59:59.998Z')).toISOString(), true); }); // ── the whole selection, as getAccountConfig runs it ──────────────────────── test('selection: generations, boundaries and clamps end to end', () => { const sets = [ keptOf({ profitTarget: 3000, consistency: 0.5 }, G1, 'g1'), keptOf({ profitTarget: 2800, consistency: 0.45 }, G2, 'g2'), ]; const rulesFor = (rulesDate) => { const kept = pickKeptRules(sets, rulesDate); return kept ? overlayKept(LIVE, kept) : LIVE; }; const cases = [ [null, 2500, undefined], ['2026-09-01T00:00:00.000Z', 3000, 'g1'], [G1.closedAt, 2800, 'g2'], [G2.closedAt, 2500, undefined], [evidenceRulesDate('2026-10-02'), 2800, 'g2'], [evidenceRulesDate('2026-10-01'), 3000, 'g1'], ]; for (const [date, target, keptId] of cases) { const eff = rulesFor(date); assert.equal(eff.profitTarget, target, String(date)); assert.equal(eff.keptRules?.id, keptId, String(date)); // Always reported as the live type. assert.equal(eff.id, LIVE.id); } });