From 4c5722c593d4077ae5abe70056907b7b0b0a80f9 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Fri, 23 Oct 2026 20:27:11 +0800 Subject: [PATCH] test(social): pin the stored owner name over a later whoami on promotion Promoting a room-only identity keeps an owner-level name the record already has (register, forInvite), but nothing pinned that order: flipping existing.ownerName || me.ownerName to me.ownerName || existing.ownerName left plugins/social at 378/378 green. #863's cases start without a name, so they cover the fill-in direction only and pass under either order. The new case stores ownerName 'Alice' and lets whoami answer 'Whoami Name', so only the stored name satisfies it. The suite goes 378 -> 379 and no source changes. Docs-not-needed: test-only change under plugins/social/test/. --- plugins/social/test/social.test.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/social/test/social.test.js b/plugins/social/test/social.test.js index 169bfb946..fecddeebf 100644 --- a/plugins/social/test/social.test.js +++ b/plugins/social/test/social.test.js @@ -382,6 +382,27 @@ test('#863: promotion fills a name the room-only record never had, instead of wa assert.equal(member.roomOnly, undefined, 'only roomOnly is dropped') }) +// The other direction of the promotion fallback (#868): a record that already carries the owner-level name +// keeps it, whatever `whoami` answers now. The stored name is the identity this home already shared with +// the peer (`shareInviter`, #848); `whoami` is only the account's current answer, so it must not rename a +// record that has one. The case above cannot catch this: it starts without a name, so it passes under +// either order of the `existing.ownerName || me.ownerName` fallback. +test('#868: a promotion keeps a stored owner name when whoami answers a different one', async (t) => { + const { ctx, stop } = await team() + t.after(stop) + const server = mockServer() + server.tools.whoami = () => ({ kind: 'owner', display_name: 'Whoami Name', auto_approve_face_rooms: false, connector_identity: { bot_handles: ['@whoami.home'] } }) + const social = createSocial(ctx.flybot, { autoAccept: false, inboxPollMs: 0 }, { client: createMcpClient({ server: 'https://s', token: 'owner-token', fetch: server.fetch }), home: { id: 'h1', handle: '@danyang.home' } }) + t.after(() => social.dispose()) + const scope = ctx.flybot.scope('social') + scope.fields.set('a', 'member', { bot_id: 'm1', handle: '@chief.danyang.home', home: '@alice.home', ownerName: 'Alice', roomOnly: true }) + await social.register(ctx.flybot.bot.get('a'), { forInvite: true }) + const member = scope.fields.get('a', 'member') + assert.equal(member.ownerName, 'Alice', 'the stored owner name wins over a later whoami answer') + assert.equal(member.home, '@alice.home', 'the stored home is kept as well') + assert.equal(member.roomOnly, undefined, 'only roomOnly is dropped') +}) + test('any local Bot can message any Bot of a friend home; the send is held, then signed as the member', async (t) => { const { ctx, tool, stop } = await team() t.after(stop)