Skip to content

Commit 4d422bf

Browse files
fix(backend): counter parity in plan/sub runners, FK-safe batch writes, onchain version gate
H4: runPlanTest + runSubPlanTest now increment testedNodes only when result.actualMbps != null (else failedNodes++), and call recomputeCounters(state) before the terminal status='done' — mirroring runAudit's reconciliation so the two subscription runners no longer over-count tested vs failed. The new else branch lives inside if(result); the thrown-error path is a separate (result falsy) branch, so no double failedNodes++. M8: continuous.js gates insertBatchResult on batchId > 0 to avoid an FK violation when an upstream insertBatch failed and currentBatchId stayed 0. M7: onchain-report.js _decodeCsv rejects any SNTR1 record whose version != v2 (returns null) instead of misparsing a future v3+ memo as v2.
1 parent 1a55dc3 commit 4d422bf

3 files changed

Lines changed: 48 additions & 16 deletions

File tree

audit/continuous.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,18 @@ async function _runOnePass(loopState, batchId, frozenNodes = null) {
281281
_emitScoped('result', { result: raw, batchId });
282282
const payload = _sanitizeBatchNodeResult(raw, batchId);
283283
_emitScoped('batch:node:result', payload);
284-
// Persist to batch_results (non-blocking, non-fatal)
285-
_getDb().then(db => {
286-
if (db) {
287-
try { db.insertBatchResult(batchId, payload); }
288-
catch (err) { console.error(`[continuous] insertBatchResult failed (batch ${batchId}, ${payload?.address}): ${err.message}`); }
289-
}
290-
}).catch(err => console.error(`[continuous] _getDb failed (batch ${batchId}): ${err?.message || err}`));
284+
// Persist to batch_results (non-blocking, non-fatal). Guard on batchId > 0:
285+
// if insertBatch failed upstream (logged, non-fatal) currentBatchId stays 0,
286+
// and writing insertBatchResult(0, …) would violate the FK to a non-existent
287+
// batch row (foreign_keys=ON) and spam errors for every node in the pass.
288+
if (batchId > 0) {
289+
_getDb().then(db => {
290+
if (db) {
291+
try { db.insertBatchResult(batchId, payload); }
292+
catch (err) { console.error(`[continuous] insertBatchResult failed (batch ${batchId}, ${payload?.address}): ${err.message}`); }
293+
}
294+
}).catch(err => console.error(`[continuous] _getDb failed (batch ${batchId}): ${err?.message || err}`));
295+
}
291296
}
292297

293298
// Use injected mock runner if provided (test path), otherwise resolve real pipeline.

