feat(notify): typed title/body/type fields for non-keysign notifications - #28
Conversation
Adds `type`, `title`, and `body` to NotificationRequest. Keysign is the default (empty type = keysign), so all existing callers are unaffected. - NotificationType enum: keysign / reminder / task_success / task_failure / generic - ResolveTitle() picks a per-type default when explicit title is absent - ResolveBody() for keysign falls back to "Vault: <vault_name>" (legacy), other types return the explicit body or empty string - IsValid() relaxes required-field checks for non-keysign types (only vault_id needed); keysign still requires all four original fields - processAppleNotification / processAndroidNotification / processWebPushNotification all use ResolveTitle/ResolveBody instead of hardcoded "Vultisig Keysign request" - SendNotification handler delegates validation to IsValid() instead of its own inline all-fields check Paired with vultisig/agent-backend#838. Closes #26 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 10 minutes and 46 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
NeOMakinG
left a comment
There was a problem hiding this comment.
testing approach
- Cloned
vultisig/notificationat92756d6b, checked outfeat/typed-notification-fields go test ./...locally: 4 packages pass (models0.36s,service0.48s,stream7.8s,ws1.3s)- Full diff walk across all 5 changed files:
api/server.go,models/notification.go,models/notification_test.go,service/notification.go,service/notification_test.go - ab#838 companion PR: 404 (not yet opened); field names are canonical Go JSON snake_case so no drift risk
- GLM glm-4.6 third-eye lane: ran independently; findings deduped below
- Codex lane: skipped (TTY/auth unavailable)
blocking
None.
preferably-blocking
None.
should-fix
s1 — dedup key doesn't include type, cross-type notifications silently dropped
api/server.go:224 — the dedup key is vaultId + ":" + appID. A keysign push followed within 30 seconds by a scheduler-issued task_success, task_failure, or reminder for the same vault collapses into the same bucket and the second notification is silently swallowed (HTTP 200 returned to the caller, zero push delivered).
This is pre-existing code, but the PR's value proposition — letting the scheduler send non-keysign notifications — activates the latent bug. Before this PR the only callers were keysign senders; after ab#838 lands, two independent producers can co-occur within the 30-second window.
Suggested fix (api/server.go around line 224):
resolvedType := req.Type
if resolvedType == "" {
resolvedType = string(models.NotificationTypeKeysign)
}
dedupKey := req.VaultId + ":" + dedupAppID + ":" + string(resolvedType)s2 — deeplink always delivered for non-keysign APNs/FCM pushes
service/notification.go:215 — Custom("deeplink", request.QRCodeData) is set unconditionally. For reminder/task_success/task_failure types, QRCodeData is empty (not required by IsValid()). The iOS app receives a push with deeplink: "". If the notification tap handler calls URL(string: deeplink) without nil-checking the result, navigation fails silently or crashes.
Same issue on FCM: Data["message"] = request.QRCodeData sends an empty string.
Suggested fix:
if request.QRCodeData != "" {
p = p.Custom("deeplink", request.QRCodeData)
}And in processAndroidNotification:
data := map[string]string{}
if request.QRCodeData != "" {
data["message"] = request.QRCodeData
}suggestion
NotificationTypeGeneric hits default in ResolveTitle switch rather than a named case — works correctly (test covers it, returns "Vultisig"), but a future reader sees a 5-value enum with only 4 explicit cases and can't tell if generic is intentional or missing. Consider adding case NotificationTypeGeneric: to make it explicit.
q
q1 — ab#838 not yet opened. PR body says paired with agent-backend#838 but that PR returns 404 today. The notification server changes are backward-safe to merge independently, but confirming ab#838 will reference and rebase onto this would be useful.
q2 — dedup intent for scheduler notifications. With the scheduler path, if a task fails and immediately retries, two task_failure notifications fire within 30 seconds. Is dedup suppressing that second one intentional? If not, s1 above should be addressed (and the 30s TTL may also be too broad for scheduler notifications vs keysign ones).
risk
Low. The core enum dispatch, backward-compat default (empty type == keysign), and all three push delivery paths (APNs/FCM/WebPush) are correct. The IsValid() centralization in server.go removes the inline 4-field all-required check that would have silently rejected every non-keysign call — that latent bug is properly fixed here.
The dedup finding (s1) is the only operationally live issue once ab#838 ships; it can be fixed in-line in ab#838 or as a follow-up before the scheduler goes to production.
verdict
APPROVED. Core change is correct, backward-compat is verified, test coverage is solid (20 cases across 3 table-driven suites). s1 (dedup key) should be resolved before or alongside ab#838 to avoid silent notification drops.
triple-lane: Claude primary + GLM glm-4.6 third-eye (ran independently); Codex skipped (TTY-unavailable). Runtime: go test ./... green on PR HEAD 92756d6b.
QA EvidenceBranch: Test run: All 4 packages pass. 20 new model test cases (TestResolveTitle 7 + TestResolveBody 5 + TestIsValid 8) verified non-vacuous against the implementation. Key invariants verified by reading source:
ab#838 status: PR not yet opened (404); no cross-repo drift risk identified as fields follow canonical Go JSON snake_case convention. |
Closes #26
Paired with vultisig/agent-backend#838 (that PR uses the new fields once this lands).
what
Reminders and scheduled task notifications were rendering as "Vultisig Keysign request" because the notification server hardcoded that title everywhere. This PR adds a
typefield + explicittitle/bodyfields so callers can send the right text.how
NotificationTypeenum:keysign(default) |reminder|task_success|task_failure|genericResolveTitle()returns the explicittitleif set, otherwise a per-type defaultResolveBody()forkeysignkeeps the legacy"Vault: <vault_name>"fallback; other types return the explicitbody(or empty string if absent)IsValid()relaxes required-field validation for non-keysign types:vault_idis always required;vault_name/local_party_id/qr_code_dataare only required for keysign (they carry the deeplink)ResolveTitle()/ResolveBody()instead of the hardcoded strings/notifyhandler delegates toIsValid()instead of its own inline all-fields checkBackward-compat: empty
type=keysign, so every existing caller is unaffected.receipts
🤖 Agent-generated