// Checks for the Vest client's own wiring (lib/vest-class.ts), past what its // pure helpers show: the market-close guard closing a leg whose open came back // unknown, and the candidate board keeping a symbol the close gate holds back; // a sync older than the bot's own fill, an open a restart cut off, an outside // trade beside an open of unknown outcome, and the client's day at 8 PM ET; // the completed cycles a Vest day's claims end, named at each claim. // // npm run check // // A scratch database, a fake clock, and every gateway call answered here — // nothing reaches Vest. import { after, test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { registerHooks, stripTypeScriptTypes } from 'node:module'; // Resolve the extensionless local imports used by Next, without a build. function resolveTypes(specifier, context, nextResolve) { try { return nextResolve(specifier, context); } catch (err) { if (err.code !== 'ERR_MODULE_NOT_FOUND' || !specifier.startsWith('.')) throw err; return nextResolve(`${specifier}.ts`, context); } } // The client's classes use parameter properties, which plain type // stripping refuses: transform them instead. function loadTypes(url, context, nextLoad) { if (!url.startsWith('file:') || !url.endsWith('.ts')) return nextLoad(url, context); const source = stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8'), { mode: 'transform' }); return { format: 'module', source, shortCircuit: true }; } registerHooks({ resolve: resolveTypes, load: loadTypes }); const directory = mkdtempSync(join(tmpdir(), 'autofirmer-vest-client-')); process.env.AUTOFIRMER_DB = join(directory, 'test.sqlite'); delete process.env.NEXT_PHASE; after(() => rmSync(directory, { recursive: true, force: true })); // No request leaves this process: axios refuses every one, and each client's // gateway is a function in this file — from construction on, since the // constructor starts a sync. const { default: axios } = await import('../lib/http-client.ts'); axios.defaults.adapter = (config) => Promise.reject(new Error(`no network in this test: ${config.method} ${config.url}`)); const { VestClient } = await import('../lib/vest-class.ts'); let gateway = null; VestClient.prototype.request = async function (bucket, method, path, opts = {}) { if (!gateway) throw new Error(`no gateway in this test: ${method} ${path}`); return gateway(method, path, opts); }; const { putVestSession } = await import('../lib/vest-session.ts'); const { botOpenedPositions, loadAccountMeta, saveAccountMeta, saveFundTransactions, wipeAccountData, loadClaimedCycles, saveClaimedCycles, } = await import('../lib/db.ts'); const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts'); const { encodeContractName } = await import('../lib/dxtrade-helpers.ts'); const { tradeDateFor, stageInputsFor, resolveEffectiveConfig, claimedCycle, endedCycleCompletion } = await import('../lib/trading-logic.ts'); const { isClientReady } = await import('../lib/platform-client.ts'); const iso = (s) => Date.parse(s); const WALLET = '0x7ce10771819f80130e204ddb5ca1522e48a9b451'; // The clock the client reads (Date.now), set to an instant and running on. const realNow = Date.now; after(() => { Date.now = realNow; }); let tokens = 0; function clockAt(when) { const start = realNow(); const t0 = iso(when); Date.now = () => t0 + (realNow() - start); tokens += 1; assert.equal(putVestSession({ accessToken: `eyJ.test${tokens}.sig`, expiresAt: Date.now() + 3_600_000, primaryAddr: WALLET, userId: 'u' }), null); } // The week of NDX hours from check-vest.mjs: maintenance at 21:00 UTC // (17:00 ET) Thursday, the weekly close at 21:00 UTC Friday. const open = (from, to) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } }); const shut = (from, to, closure) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'unspecified', closure, tradingStatus: closure === 'weekly' ? 'CLOSE' : 'OPEN', price: { allow: closure !== 'weekly' } } }); const week = [ open('2026-10-28T22:00:00Z', '2026-10-29T21:00:00Z'), shut('2026-10-29T21:00:00Z', '2026-10-29T22:00:00Z', 'maintenance'), open('2026-10-29T22:00:00Z', '2026-10-30T21:00:00Z'), shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'), open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'), ]; const spec = (symbol, assetId) => ({ symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING', }); const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9)]]); /** A client with no sync running, its gateway answered by `answer`, holding * acc-1 and acc-2 under `ids` (the ids a sync gives them: A and B). */ function testClient(answer, ids = [1, 2]) { gateway = null; const client = new VestClient({ firmId: 'vest-test', firmName: 'Vest test', wallet: WALLET }, async () => {}); clearInterval(client.syncInterval); clearInterval(client.watchdogInterval); gateway = answer; client.specs = specs; client.symbolSpecs = async () => specs; client.marketHours = async (assetId) => (assetId === 7 ? week : []); client.accountList = ids.map((id, i) => ({ id, name: `V50K-F-D3-${i + 1}`, active: true })); ids.forEach((id, i) => { client.uuidById.set(id, `acc-${i + 1}`); client.capital.set(id, { status: VEST_STATUS_ACTIVE }); }); return client; } const A = vestAccountId('acc-1'); const B = vestAccountId('acc-2'); const NDX = encodeContractName('NDX-USD', 0.001); const rawPosition = (positionId, accountId, openDate, extra = {}) => ({ positionId, accountId, symbol: 'NDX-USD', side: 'LONG', quantity: '0.500', openPrice: '20000', collateral: '500', leverage: '20', openDate, stopLosses: [], takeProfits: [], ...extra, }); /** What a sync reads (acc-1 and acc-2 funded, $50k each, and the login's * main account; `positions` open), and the history reads after it. */ function syncReads(method, path, positions) { if (method !== 'GET') return null; if (path === '/v3/capital/accounts/active') { return { data: { accounts: ['acc-1', 'acc-2'].map((id, i) => ({ id, status: VEST_STATUS_ACTIVE, attempt_index: i + 1, initial_capital: '50000', max_daily_loss_pct: '0.03', stage: 1, stages: [{ ordinal: 1, kind: 'funded' }], })) } }; } if (path === '/v3/accounts') { return { data: { accounts: [ { account_id: 'acc-1', amount: '50000', account_type: 2 }, { account_id: 'acc-2', amount: '50000', account_type: 2 }, { account_id: 'main', amount: '0', account_type: 1 }, ] } }; } if (path === '/v3/positions/opened') return { data: { positions } }; if (path === '/v3/positions/opened-orders') return { data: { orders: [] } }; if (path === '/v3/ticker/latest') return { data: { tickers: [] } }; if (path === '/v3/positions') return { data: { positions: [] } }; return null; } /** testClient on the ids a sync gives, once the constructor's own sync * (which reaches no gateway) is over, with its history taken as read — so * each requestSync here is one good sync, and starts nothing else. */ async function syncingClient(answer) { const client = testClient(answer, [A, B]); await client.syncRunning; client.historyLoaded = true; client.historyLoadedIds = new Set([A, B]); client.lastDaysFetchStartedAt = Date.now(); client.lastDaysFetchSucceededAt = Infinity; return client; } const unanswered = (method, path) => { throw new Error(`no gateway in this test: ${method} ${path}`); }; test('the market-close guard closes a bot leg whose open came back unknown, not a hand trade', async () => { let positions = []; const closes = []; const client = testClient((method, path, opts) => { if (method === 'GET' && path === '/v3/positions/opened') return { data: { positions } }; if (method === 'POST' && path === '/v3/positions/open') throw new Error('socket hang up'); if (method === 'POST' && path === '/v3/positions/close') { closes.push({ account: opts.account, ...opts.body }); return { data: {} }; } throw new Error(`no gateway in this test: ${method} ${path}`); }); try { // Friday 8:30 AM ET: the entry's send fails in transit, but it filled. clockAt('2026-10-30T12:30:00Z'); await assert.rejects(client.sendOrder(1, encodeContractName('NDX-USD', 0.001), 500, 'Buy', 'Market'), /outcome unknown/); const raw = (positionId, accountId, openDate) => ({ positionId, accountId, symbol: 'NDX-USD', side: 'LONG', quantity: '0.500', openPrice: '20000', collateral: '500', leverage: '20', openDate, stopLosses: [], takeProfits: [], }); positions = [raw('9001', 'acc-1', iso('2026-10-30T12:30:01Z')), raw('9002', 'acc-2', iso('2026-10-30T11:00:00Z'))]; client.positionsDetail.set(1, []); client.positionsDetail.set(2, [{ positionId: '9002', accountId: 'acc-2', symbol: 'NDX-USD', side: 'long', quantity: 0.5 }]); client.syncComplete = true; // 4:56 PM ET: four minutes to the weekly close. clockAt('2026-10-30T20:56:00Z'); await client.guardClose(); assert.deepEqual(closes, [{ account: 'acc-1', positionId: '9001', symbol: 'NDX-USD', orderType: 'market' }]); assert.deepEqual([...botOpenedPositions(['9001', '9002'])], ['9001']); // A close just sent isn't sent again while a read may not show it. await client.guardClose(); assert.equal(closes.length, 1); } finally { client.disconnect(); } }); test('a symbol the close gate holds back stays on the board, and isn\'t picked; the maintenance hour is ridden', async () => { const client = testClient((method, path) => { throw new Error(`no gateway in this test: ${method} ${path}`); }); client.refreshMarks = async () => {}; client.volatility = async (symbols) => new Map(symbols.map((s) => [s, { vol: 0.004, candles: 168 }])); const marked = () => { for (const s of ['NDX-USD', 'BTC-PERP']) client.marks.set(s, { mark: s === 'NDX-USD' ? 20000 : 70000, at: Date.now() }); }; try { // Thursday 2 PM ET: NDX's maintenance hour is 3 h off, and reopens // before the reset — no gate. clockAt('2026-10-29T18:00:00Z'); marked(); await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false); const thursday = client.lastCandidates.find((c) => c.id === 'NDX-USD'); assert.equal(thursday.closesAt, undefined); // Friday 2 PM ET: the weekly close is 3 h off; crypto never closes. clockAt('2026-10-30T18:00:00Z'); marked(); const pick = await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false); assert.equal(pick.id, 'BTC-PERP'); assert.equal(pick.closesAt, undefined); assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']); const ndx = client.lastCandidates.find((c) => c.id === 'NDX-USD'); assert.equal(ndx.closesAt, iso('2026-10-30T21:00:00Z')); // The survey keeps it up for the dashboard, though nothing is picked. assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null); assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']); // And the client's day is Vest's. assert.equal(client.tradeDate(iso('2026-10-27T22:30:00Z')), vestDayOf(iso('2026-10-27T22:30:00Z'))); } finally { client.disconnect(); } }); test("a sync whose positions read predates the bot's own fill leaves the fill's record alone", async () => { let positions = []; let holdRead = false; let releaseRead = null; clockAt('2026-10-28T14:00:00Z'); const client = await syncingClient((method, path) => { if (method === 'GET' && path === '/v3/positions/opened' && holdRead) { // Read now, flat — answered once released. holdRead = false; const served = positions; return new Promise((resolve) => { releaseRead = () => resolve({ data: { positions: served } }); }); } const read = syncReads(method, path, positions); if (read) return read; if (method === 'POST' && path === '/v3/positions/open') { positions = [rawPosition('9101', 'acc-1', Date.now() + 300)]; return { data: { orderId: 'o-9101', positionId: '9101' } }; } return unanswered(method, path); }); try { await client.requestSync(); // A sync reads acc-1 flat; the entry fills while it is still out. holdRead = true; const sync = client.requestSync(); await client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'); // Filled: the note kept against a restart goes. assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null'); releaseRead(); await sync; assert.deepEqual(client.positionsDetail.get(A).map((p) => p.positionId), ['9101']); assert.equal(client.positions[A].netPos, 0.5); // A read lagging the fill: the leg's flatten is refused as a position // still out there (FLATTEN FAILED), not taken as nothing to flatten. positions = []; await assert.rejects(client.sendOrder(A, NDX, 500, 'Sell', 'Market', undefined, 'close'), /last saw a long one open/); // A sync that went out after the fill writes what it reads. await client.requestSync(); assert.deepEqual(client.positionsDetail.get(A), []); assert.equal(client.positions[A], undefined); } finally { client.disconnect(); } }); test("an open a restart cut off is marked the bot's once a read after the restart shows it", async () => { let positions = []; let posted; const postedAt = new Promise((resolve) => { posted = resolve; }); clockAt('2026-10-28T15:00:00Z'); const before = await syncingClient((method, path) => { const read = syncReads(method, path, positions); if (read) return read; // The app stops with the open out. if (method === 'POST' && path === '/v3/positions/open') { posted(Date.now()); return new Promise(() => {}); } return unanswered(method, path); }); void before.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open').catch(() => {}); const sentAt = await postedAt; before.disconnect(); // Vest filled it, and an outside long opened on acc-2 with it. acc-2 has // an older note, of a short that never showed. positions = [rawPosition('9201', 'acc-1', sentAt + 400), rawPosition('9202', 'acc-2', sentAt + 400)]; saveAccountMeta(B, 'vest_pending_open', JSON.stringify({ accountId: 'acc-2', symbol: 'NDX-USD', side: 'short', sentAt: sentAt - 10 * 60_000 })); clockAt('2026-10-28T15:02:00Z'); let refuse = false; const after = await syncingClient((method, path) => { const read = syncReads(method, path, positions); if (read) return read; if (method === 'POST' && path === '/v3/positions/open' && refuse) throw new VestApiError('/v3/positions/open: refused (HTTP 400)', 400, 'REFUSED'); return unanswered(method, path); }); try { await after.requestSync(); assert.deepEqual([...botOpenedPositions(['9201', '9202'])], ['9201']); assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null'); assert.equal(loadAccountMeta(B, 'vest_pending_open'), 'null'); // An open Vest refuses leaves no note behind. positions = []; refuse = true; await assert.rejects(after.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /refused/); assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null'); } finally { after.disconnect(); } }); test("an open of unknown outcome isn't matched to an outside trade after its error, nor to one of several", async () => { let positions = []; let open = null; let onRead = () => {}; clockAt('2026-10-28T16:00:00Z'); const client = await syncingClient((method, path) => { if (method === 'GET' && path === '/v3/positions/opened') onRead(); const read = syncReads(method, path, positions); if (read) return read; if (method === 'POST' && path === '/v3/positions/open') return open(); return unanswered(method, path); }); try { await client.requestSync(); // The entry's send fails in transit, and it never went in. Half a // minute on, an outside copy-trader opens NDX long on every account. open = () => { throw new Error('socket hang up'); }; await assert.rejects(client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /outcome unknown/); const errorAt = Date.now(); positions = [rawPosition('9301', 'acc-1', errorAt + 30_000), rawPosition('9302', 'acc-2', errorAt + 30_000)]; clockAt('2026-10-28T16:01:00Z'); await client.requestSync(); assert.deepEqual([...botOpenedPositions(['9301', '9302'])], []); assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null'); // Another on acc-2, and two positions show from that moment: either // could be the bot's, so neither is marked. positions = []; clockAt('2026-10-28T16:05:00Z'); open = () => { positions = [rawPosition('9303', 'acc-2', Date.now() + 100), rawPosition('9304', 'acc-2', Date.now() + 150)]; throw new Error('socket hang up'); }; await assert.rejects(client.sendOrder(B, NDX, 500, 'Buy', 'Market', undefined, 'open'), /outcome unknown/); await client.requestSync(); assert.deepEqual([...botOpenedPositions(['9303', '9304'])], []); // An open whose answer named its position, though no read showed it // within the wait: that position is the bot's, even with a stop a // hand has put on it since. positions = []; clockAt('2026-10-28T16:10:00Z'); let answered = false; open = () => { answered = true; return { data: { orderId: 'o-9305', positionId: '9305' } }; }; // The wait's first read comes back past its deadline. onRead = () => { if (answered) { answered = false; clockAt('2026-10-28T16:10:45Z'); } }; await assert.rejects(client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /not seen filled/); const stop = { id: 'sl-9305', triggerPrice: '19900', executionType: 'market', status: 'awaiting' }; positions = [rawPosition('9305', 'acc-1', iso('2026-10-28T16:10:00Z') + 500, { stopLosses: [stop] })]; await client.requestSync(); assert.deepEqual([...botOpenedPositions(['9305'])], ['9305']); } finally { client.disconnect(); } }); test("the client's Vest day turns with its numbers, and a sync that stalls stops the trading it holds back", async () => { clockAt('2026-10-27T23:58:00Z'); const client = await syncingClient((method, path) => syncReads(method, path, []) ?? unanswered(method, path)); // The day the engine plans the lane on, at this test's clock. const engineDay = () => tradeDateFor(client, new Date(Date.now())); try { // 7:58 PM ET, before any sync: the day now. assert.equal(engineDay(), '2026-10-27'); await client.requestSync(); // 8:00:20 PM ET: Vest's day has turned, but the numbers the last sync // worked out are still the ended day's — and so is the engine's day. clockAt('2026-10-28T00:00:20Z'); assert.equal(vestDayOf(Date.now()), '2026-10-28'); assert.equal(engineDay(), '2026-10-27'); assert.equal(isClientReady(client), true); await client.requestSync(); assert.equal(engineDay(), '2026-10-28'); // No sync after the next reset: the day holds back only while the // scheduler still trades on the last sync's numbers. clockAt('2026-10-28T23:59:00Z'); await client.requestSync(); clockAt('2026-10-29T00:03:00Z'); assert.equal(engineDay(), '2026-10-28'); assert.equal(isClientReady(client), true); clockAt('2026-10-29T00:05:00Z'); assert.equal(engineDay(), '2026-10-28'); assert.equal(isClientReady(client), false); } finally { client.disconnect(); } }); test('two payouts on one Vest day share its fund row, and still count two toward the stage', async () => { clockAt('2026-10-27T23:00:00Z'); const client = await syncingClient(unanswered); try { // $1,500 then $3,000 taken on Vest day 10-27: Vest's two WITHDRAWs, one row. client.withdrawals.set(A, [{ at: iso('2026-10-27T15:00:00Z'), amount: -1500 }, { at: iso('2026-10-27T19:00:00Z'), amount: -3000 }]); client.fundTransactions[A] = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -4500 }]; assert.deepEqual(client.payoutDays(A), ['2026-10-27', '2026-10-27']); // A type whose stages don't reset each day trades stage 3's $2,000, not stage 2's again. const stages = [{ profit: 3000, consistency: 0, minTradingDays: 0 }, { profit: 2000, consistency: 0, minTradingDays: 0 }]; const inputs = stageInputsFor(client, A, { stagesSameDay: false }); assert.equal(inputs.payouts, 2); assert.equal(resolveEffectiveConfig(1500, 0, 0, false, stages, 0, client.fundTransactions[A], 0, inputs).profitTarget, 2000); // Before its history is read: one per negative fund row. client.historyLoadedIds.delete(A); assert.equal(stageInputsFor(client, A, {}).payouts, undefined); } finally { client.disconnect(); } }); test('each claim of a Vest day names the completed cycle it ends, the second and the one after the reset too', async () => { // acc-1, funded $50k, Vest's own numbers: its balance, and its reset balance since 20:00 ET. let balance = 51500; let reset = { at: '2026-10-27T00:00:00Z', balance: 50000 }; clockAt('2026-10-27T19:00:00Z'); // 3 PM ET, Vest day 10-27 wipeAccountData(A); const client = await syncingClient((method, path, opts) => { const read = syncReads(method, path, []); if (read && path === '/v3/capital/accounts/active') { read.data.accounts[0] = { ...read.data.accounts[0], daily_reset_at: reset.at, daily_reset_balance: String(reset.balance) }; } if (read && path === '/v3/accounts') read.data.accounts[0] = { ...read.data.accounts[0], amount: String(balance) }; if (read) return read; if (method === 'POST' && path === '/v3/capital/withdraw' && opts.body.account_id === 'acc-1') { balance = Math.round((balance - Number(opts.body.amount)) * 100) / 100; return { data: { trader_amount: opts.body.amount } }; } return unanswered(method, path); }); // The history read a claim starts isn't this test's: what it books is set here. client.fetchDaysTraded = async () => ({ failedAccounts: 0 }); const cfg = { profitTarget: 1500, consistency: 0, minDayPnL: 0, minTradingDays: 0, accountSize: 50000, targetSameEquity: false, splitEnabled: true, payoutable: true, tradesEveryDay: true, combineCmeHolidays: false, stagesSameDay: true, dailyLossLimit: 1500, withdrawalStages: [{ profit: 3000, consistency: 0, minTradingDays: 0, dailyLossLimit: 3000 }], }; /** The day's trading so far, as the history read books it, and Vest's WITHDRAWs for the claims made. */ const history = (dayPnl) => { client.fullDailyPnL[A] = [{ date: '2026-10-27', pnl: dayPnl }]; client.applyCycleSlice(A); client.withdrawals.set(A, client.claimsOf(A).map((c) => ({ at: c.at, amount: -c.amount }))); }; const named = () => claimedCycle(client, A, cfg, new Date(Date.now())); try { saveFundTransactions(A, 'V50K-F-D3-1', [{ date: '2026-10-20', amount: 50000 }]); saveAccountMeta(A, 'last_fund_date', '2026-10-20'); client.fundTransactions[A] = [{ date: '2026-10-20', amount: 50000 }]; client.lastFundDates[A] = '2026-10-20'; history(1500); await client.requestSync(); // Stage 1's $1,500, claimed: the account's first cycle. assert.deepEqual(named(), { date: '2026-10-27', stage: 1, drain: false, cycle: '2026-10-20', cycleSeq: 0 }); await client.claimProfit(A, 1500); await client.syncRunning; // the sync the claim starts // 6 PM ET: stage 2's $3,000 made, and claimed the same Vest day — its // payout books into 10-27's row, and the latest fund date stays. clockAt('2026-10-27T22:00:00Z'); balance = 53000; history(4500); await client.requestSync(); assert.deepEqual(named(), { date: '2026-10-27', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 }); await client.claimProfit(A, 3000); await client.syncRunning; assert.equal(client.lastFundDates[A], '2026-10-27'); assert.deepEqual(client.fundTransactions[A].at(-1), { date: '2026-10-27', amount: -4500 }); // 6:30 PM ET: the day's third cycle, $1,500 of stage 2's $3,000 — not complete. clockAt('2026-10-27T22:30:00Z'); balance = 51500; history(6000); await client.requestSync(); assert.equal(named(), null); // 8:00:30 PM ET: the reset puts it back on stage 1's $1,500, met — // claimed at the first tick. clockAt('2026-10-28T00:00:30Z'); reset = { at: '2026-10-28T00:00:00Z', balance: 51500 }; await client.requestSync(); assert.equal(tradeDateFor(client, new Date(Date.now())), '2026-10-28'); assert.deepEqual(named(), { date: '2026-10-28', stage: 3, drain: false, cycle: '2026-10-27', cycleSeq: 1 }); await client.claimProfit(A, 1500); await client.syncRunning; assert.equal(client.lastFundDates[A], '2026-10-28'); // The catch-up names none of it: the cycle's P&L sits in 10-27's row, // which the cycle before owns. clockAt('2026-10-28T00:10:00Z'); await client.requestSync(); assert.equal(endedCycleCompletion({ accountId: A, rawTradeDate: tradeDateFor(client, new Date(Date.now())), client, fullDailyPnL: client.fullDailyPnL[A], cycleDailyPnL: client.dailyPnL[A], fundTransactions: client.fundTransactions[A], lastFundDate: client.lastFundDates[A], endedCycleStart: JSON.parse(loadAccountMeta(A, 'ended_cycle_start')), rawRealizedPnL: client.accountCashBalances[A].realizedPnL, equityProfit: client.accountCashBalances[A].amount - 50000, cfg, now: new Date(Date.now()), }), null); // Kept as the claims leave them; an entry that doesn't read is left out. const kept = [{ date: '2026-10-27', stage: 2, drain: false, cycle: '2026-10-27', cycleSeq: 0 }]; saveClaimedCycles(A, [...kept, { date: '2026-10-28', stage: '3', drain: false, cycle: '2026-10-27', cycleSeq: 1 }]); assert.deepEqual(loadClaimedCycles(A), kept); saveAccountMeta(A, 'claimed_cycles', '{'); assert.deepEqual(loadClaimedCycles(A), []); } finally { client.disconnect(); } });