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:
co-authored by
Claude Opus 5
parent
33de2223f0
commit
cb6f04ef3c
@@ -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),
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
Reference in New Issue
Block a user