audit/pipeline.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,16 +1724,23 @@ export async function runPlanTest(planId, state, broadcast) {
17241724
const _ptSnippet = _ptRaw.length > 4096 ? _ptRaw.slice(-4096) : (_ptRaw || null);
17251725

17261726
if (result) {
1727-
state.testedNodes++;
17281727
result.inPlan = true;
17291728
result.planIds = [planId];
17301729
result.diag = result.diag || {};
17311730
result.diag.planId = planId;
17321731
result.diag.subscriptionId = subscriptionId;
17331732
result.diag.viaSubscription = true;
1734-
if (result.slaApplicable && result.pass15mbps) state.passed15++;
1735-
if (result.pass10mbps) state.passed10++;
1736-
if (result.passBaseline) state.passedBaseline++;
1733+
// Classify exactly as runAudit / recomputeCounters do: only an
1734+
// actualMbps != null result counts as tested (passed); a null-mbps
1735+
// result is a soft failure — otherwise testedNodes over-counts.
1736+
if (result.actualMbps != null) {
1737+
state.testedNodes++;
1738+
if (result.slaApplicable && result.pass15mbps) state.passed15++;
1739+
if (result.pass10mbps) state.passed10++;
1740+
if (result.passBaseline) state.passedBaseline++;
1741+
} else {
1742+
state.failedNodes++;
1743+
}
17371744
upsertResult(state, result);
17381745
saveResults(state);
17391746
broadcast('result', { result, state });
@@ -1767,6 +1774,9 @@ export async function runPlanTest(planId, state, broadcast) {
17671774

17681775
emergencyCleanupSync();
17691776
await _finalizeOnchainReporter();
1777+
// Reconcile live-incremented counters against results[] — mirrors runAudit
1778+
// so the final persisted summary matches the authoritative classifier.
1779+
recomputeCounters(state);
17701780
state.status = 'done';
17711781
state.completedAt = new Date().toISOString();
17721782
state.currentNode = null;
@@ -2198,7 +2208,6 @@ export async function runSubPlanTest(planId, subscriptionId, granterAddr, state,
21982208
const _spSnippet = _spRaw.length > 4096 ? _spRaw.slice(-4096) : (_spRaw || null);
21992209

22002210
if (result) {
2201-
state.testedNodes++;
22022211
result.inPlan = true;
22032212
result.planIds = [planId];
22042213
result.diag = result.diag || {};
@@ -2208,9 +2217,18 @@ export async function runSubPlanTest(planId, subscriptionId, granterAddr, state,
22082217
result.diag.feeGranted = !_selfGranter;
22092218
result.diag.selfGranter = _selfGranter;
22102219
result.diag.granter = granterAddr;
2211-
if (result.slaApplicable && result.pass15mbps) state.passed15++;
2212-
if (result.pass10mbps) state.passed10++;
2213-
if (result.passBaseline) state.passedBaseline++;
2220+
// Classify exactly as runAudit / recomputeCounters do: only an
2221+
// actualMbps != null result counts as tested (passed); a null-mbps
2222+
// result is a soft failure. Without this, testedNodes over-counts and
2223+
// the live header (passed = testedNodes) is wrong until a recompute.
2224+
if (result.actualMbps != null) {
2225+
state.testedNodes++;
2226+
if (result.slaApplicable && result.pass15mbps) state.passed15++;
2227+
if (result.pass10mbps) state.passed10++;
2228+
if (result.passBaseline) state.passedBaseline++;
2229+
} else {
2230+
state.failedNodes++;
2231+
}
22142232
upsertResult(state, result);
22152233
state.resumeHeadAddr = null;
22162234
saveResults(state);
@@ -2251,6 +2269,9 @@ export async function runSubPlanTest(planId, subscriptionId, granterAddr, state,
22512269

22522270
emergencyCleanupSync();
22532271
await _finalizeOnchainReporter();
2272+
// Reconcile live-incremented counters against results[] — mirrors runAudit
2273+
// so the final persisted summary matches the authoritative classifier.
2274+
recomputeCounters(state);
22542275
state.status = 'done';
22552276
state.completedAt = new Date().toISOString();
22562277
state.currentNode = null;

core/onchain-report.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ export function decodeMemo(memo, hrp = DEFAULT_HRP) {
170170
return _decodeCsv(memo);
171171
}
172172
if (memo.startsWith(`${MAGIC}${SEP}v`)) {
173-
// Future versions / unknown — try CSV-style anyway, return null on failure
173+
// Unknown / future SNTR1 version (e.g. v3+). The v2 CSV parser would
174+
// best-effort-decode it into confidently-wrong records, so refuse it and
175+
// return null (undecodable) instead. _decodeCsv enforces ver === 'v2'.
174176
return _decodeCsv(memo);
175177
}
176178

@@ -189,6 +191,10 @@ function _decodeCsv(memo) {
189191
const headerParts = lines[0].split(SEP);
190192
if (headerParts[0] !== MAGIC) return null;
191193
const ver = headerParts[1] || '';
194+
// Only the supported wire version is parseable. A future layout (v3+) must
195+
// NOT be coerced through the v2 parser into wrong records — treat as
196+
// undecodable and return null so callers can skip/ignore it.
197+
if (ver !== `v${VERSION}`) return null;
192198
const region = (headerParts[2] || '').replace(/-/g, '').trim() || null;
193199
let baselineMbps = 0;
194200
let startedAt = 0;

0 commit comments

Comments
 (0)