mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
test(realtime): broadcast parity for every MCP write tool
A suite under tests/integration/broadcast-parity boots the app once per group, runs a write tool through a real MCP session the way the assistant attaches the registry, runs its REST twin on a twin scene, and records every broadcast. Per case it checks the exact events in order and count, that no MCP broadcast skips a socket (the asking tab hears its own change), that nothing is sent inside a transaction, that every payload parses with its registered schema, that REST sends the same sequence to the same room and recipients, and that MCP's keys cover REST's. A registry test lists the write tools as an MCP client sees them and fails on a tool without a row, a stale row, or an exemption without a reason; exemptions run too and must send nothing, and the tools still waiting for a case are frozen in a list that may only shrink. The new events the next changes send are registered now (member:updated, todo:reordered, trips:changed, settings:updated, tags:changed, notification:read-all, journey:updated, journey:deleted, atlas:changed); the client keeps them apart as awaiting a sender, and the journey page already leaves a journey that journey:deleted says is gone.
This commit is contained in:
@@ -72,6 +72,9 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [
|
||||
'vacay:share-removed',
|
||||
'vacay:shared-update',
|
||||
// Journey — pages/journeyDetail/useJourneyDetail ('journey:' prefix listener)
|
||||
// reads the open journey again, and leaves it once journey:deleted says it is gone.
|
||||
'journey:updated',
|
||||
'journey:deleted',
|
||||
'journey:trip:synced',
|
||||
'journey:entry:created',
|
||||
'journey:entry:updated',
|
||||
@@ -142,3 +145,19 @@ export const IGNORED_WS_EVENTS = [
|
||||
'member:added',
|
||||
'member:removed',
|
||||
] as const satisfies readonly TrekWsEventName[]
|
||||
|
||||
/**
|
||||
* Events registered ahead of the write that sends them: nothing on the server
|
||||
* sends these yet, so there is nothing for the client to act on. They are not
|
||||
* a decision to ignore. The change that adds a sender moves its event to a
|
||||
* handler, and the list only shrinks.
|
||||
*/
|
||||
export const AWAITING_SENDER_WS_EVENTS = [
|
||||
'member:updated',
|
||||
'todo:reordered',
|
||||
'trips:changed',
|
||||
'settings:updated',
|
||||
'tags:changed',
|
||||
'notification:read-all',
|
||||
'atlas:changed',
|
||||
] as const satisfies readonly TrekWsEventName[]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-JRN-DETHOOK-001 to FE-JRN-DETHOOK-031
|
||||
// FE-JRN-DETHOOK-001 to FE-JRN-DETHOOK-032
|
||||
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
|
||||
import { useLocation } from 'react-router';
|
||||
import { delay, http, HttpResponse } from 'msw';
|
||||
@@ -243,6 +243,23 @@ describe('useJourneyDetail', () => {
|
||||
expect(latest.current?.title).toBe('Japan 2026');
|
||||
});
|
||||
|
||||
it('FE-JRN-DETHOOK-032: a journey deleted elsewhere returns to the journey list instead of reading it again', async () => {
|
||||
setup();
|
||||
await waitFor(() => expect(latest.current).not.toBeNull());
|
||||
const handler = vi.mocked(addListener).mock.calls[0][0] as (e: Record<string, unknown>) => void;
|
||||
const reads = vi.fn();
|
||||
server.use(http.get('/api/journeys/7', () => {
|
||||
reads();
|
||||
return HttpResponse.json({ error: 'gone' }, { status: 404 });
|
||||
}));
|
||||
|
||||
handler({ type: 'journey:deleted', journeyId: 7 });
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/journey');
|
||||
expect(addToast).toHaveBeenCalledWith('Journey deleted', 'info', undefined);
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
expect(reads).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-JRN-DETHOOK-010: the websocket listener is detached on unmount', async () => {
|
||||
const { unmount } = setup();
|
||||
await waitFor(() => expect(addListener).toHaveBeenCalled());
|
||||
|
||||
@@ -123,6 +123,15 @@ export function useJourneyDetail({ journeyId, embedded = false }: JourneyDetailO
|
||||
const type = event.type as string
|
||||
if (!type?.startsWith('journey:')) return
|
||||
if (event.journeyId !== journeyId) return
|
||||
// Its owner deleted it elsewhere: there is nothing left to read again, so
|
||||
// the page goes back to the list (a journal shown inside a trip stays put).
|
||||
if (type === 'journey:deleted') {
|
||||
if (!embedded) {
|
||||
toast.info(t('journey.deleted'))
|
||||
void navigate('/journey')
|
||||
}
|
||||
return
|
||||
}
|
||||
// reload journey data on any change from other contributors
|
||||
loadJourney(journeyId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user