Skip to content

Commit aa2d951

Browse files
fix(server): state-reset bugs (clear/delete history, route isolation, spend)
From the bug hunt, adversarially reviewed (safe to commit, no CRITICAL/HIGH): - /api/clear: add isPipelineBusy() 409 guard (was wiping results mid-run, racing the pipeline) and reset the stale header fields (spentUdvpn/estimatedTotalCost/ totalNodes/currentNode/resumeHeadAddr/activeBatchId); keeps run identity. - clearActiveRunView: also clear baselineHistory/nodeSpeedHistory + currentNode/ resumeHeadAddr/activeBatchId — fixes the reported bug where deleting the active run left the "Last 10 baseline / node speeds" charts showing the deleted run. - loadRunIntoState: reset run-mode context (testRun=false; runMode derived from the run's SQLite row, never 'test', default p2p; planId/subId/granter=null; batch/resumeHead/currentNode/continuousLoop cleared) — closes the route- isolation leak where a loaded run inherited a prior TEST RUN's mode and a later Retest hijacked into TEST_RUN_SKIP. - idle balance refresher: don't zero spentUdvpn while a run is in-progress OR stopped-resumable (spendLocked = isPipelineBusy() || status==='stopped'), so a later Resume doesn't under-report cumulative on-chain spend. Empty catch → logged. Test suite green (188/31/45/35).
1 parent 13e53e6 commit aa2d951

1 file changed

Lines changed: 59 additions & 6 deletions

File tree

server.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,29 @@ function loadRunIntoState(num) {
815815
state.activeDbRunId = _runMeta?.dbRunId != null ? Number(_runMeta.dbRunId) : null;
816816
try { setActiveDbRunId(state.activeDbRunId); }
817817
catch (e) { console.error('[runs] setActiveDbRunId on load failed:', e.message); }
818+
// Route-isolation: a loaded run is a read-only historical snapshot. Reset the
819+
// run-mode context so a subsequent Retest (which clears loadedReadonly) can't
820+
// inherit a stale mode from the PRIOR live run — e.g. loading a P2P run after a
821+
// TEST RUN would otherwise leave state.testRun=true / runMode='test' and the
822+
// Retest would hijack into TEST_RUN_SKIP rows (the hijack CLAUDE.md warns of).
823+
// Derive the loaded run's mode from its SQLite row when known; default to
824+
// 'p2p' and NEVER leave 'test'.
825+
let _loadedMode = 'p2p';
826+
if (state.activeDbRunId != null) {
827+
try {
828+
const _runRow = getRun(state.activeDbRunId);
829+
if (_runRow && _runRow.mode && _runRow.mode !== 'test') _loadedMode = _runRow.mode;
830+
} catch (e) { console.error('[runs] mode lookup on load failed:', e.message); }
831+
}
832+
state.testRun = false;
833+
state.runMode = _loadedMode;
834+
state.runPlanId = null;
835+
state.runSubscriptionId = null;
836+
state.runGranter = null;
837+
state.activeBatchId = 0;
838+
state.resumeHeadAddr = null;
839+
state.currentNode = null;
840+
state.continuousLoop = false;
818841
return data;
819842
}
820843

@@ -900,6 +923,15 @@ function clearActiveRunView() {
900923
state.spentUdvpn = 0;
901924
state.estimatedTotalCost = '0 P2P';
902925
state.auditLogPath = null;
926+
// Charts + transient run pointers also belong to the now-deleted run: without
927+
// clearing these the dashboard's "Last 10 baseline" / "Last 10 node speeds"
928+
// and currentNode keep rendering the DELETED run's data, and a stale
929+
// activeBatchId/resumeHeadAddr could mis-target a later resume.
930+
state.baselineHistory = [];
931+
state.nodeSpeedHistory = [];
932+
state.currentNode = null;
933+
state.resumeHeadAddr = null;
934+
state.activeBatchId = 0;
903935
logBuffer.length = 0;
904936
try { flushStateSnapshot(); } catch (e) { console.error('[deleteRun] snapshot flush failed:', e.message); }
905937
}
@@ -2576,11 +2608,25 @@ app.post('/api/test-sub-plan', adminOnly, async (req, res) => {
25762608
});
25772609

25782610
app.post('/api/clear', adminOnly, (req, res) => {
2611+
// Guard: clearing mid-run corrupts state — the pipeline keeps pushing rows
2612+
// into the same results array we'd be emptying. Refuse while a run is live.
2613+
if (isPipelineBusy()) return res.status(409).json({ error: 'RUN_ACTIVE', message: 'Stop the run before clearing.' });
25792614
getResults().length = 0;
25802615
state.testedNodes = state.failedNodes = state.skippedNodes = state.passed15 = state.passed10 = state.passedBaseline = 0;
25812616
state.retryCount = 0;
25822617
state.baselineHistory = [];
25832618
state.nodeSpeedHistory = [];
2619+
// /api/clear wipes the DISPLAYED rows but keeps the active-run identity
2620+
// (activeRunNumber / activeDbRunId / pipeline run dir stay attached — use
2621+
// clearActiveRunView()/deleteRun to drop the identity). The counters above
2622+
// leave spend/total/currentNode stale, so the header would still show the old
2623+
// Net Spend / Total. Reset those transient fields too for a consistent wipe.
2624+
state.spentUdvpn = 0;
2625+
state.estimatedTotalCost = '0 P2P';
2626+
state.totalNodes = 0;
2627+
state.currentNode = null;
2628+
state.resumeHeadAddr = null;
2629+
state.activeBatchId = 0;
25842630
saveResults();
25852631
broadcastStateFresh();
25862632
res.json({ ok: true });
@@ -3141,10 +3187,15 @@ app.listen(PORT, LISTEN_HOST, async () => {
31413187
// ─── Periodic balance refresh (runs even when idle) ────────────────────────
31423188
if (MNEMONIC) {
31433189
setInterval(async () => {
3144-
// Skip refresh while the pipeline is doing its OWN balance refresh
3145-
// (actively testing, or parked in the insufficient-funds poll loop). This
3146-
// is deliberately NOT isPipelineBusy() — during paused_internet/'paused'
3147-
// the pipeline isn't touching balance, so the periodic refresh is fine.
3190+
// Whether this refresher may zero spentUdvpn. It must NOT while a run is
3191+
// in-progress OR resumable: the tester is an on-chain spend oracle, and a
3192+
// paused_internet/'paused' or 'stopped'-but-resumable run still has
3193+
// cumulative spend that a later Resume must report. Zeroing it here makes
3194+
// the resumed run under-report. Only when truly idle ('idle'/'done') is
3195+
// the live chain balance the sole truth and spentUdvpn safe to reset.
3196+
const spendLocked = isPipelineBusy() || state.status === 'stopped';
3197+
// Skip entirely while the pipeline does its OWN balance refresh (actively
3198+
// testing, or parked in the insufficient-funds poll loop).
31483199
if (state.status === 'running' || state.status === 'paused_balance') return;
31493200
try {
31503201
const w = await DirectSecp256k1HdWallet.fromMnemonic(MNEMONIC, { prefix: 'sent' });
@@ -3155,11 +3206,13 @@ app.listen(PORT, LISTEN_HOST, async () => {
31553206
tmpClient.disconnect();
31563207
if (fresh !== state.balanceUdvpn) {
31573208
state.balanceUdvpn = fresh;
3158-
state.spentUdvpn = 0;
3209+
// Only reset the spend estimate when truly idle — never on a paused or
3210+
// stopped-but-resumable run (see spendLocked above).
3211+
if (!spendLocked) state.spentUdvpn = 0;
31593212
state.balance = `${(fresh / 1_000_000).toFixed(4)} P2P`;
31603213
broadcast('state', { state });
31613214
}
3162-
} catch { /* non-critical */ }
3215+
} catch (e) { console.error('[balance-refresh] periodic refresh failed:', e.message); }
31633216
}, 2 * 60_000); // Every 2 minutes
31643217
}
31653218
});

0 commit comments

Comments
 (0)