fix(meshcore): stop upsertNode from clobbering stored node data with nulls (#3504)#3510
Conversation
…nulls (#3504) MeshCoreRepository.upsertNode's update branch did `.set({ ...node, ... })`, ignoring the `existing` row it fetches. Callers pass `field: value ?? null` for fields they didn't observe this time — persistContact (meshcoreManager.ts:1154) and the telemetry-config route (meshcoreRoutes.ts:2346) both do — so an incoming null silently overwrote a previously-stored name / position / advType. (The telemetry poller is safe: it passes a partial object and drizzle only writes present keys.) Merge instead: drop null/undefined incoming fields so only columns the caller actually provided a value for are updated; the stored value is preserved otherwise. `false`/`0`/`''` are still written (only null/undefined means "not observed"). Exception: outPath/pathLen remain clearable via null (CMD_RESET_PATH) — preserving the existing, tested behavior. Verified reachability first (the route + persistContact pass explicit nulls), unlike #3503 which was dead code. Adds a regression test asserting a null-laden re-upsert preserves stored name/position; the existing CMD_RESET_PATH clearable test still passes. Closes #3504. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshcore): stop upsertNode from clobbering stored node data with nulls (#3504)
SummaryThis is a solid, well-reasoned fix. The bug is genuine and reachable (confirmed at Code Review:
|
…atteryMv:0 (review) claude-pr review nits on #3510 (both non-blocking, applied): - Hoist CLEARABLE_VIA_NULL to a module-level const so it reads as a fixed policy. - Add a test asserting batteryMv: 0 is written (not skipped), guarding the merge guard's '!== null' against a future change to a truthiness check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshcore): stop upsertNode from clobbering stored node data with nulls (#3504)
SummaryThis is a well-executed fix for a genuine, reachable bug. The previous Claude review suggested two minor improvements; both have been addressed in the latest commit ( Previous Suggestions — Addressed1.
2. The new test at Code Review:
|
| Test | What it covers | Status |
|---|---|---|
upsertNode does NOT clobber stored name/position with incoming nulls (#3504) (line 134) |
Primary regression test — null fields don't wipe stored values; a provided non-null (advType) still updates |
✅ New |
upsertNode writes a falsy-but-valid value like batteryMv: 0 (not skipped as "absent") (line 156) |
Guards !== null filter against future truthiness-check regression; preserves unrelated fields |
✅ New |
upsertNode persists out_path and path_len round-trip (line 84) |
CLEARABLE_VIA_NULL path — null still clears outPath/pathLen (CMD_RESET_PATH) |
✅ Existing, still passes |
All 24+ meshcore tests pass per the PR description. The test suite now covers: null-clobber prevention, falsy-valid write, clearable-via-null opt-in, source scoping, cross-source isolation, and the stub-insert path.
No remaining concerns
The implementation is correct, the edge cases are covered, and both suggestions from the previous review have been applied cleanly. The CLEARABLE_VIA_NULL module-scope placement is idiomatic and the new batteryMv: 0 test adds real regression value.
Verdict: Approve. Ready to merge.
Closes #3504.
Bug (reachability verified — this one is real)
MeshCoreRepository.upsertNode's update branch did.set({ ...node, sourceId, updatedAt }), ignoring theexistingrow it fetches. Two real callers passfield: value ?? nullfor fields they didn't observe this cycle:meshcoreManager.persistContact(meshcoreManager.ts:1154)meshcoreRoutes.ts:2346, the [BUG] MeshCore telemetry retrieval fails on every repeater (meshcore-only setup, v4.6.1) #3092 advType-stamping path)So an incoming
null(no position / no name this time) silently overwrote a previously-stored name / position / advType.(The telemetry poller at
meshcoreTelemetryPoller.ts:249is safe — it passes a partial{ publicKey, batteryMv }object, and drizzle.set()only writes keys present, so it never nulls other columns.)Fix
Merge instead of clobber: drop
null/undefinedincoming fields so only columns the caller actually provided a value for are updated; the stored value is preserved otherwise.false/0/''are still written (onlynull/undefinedmeans "not observed").Per-column exception:
outPath/pathLenremain clearable via null — there a null means the route was reset (CMD_RESET_PATH), not "unobserved". This preserves the existing, tested behavior (meshcore.test.ts:99-108). This is the audit's "per-column opt-in" recommendation in practice.Tests
name: null, latitude: null, longitude: null) must preserve the stored name/position, while a providedadvTypestill updates.Validation
tsc --noEmit: cleanNote: I led with the reachability check this time (#3503 turned out to be dead code). Here the buggy path is genuinely reachable — confirmed the callers pass explicit nulls.
🤖 Generated with Claude Code