Extract dashboard room message route (#76)
* review: harden readonly git checks and lint verification * test: satisfy readonly reviewer sandbox typing * test: fix readonly reviewer sandbox assertions * test: remove impossible readonly sandbox guards * test: relax readonly sandbox assertions * test: cast readonly sandbox expectation shape * test: cast readonly sandbox expectation through unknown * Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke * Add STEP_DONE guards, verdict storage, and stale delivery suppression * style: format paired stepdone telemetry files * Route STEP_DONE through reviewer * Add structured Discord attachments * style: format structured attachment test * Fix room thread bot output parity * Remove duplicate work item attachment migration from owner branch * Remove legacy dashboard rooms renderer * Extract dashboard parsed body renderer * Extract dashboard redaction helpers * Extract dashboard RoomCardV2 component * Include RoomCardV2 test in vitest suite * Extract dashboard RoomBoardV2 component * Extract dashboard EmptyState component * Extract dashboard InboxPanel component * Split dashboard InboxPanel card renderer * Extract dashboard TaskPanel component * Extract dashboard UsagePanel component * Fix dashboard room thread chunk rendering * Extract dashboard ServicePanel component * Render dashboard live progress markdown * Extract dashboard SettingsPanel component * Fix structured attachment rendering * Extract dashboard simple route table * Extract dashboard settings routes * Extract dashboard service routes * Extract dashboard room timeline routes * Extract dashboard task routes * Extract dashboard overview route * Extract dashboard room message route
This commit is contained in:
165
src/web-dashboard-room-message-routes.test.ts
Normal file
165
src/web-dashboard-room-message-routes.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { NewMessage, RegisteredGroup } from './types.js';
|
||||
import {
|
||||
createRoomMessageIdCache,
|
||||
handleRoomMessageRoute,
|
||||
} from './web-dashboard-room-message-routes.js';
|
||||
|
||||
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(value), {
|
||||
...init,
|
||||
headers: { 'content-type': 'application/json', ...init?.headers },
|
||||
});
|
||||
}
|
||||
|
||||
function roomMessageRequest(
|
||||
roomJid: string,
|
||||
body: unknown,
|
||||
method = 'POST',
|
||||
): Request {
|
||||
return new Request(
|
||||
`http://localhost/api/rooms/${encodeURIComponent(roomJid)}/messages`,
|
||||
{
|
||||
method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: method === 'POST' ? JSON.stringify(body) : undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function makeDeps(
|
||||
overrides: {
|
||||
rooms?: Record<string, RegisteredGroup>;
|
||||
existingMessage?: (chatJid: string, id: string) => boolean;
|
||||
messages?: NewMessage[];
|
||||
queued?: Array<{ chatJid: string; groupFolder: string }>;
|
||||
} = {},
|
||||
) {
|
||||
const messages = overrides.messages ?? [];
|
||||
const queued = overrides.queued ?? [];
|
||||
return {
|
||||
enqueueMessageCheck: (chatJid: string, groupFolder: string) => {
|
||||
queued.push({ chatJid, groupFolder });
|
||||
},
|
||||
loadRoomBindings: () =>
|
||||
overrides.rooms ?? {
|
||||
'dc:ops': {
|
||||
name: '#ops',
|
||||
folder: 'ops-room',
|
||||
added_at: '2026-04-26T05:00:00.000Z',
|
||||
},
|
||||
},
|
||||
messageExists: overrides.existingMessage ?? (() => false),
|
||||
rememberRoomMessageId: createRoomMessageIdCache(),
|
||||
writeChatMetadata: (
|
||||
_chatJid: string,
|
||||
_timestamp: string,
|
||||
_name?: string,
|
||||
_channel?: string,
|
||||
_isGroup?: boolean,
|
||||
) => undefined,
|
||||
writeMessage: (message: NewMessage) => {
|
||||
messages.push(message);
|
||||
},
|
||||
messages,
|
||||
queued,
|
||||
};
|
||||
}
|
||||
|
||||
describe('web dashboard room message routes', () => {
|
||||
it('injects messages and deduplicates repeated request ids', async () => {
|
||||
const messages: NewMessage[] = [];
|
||||
const queued: Array<{ chatJid: string; groupFolder: string }> = [];
|
||||
const deps = makeDeps({ messages, queued });
|
||||
|
||||
const first = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
now: () => '2026-04-26T05:10:00.000Z',
|
||||
request: roomMessageRequest('dc:ops', {
|
||||
nickname: ' 눈쟁이 ',
|
||||
requestId: 'compose 1',
|
||||
text: ' run a dashboard check ',
|
||||
}),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||
});
|
||||
const second = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
request: roomMessageRequest('dc:ops', {
|
||||
requestId: 'compose 1',
|
||||
text: 'second submit',
|
||||
}),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||
});
|
||||
|
||||
expect(first?.status).toBe(200);
|
||||
expect(second?.status).toBe(200);
|
||||
await expect(first?.json()).resolves.toMatchObject({
|
||||
id: 'web-compose-1',
|
||||
queued: true,
|
||||
});
|
||||
await expect(second?.json()).resolves.toMatchObject({
|
||||
id: 'web-compose-1',
|
||||
queued: false,
|
||||
duplicate: true,
|
||||
});
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toMatchObject({
|
||||
chat_jid: 'dc:ops',
|
||||
content: 'run a dashboard check',
|
||||
id: 'web-compose-1',
|
||||
message_source_kind: 'ipc_injected_human',
|
||||
sender: 'web-dashboard',
|
||||
sender_name: '눈쟁이',
|
||||
timestamp: '2026-04-26T05:10:00.000Z',
|
||||
});
|
||||
expect(queued).toEqual([{ chatJid: 'dc:ops', groupFolder: 'ops-room' }]);
|
||||
});
|
||||
|
||||
it('handles fall-through and invalid room message requests', async () => {
|
||||
const deps = makeDeps({ rooms: {} });
|
||||
|
||||
const fallThrough = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
request: new Request('http://localhost/api/overview'),
|
||||
url: new URL('http://localhost/api/overview'),
|
||||
});
|
||||
expect(fallThrough).toBeNull();
|
||||
|
||||
const notConfigured = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
enqueueMessageCheck: undefined,
|
||||
jsonResponse,
|
||||
request: roomMessageRequest('dc:ops', { text: 'hello' }),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||
});
|
||||
expect(notConfigured?.status).toBe(503);
|
||||
|
||||
const empty = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
request: roomMessageRequest('dc:ops', { text: ' ' }),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||
});
|
||||
expect(empty?.status).toBe(400);
|
||||
|
||||
const missing = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
request: roomMessageRequest('dc:missing', { text: 'hello' }),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Amissing/messages'),
|
||||
});
|
||||
expect(missing?.status).toBe(404);
|
||||
|
||||
const wrongMethod = await handleRoomMessageRoute({
|
||||
...deps,
|
||||
jsonResponse,
|
||||
request: roomMessageRequest('dc:ops', null, 'GET'),
|
||||
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||
});
|
||||
expect(wrongMethod?.status).toBe(405);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user