feat(iios): @mentions via inbox fan-out + my-annotations query (pins/saves)
Mentions ride the existing event-driven inbox — no chat parsing in the kernel: - send() carries an OPAQUE mentions[] (userIds) into the message event; the kernel never parses "@". The app supplies the notify-list. - InboxProjector fans out a MENTION inbox item (new generic inbox kind) to each mentioned *participant* (never the sender), idempotent per source message; reading the thread resolves the reader's MENTION + NEEDS_REPLY items to DONE. - New MENTION value in the generic IiosInboxItemKind taxonomy (migration). Pins/saves reuse the annotation primitive; new generic query powers a Saved list: - MessageService.listMyAnnotated(principal, type) + GET /v1/threads/my-annotations returns the caller's annotated messages (type "save") with thread context. Tests: 182 pass (+4: mention fan-out, resolve-on-read, saved query, opaque mentions). Verified live: mention→inbox delivery, read→resolve, pin/save persistence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,4 +202,28 @@ describe('Interaction annotations (generic reactions primitive)', () => {
|
||||
const msg = await s.send(threadId, alice, { content: 'secret' }, 'k1');
|
||||
await expect(s.toggleAnnotation(msg.id, bob, 'reaction', '👍')).rejects.toBeInstanceOf(PolicyDeniedError);
|
||||
});
|
||||
|
||||
it('listMyAnnotated returns the caller’s saved messages with thread context; others see none', async () => {
|
||||
const s = gov();
|
||||
const { threadId } = await s.openThread(null, alice, { membership: 'group', creatorRole: 'ADMIN', subject: 'Design' });
|
||||
await s.addParticipant(threadId, alice, 'bob');
|
||||
const msg = await s.send(threadId, alice, { content: 'save this' }, 'k1');
|
||||
await s.toggleAnnotation(msg.id, alice, 'save', ''); // save is a personal annotation
|
||||
|
||||
const saved = await s.listMyAnnotated(alice, 'save');
|
||||
expect(saved).toHaveLength(1);
|
||||
expect(saved[0]).toMatchObject({ threadId, threadSubject: 'Design' });
|
||||
expect(saved[0]?.message.content).toBe('save this');
|
||||
expect(await s.listMyAnnotated(bob, 'save')).toHaveLength(0); // bob saved nothing
|
||||
});
|
||||
|
||||
it('send carries an OPAQUE mentions[] into the message event (kernel never parses @)', async () => {
|
||||
const s = svc();
|
||||
const { threadId } = await s.openThread(null, alice);
|
||||
await s.openThread(threadId, bob);
|
||||
await s.send(threadId, alice, { content: 'hi @bob' }, 'k1', undefined, undefined, ['bob']);
|
||||
const ev = await prisma.iiosOutboxEvent.findFirstOrThrow({ where: { eventType: 'com.insignia.iios.message.sent.v1' } });
|
||||
const ce = ev.cloudEvent as { data: { mentions?: string[] } };
|
||||
expect(ce.data.mentions).toEqual(['bob']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user