fix(nodes): don't clobber learned name/macaddr/hwModel with blanks (#3505)#3512
Conversation
…3505) The node upsert merge guard treated '' and 0 as "provided", so a future caller passing a blank name or hwModel 0 would overwrite a stored value — the latent hole behind #3456 (which was fixed only at the mqttIngestion call site, not the guard). This is the durable form of that fix. Per-column policy (not a blanket skip-empty, since 0 is legitimate for most numerics): - SQLite upsertNodeSqlite: longName/shortName/macaddr now use a setIfNonBlank guard ('' preserves existing); hwModel skips 0 (HardwareModel.UNSET). - Async PG/MySQL update: same via a nameOrExisting() helper + hwModel-0 guard. - snr/rssi/battery/voltage/lat-lon/role etc. still write 0 (genuine zeros). Adds nodes.upsertGuard.test.ts: a blank re-upsert preserves name/macaddr/hwModel, while snr:0/batteryLevel:0 still write. (New file rather than editing nodes.test.ts to avoid colliding with the in-flight #3501 test-DB migration.) Closes #3505. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review
Overall AssessmentThis is a well-reasoned hardening fix. The per-column policy (blank strings and Code Quality
The const nameOrExisting = (incoming: any, existing: any) =>
(incoming === null || incoming === undefined || incoming === '') ? existing : incoming;Minor concerns:
The SQLite path is clean and consistent. Test CoverageThe tests in
Gaps to consider:
Potential Bug
const upsertSet = {
longName: nodeData.longName ?? null, // '' would persist here
shortName: nodeData.shortName ?? null, // same
macaddr: nodeData.macaddr ?? null,
hwModel: nodeData.hwModel ?? null, // 0 would persist here
...
};This is the same pattern that caused the original bug. Since this is the race-condition fallback ( Minor Observations
SummaryThe fix is correct for the primary update path ( |
…3512) The INSERT … ON CONFLICT DO UPDATE race-fallback (upsertSet) still used the bare ?? null pattern, letting ''/0 persist and reintroducing the #3456/#3505 clobber under a concurrent-insert race. Normalize blank strings / hwModel 0 to NULL there (no existing row is fetched on the insert branch, so the nameOrExisting helper can't apply — blank→null is the consistent fix). Also add async-path (PG/MySQL-compatible) guard tests to the shared nodes.test.ts suite, per review feedback, mirroring the SQLite guard tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @Yeraze's task —— View job PR Review
Overall AssessmentThis is a solid iteration over the previous review. The two main gaps called out before — the What was fixed since the last review
The previous review flagged that const blankToNull = (v: any) => (v === '' || v === null || v === undefined) ? null : v;
const upsertSet = {
longName: blankToNull(nodeData.longName), // '' → null ✓
shortName: blankToNull(nodeData.shortName), // '' → null ✓
hwModel: (nodeData.hwModel === 0 || ...) ? null : nodeData.hwModel, // 0 → null ✓
...
};The comment on this block (lines 464–469) correctly explains the intent: this is the "first-seen" insert path where there's no prior value to preserve, so Async Tests at lines 284–323 verify the Remaining minor observations
macaddr: nodeData.macaddr ?? null, // '' would persist here
const nameOrExisting = (incoming: any, existing: any) =>Both parameters are
No blocking issuesThe original latent bug is closed from all three angles:
Tests cover the async and SQLite paths. The |
Consistency: macaddr in the ON CONFLICT DO UPDATE block still used ?? null while longName/shortName already use blankToNull. Align it so a blank '' macaddr normalizes to null on the conflict-update path like the other identity fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review
Overall AssessmentThis PR is in excellent shape. All gaps from the two previous automated review rounds have been addressed. The fix is now consistent across all three code paths, and the test suite covers the key scenarios well. What's coveredThree-path consistency:
All three paths are consistent. The previous gap ( Test coverage:
Minor observations (none blocking)
No blocking issuesThe latent clobber bug is fully closed across all three paths. The |
Closes #3505. The durable form of the #3456 fix.
Problem (latent hole)
The node upsert merge guard treated
''and0as "provided":upsertNodeSqlite:setIfProvidedonly checks!== null && !== undefined, so''/0overwrite.nodeData.x ?? existingNode.x—??treats''/0as present.#3456 (MQTT-sourced nodes appearing nameless) was fixed only at the call site (mqttIngestion stopped passing blanks), not the guard. So any future caller passing a blank name or
hwModel: 0re-introduces it. (No live caller triggers it today — this is hardening.)Fix — per-column policy (not a blanket skip-empty)
0is legitimate for most numerics (snr/battery/lat-lon, and role 0 = CLIENT), so only the genuinely-blank-meaningless fields are guarded:longName/shortName/macaddr:''now preserves the stored value (newsetIfNonBlankguard on SQLite; anameOrExisting()helper on the async path).hwModel:0=HardwareModel.UNSET(unknown) → preserve a known model.snr/rssi/battery/voltage/lat-lon/rolestill write0.Test
New
nodes.upsertGuard.test.ts:longName: '',hwModel: 0, …) preserves the learned name/macaddr/hwModel.snr: 0/batteryLevel: 0re-upsert still writes 0 (guards against an over-aggressive truthiness check).(New file rather than editing
nodes.test.ts, to avoid colliding with the in-flight #3501 test-DB-helper migration.)Validation
tsc --noEmit: clean🤖 Generated with Claude Code