fix(desktop): let the desktop repair a keyless group

`repairKeylessGroup` was wired in src/ui/main.jsx and dispatched in bare.js, and
simply absent from src/ui-desktop/main.jsx (TODO #146).

Nothing failed loudly, because a missing proxy entry just reads as `undefined`.
handleInviteLink is SHARED between the two UIs and gates the keyless-group repair
on that entry being truthy, so on desktop the branch fell through to
`already_member` - the precise dead end TODO #124 exists to remove, whose own
comment says "returning already_member here is exactly what made that cure a dead
end". A keyless group on Pear Desktop could not be healed by any user action.

The one-line fix is the small part. #146 also asked for a wholesale diff of the
two proxies, and doing it turned up 30 differing methods - almost all of them
legitimate, because the proxies are DELIBERATELY different: mobile has haptics,
QR scanning and Lightning, desktop has launch-at-login. So a test demanding they
match would be wrong, and would be deleted the first time it got in the way.

What must hold is narrower and actually true: every db method that SHARED code
calls has to exist in BOTH proxies. test/dbProxyParity.test.js enforces exactly
that, and a fourth test guards the guard by asserting the two are still allowed
to differ where nothing shared depends on them.

Audited with that rule: the desktop's own code calls 22 db methods and every one
is present. The only real gap in the whole surface was this one, reached through
shared src/invite.js.

334 unit tests, 4 new. Falsified by removing the fix, where 3 of them fail.

Verified on the running desktop app over CDP: the renderer round-trips
`repairKeylessGroup` to the worklet and gets `{repaired:false,
reason:'not-a-member'}` for a group that does not exist - the correct answer, and
the `reason` that TODO #145's work now surfaces to the user. Note what that does
and does not prove: it establishes the worklet answers this method on the desktop
path, which was never exercised before; the proxy entry itself is proven by the
static test plus its falsification, since `db` is module-local and not reachable
from the console.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG8ayyquJuDMPSVLKDQKVh
This commit is contained in:
Your Name
2026-07-27 09:48:49 -05:00
co-authored by Claude Opus 5
parent 33de2223f0
commit cb6f04ef3c
2 changed files with 101 additions and 0 deletions
+9
View File
@@ -75,6 +75,15 @@ const db = {
deleteGroup: (id) => window.__pearDB.call('deleteGroup', id),
isBlockedFromGroup: (id) => window.__pearDB.call('isBlockedFromGroup', id),
clearBlockedFromGroup: (id) => window.__pearDB.call('clearBlockedFromGroup', id),
// TODO #146 - this was missing, and its absence was invisible. handleInviteLink
// is SHARED with mobile (src/invite.js) and gates the keyless-group repair on
// `db.repairKeylessGroup` being truthy, so on desktop the branch silently fell
// through to `already_member` - which is exactly the dead end TODO #124 exists
// to remove ("returning already_member here is exactly what made that cure a
// dead end"), reintroduced on desktop by omission. A keyless group on Pear
// Desktop could therefore not be healed by any user action at all. Nothing
// failed loudly, because a missing proxy entry just reads as undefined.
repairKeylessGroup: (id, k) => window.__pearDB.call('repairKeylessGroup', id, k),
reinviteMember: (gid, mid) => window.__pearDB.call('reinviteMember', gid, mid),
listMembers: (gid) => window.__pearDB.call('listMembers', gid),
putMember: (gid, m) => window.__pearDB.call('putMember', gid, m),
+92
View File
@@ -0,0 +1,92 @@
// TODO #146 - the two db proxies are allowed to differ, but not where shared
// code depends on them.
//
// `repairKeylessGroup` was wired in src/ui/main.jsx and dispatched in bare.js,
// and simply absent from src/ui-desktop/main.jsx. Nothing failed loudly: a
// missing proxy entry just reads as `undefined`, and handleInviteLink - which is
// SHARED between the two UIs - gates the keyless-group repair on that entry
// being truthy. So on desktop the branch fell through to `already_member`, the
// precise dead end TODO #124 exists to remove, reintroduced by omission. A
// keyless group on Pear Desktop could not be healed by any user action.
//
// It stayed invisible until a trace happened to land on it, which is the part
// worth fixing permanently. The two proxies are DELIBERATELY different - mobile
// has haptics, QR scanning and Lightning; desktop has launch-at-login - so
// demanding they match would be wrong and would be deleted the first time it
// got in the way. What must hold is narrower and actually true:
//
// every db method that SHARED code calls must exist in BOTH proxies.
//
// (bugfix/desktop-db-proxy-parity)
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.join(__dirname, '..')
const read = (p) => fs.readFileSync(path.join(root, p), 'utf8')
// Modules imported by BOTH src/ui and src/ui-desktop, so anything they call on
// `db` has to be answerable in either host.
const SHARED_MODULES = ['src/invite.js']
const PROXIES = {
mobile: 'src/ui/main.jsx',
desktop: 'src/ui-desktop/main.jsx',
}
// Method names defined in a proxy object literal: two-space-indented `name:`.
function proxyMethods (file) {
return new Set([...read(file).matchAll(/^ {2}([A-Za-z_$][\w$]*)\s*:/gm)].map(m => m[1]))
}
// `db.foo(` and `db?.foo(` - the CALL form only. A bare `db.foo` reference (as
// in the truthiness gate this bug hid behind) is deliberately not counted here:
// what matters is the call, and matching bare references would also match
// prose like "db.js" in a comment.
function dbCallsIn (file) {
return new Set([...read(file).matchAll(/\bdb\??\.\s*([A-Za-z_$][\w$]*)\s*\(/g)].map(m => m[1]))
}
test('shared modules only call db methods that BOTH proxies provide', () => {
const mobile = proxyMethods(PROXIES.mobile)
const desktop = proxyMethods(PROXIES.desktop)
const gaps = []
for (const mod of SHARED_MODULES) {
for (const method of dbCallsIn(mod)) {
if (!mobile.has(method)) gaps.push(`${mod} calls db.${method}() - missing from ${PROXIES.mobile}`)
if (!desktop.has(method)) gaps.push(`${mod} calls db.${method}() - missing from ${PROXIES.desktop}`)
}
}
assert.deepEqual(gaps, [],
'a shared module calls a db method one host cannot answer; it will read as undefined and fail silently')
})
test('the desktop can repair a keyless group', () => {
// Named explicitly rather than left to the sweep above, because this is the
// one that was actually broken and the sweep would go quiet if the shared
// call were ever refactored out of the call form it matches.
assert.match(read(PROXIES.desktop), /repairKeylessGroup:/,
'without this a keyless group on desktop cannot be healed by any user action (TODO #124/#146)')
})
test('both proxies answer everything the shared invite flow needs', () => {
// The specific list, so a future edit to invite.js that adds a db call is
// caught by name rather than only in aggregate.
const needed = ['getGroup', 'putGroup', 'putMember', 'getProfile',
'isBlockedFromGroup', 'clearBlockedFromGroup', 'deleteGroup', 'repairKeylessGroup']
for (const [host, file] of Object.entries(PROXIES)) {
const have = proxyMethods(file)
for (const m of needed) assert.ok(have.has(m), `${host} proxy is missing ${m}`)
}
})
test('the proxies are allowed to differ where nothing shared depends on them', () => {
// Guards the guard. If someone "fixes" the above by making the two identical,
// this fails and says why that is not the goal: mobile genuinely has haptics
// and QR scanning, desktop genuinely has launch-at-login.
const mobile = proxyMethods(PROXIES.mobile)
const desktop = proxyMethods(PROXIES.desktop)
assert.ok(mobile.has('haptic') && !desktop.has('haptic'), 'haptics are mobile-only by design')
assert.ok(desktop.has('setLaunchAtLogin') && !mobile.has('setLaunchAtLogin'), 'launch-at-login is desktop-only by design')
})