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:
Eyejoker
2026-04-28 19:25:49 +09:00
committed by GitHub
parent 87d0ef8c05
commit 49382713ca
3 changed files with 358 additions and 127 deletions

View 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);
});
});