feat: sync interview content in real time

Publish shared-document and note events through RabbitMQ, stream token-scoped document updates to participants, and update connected interviewer and participant browsers through resilient SSE subscriptions.

Add snapshots, heartbeats, reconnection behavior, proxy streaming configuration, privacy filtering, and end-to-end RabbitMQ coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Daniel Noulet 2026-09-19 22:22:42 +02:00
parent e4fee06e2f
commit c050815900
16 changed files with 438 additions and 101 deletions

View file

@ -853,7 +853,17 @@ describe('Go Guess frontend', () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(`event: note\ndata: ${JSON.stringify(liveNote)}\n\n`),
new TextEncoder().encode(
`event: interview-event\ndata: ${JSON.stringify({
type: 'note.created',
interviewId: 80,
note: liveNote,
})}\n\nevent: interview-event\ndata: ${JSON.stringify({
type: 'document.updated',
interviewId: 80,
sharedDocument: 'Remote agenda',
})}\n\n`,
),
)
controller.close()
},
@ -867,6 +877,7 @@ describe('Go Guess frontend', () => {
})
renderApp('/scheduled-interviews/80')
expect(await screen.findByText('Strong system design answer.')).toBeInTheDocument()
expect(screen.getByLabelText('Shared documentation')).toHaveValue('Remote agenda')
await user.click(screen.getByRole('button', { name: 'Copy participant interview link' }))
expect(writeText).toHaveBeenCalledWith('/participant/meeting/meeting-token')
expect(
@ -903,11 +914,30 @@ describe('Go Guess frontend', () => {
status: 'scheduled',
sharedDocument: 'Please prepare the architecture exercise.',
})
if (String(input) === '/api/participant-meetings/meeting-token/events') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
`event: interview-event\ndata: ${JSON.stringify({
type: 'document.updated',
interviewId: 80,
sharedDocument: 'The architecture exercise changed live.',
})}\n\n`,
),
)
controller.close()
},
})
return Promise.resolve(
new Response(stream, { status: 200, headers: { 'Content-Type': 'text/event-stream' } }),
)
}
throw new Error(`Unexpected request: ${String(input)}`)
})
renderApp('/participant/meeting/meeting-token')
expect(await screen.findByRole('heading', { name: job.title })).toBeInTheDocument()
expect(screen.getByText('Please prepare the architecture exercise.')).toBeInTheDocument()
expect(await screen.findByText('The architecture exercise changed live.')).toBeInTheDocument()
expect(screen.queryByText(/live notes/i)).not.toBeInTheDocument()
expect(screen.queryByText(question.referenceAnswer)).not.toBeInTheDocument()
})