Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions plugins/social/test/social.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10379,3 +10379,78 @@ test('#881: the two room refusals reach the person as sentences', async (t) => {
code = 'no_relationship'
await assert.rejects(social.sendChat({ handle: '@coder.bob.home', text: 'into the room' }), /the room no longer lets these Bots reach each other/)
})


// #882: the room envelope's hold rule is code and docs only — nothing reds when it is widened. These
// cases pin the two rules: a hold with a finding of its own keeps its card, and an envelope that
// carries an attachment is never cleared even when its hold is a clean blanket one (#847 stays pinned
// by the test above).
test('#882: a room envelope held with a finding of its own never clears itself', async (t) => {
for (const holdReason of ['attachment', 'policy', 'risk']) {
const { ctx, call, stop } = await team()
t.after(stop)
const server = holdingServer({ holdReason })
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())
await grantTeam(ctx.flybot)
await social.registerAll()
const envelope = (op, extra = {}) => `FLYBOT-ROOM-SHARE\n${JSON.stringify({ v: 1, op, roomId: 'session-e2439f82', name: 'You, Chief, Chief', invitedHandle: '@coder.bob.home', seq: 0, ...extra })}`
assert.equal((await ctx.flybot.bot.send({ from: 'a', to: '@coder.bob.home', text: envelope('join'), via: 'room-share' })).action, 'allow')
for (let i = 0; i < 50 && !server.calls.some(c => c.name === 'list_approvals'); i += 1) await new Promise(resolve => setTimeout(resolve, 10))
assert.equal(server.calls.some(c => c.name === 'list_approvals'), true, `${holdReason}: the hold was looked at`)
assert.equal(server.calls.some(c => c.name === 'decide_approval'), false, `${holdReason}: the owner still decides this one`)
assert.equal(server.queue.length, 1, `${holdReason}: the hold is still pending`)
assert.equal((await call('flybot/social/status', {})).value.approvals.length, 1, `${holdReason}: its card is listed`)
}
})

test('#882: an envelope carrying an attachment is not cleared even on a clean blanket hold', async (t) => {
const { ctx, call, stop } = await team()
t.after(stop)
const server = holdingServer({ holdReason: 'blanket_outbound' })
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())
await grantTeam(ctx.flybot)
await social.registerAll()
const envelope = `FLYBOT-ROOM-SHARE\n${JSON.stringify({ v: 1, op: 'join', roomId: 'session-e2439f82', name: 'You, Chief, Chief', invitedHandle: '@coder.bob.home', seq: 0 })}`
const dir = mkdtempSync(join(tmpdir(), 'flybot-att-'))
t.after(() => rmSync(dir, { recursive: true, force: true, maxRetries: 5 }))
const noted = join(dir, 'notes.txt')
writeFileSync(noted, 'a note')
const file = { name: 'notes.txt', size: 6, mime: 'text/plain', path: noted }
// A Bot's file is shown to the owner before it leaves, so the local card comes first; approving it
// is what would let the transport clear Bot Social's blanket hold — and the guard must stop that.
assert.equal((await ctx.flybot.bot.send({ from: 'a', to: '@coder.bob.home', text: envelope, attachments: [file], via: 'room-share' })).action, 'hold')
const card = ctx.flybot.approvals.list().at(-1)
assert.equal(card.request.attachments.length, 1, 'the owner sees the file first')
await ctx.flybot.approvals.decide(card.id, 'approve')
for (let i = 0; i < 50 && server.calls.filter(c => c.name === 'send_as_member').length === 0; i += 1) await new Promise(resolve => setTimeout(resolve, 10))
await new Promise(resolve => setTimeout(resolve, 30))
assert.ok(server.calls.filter(c => c.name === 'send_as_member').at(-1).args.attachments?.length, 'the file went with it')
assert.equal(server.calls.some(c => c.name === 'decide_approval'), false, 'a file-carrying envelope waits for the owner')
assert.equal(server.queue.length, 1, 'its hold is still pending')
assert.equal((await call('flybot/social/status', {})).value.approvals.length, 1)
})

// #881 follow-up (Reviewer's request on #886): the local-room guard had no case — dropping it kept the
// whole suite green. A room line names no room even when both Bots are also in one open face room.
test('#881: a room line names no room, even when both Bots share a face room too', async (t) => {
const server = roomServer()
const peerRoom = server.face.peer('123456')
peerRoom.members[0].published = true
const { ctx, call, social } = await roomTeam(t, server, { autoAccept: true })
await call('flybot/social/face-join', { start: true, code: '123456' })
await ctx.flybot.grants.set('a', [{ grantee: '@coder.bob.home' }])
await social.remoteBots({ fresh: true })
for (const via of ['room', 'room-share']) {
// The local room's line is the group's own record: it names no room, so the far end answers the
// plain 1:1 refusal. That refusal is the guard working — what this case pins is the missing
// `room_id` on the wire.
await assert.rejects(ctx.flybot.bot.send({ from: 'a', to: '@coder.bob.home', text: `a ${via} line`, via, role: 'reply' }))
const sent = server.calls.filter(c => c.name === 'send_as_member').at(-1)
assert.equal('room_id' in sent.args, false, `via: ${via} names no room`)
}
// The control: the same pair over a plain 1:1 line does name it.
assert.equal((await ctx.flybot.bot.send({ from: 'a', to: '@coder.bob.home', text: 'plain line', role: 'reply' })).action, 'allow')
assert.equal(server.calls.filter(c => c.name === 'send_as_member').at(-1).args.room_id, 'frm_123456')
})
Loading