Merge pull request #80 from phj1081/codex/test-dashboard-route-state-helpers
Harden dashboard room helpers and labels
This commit is contained in:
@@ -24,8 +24,13 @@ const formatters = {
|
|||||||
formatDate: (value: string | null | undefined) => value ?? '-',
|
formatDate: (value: string | null | undefined) => value ?? '-',
|
||||||
formatDuration: (value: number | null) => (value === null ? '-' : `${value}`),
|
formatDuration: (value: number | null) => (value === null ? '-' : `${value}`),
|
||||||
formatLiveElapsed: (value: number) => `${value}`,
|
formatLiveElapsed: (value: number) => `${value}`,
|
||||||
senderRoleClass: (value: string | null | undefined) =>
|
senderRoleClass: (value: string | null | undefined) => {
|
||||||
(value ?? '').toLowerCase().includes('owner') ? 'role-owner' : 'role-human',
|
const role = (value ?? '').toLowerCase();
|
||||||
|
if (role.includes('owner')) return 'role-owner';
|
||||||
|
if (role.includes('reviewer')) return 'role-reviewer';
|
||||||
|
if (role.includes('arbiter')) return 'role-arbiter';
|
||||||
|
return 'role-human';
|
||||||
|
},
|
||||||
statusLabel: (status: string) => status,
|
statusLabel: (status: string) => status,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,6 +115,51 @@ describe('RoomCardV2', () => {
|
|||||||
expect(html).toContain('prod 배포 완료');
|
expect(html).toContain('prod 배포 완료');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('localizes protocol role and verdict labels in Korean rooms', () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
createElement(RoomCardV2, {
|
||||||
|
activity: activity({
|
||||||
|
pairedTask: {
|
||||||
|
id: 'task-1',
|
||||||
|
title: 'Review pending',
|
||||||
|
status: 'in_review',
|
||||||
|
roundTripCount: 2,
|
||||||
|
updatedAt: '2026-04-28T02:03:00.000Z',
|
||||||
|
currentTurn: null,
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
turnNumber: 2,
|
||||||
|
role: 'reviewer',
|
||||||
|
verdict: 'continue',
|
||||||
|
createdAt: '2026-04-28T02:02:00.000Z',
|
||||||
|
outputText: '검증 완료. 다음 단계로 진행 가능합니다.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
activityLoading: false,
|
||||||
|
busy: false,
|
||||||
|
draft: '',
|
||||||
|
entry,
|
||||||
|
expanded: true,
|
||||||
|
inboxItems: [],
|
||||||
|
locale: 'ko',
|
||||||
|
onDraftChange: () => {},
|
||||||
|
onSendMessage: () => {},
|
||||||
|
onToggle: () => {},
|
||||||
|
pinned: true,
|
||||||
|
t,
|
||||||
|
...formatters,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain('>리뷰어</span>');
|
||||||
|
expect(html).toContain('>계속</span>');
|
||||||
|
expect(html).not.toContain('>reviewer</span>');
|
||||||
|
expect(html).not.toContain('>continue</span>');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders live progress as markdown', () => {
|
it('renders live progress as markdown', () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
createElement(RoomCardV2, {
|
createElement(RoomCardV2, {
|
||||||
@@ -244,6 +294,7 @@ describe('RoomCardV2', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain('class="room-watcher-fold"');
|
expect(html).toContain('class="room-watcher-fold"');
|
||||||
|
expect(html).toContain('>리뷰어</strong>');
|
||||||
expect(html).toContain(watcherTail);
|
expect(html).toContain(watcherTail);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { DashboardOverview, DashboardRoomActivity } from './api';
|
|||||||
import type { Locale, Messages } from './i18n';
|
import type { Locale, Messages } from './i18n';
|
||||||
import { ParsedBody } from './ParsedBody';
|
import { ParsedBody } from './ParsedBody';
|
||||||
import { RoomAttachmentGallery } from './RoomAttachmentGallery';
|
import { RoomAttachmentGallery } from './RoomAttachmentGallery';
|
||||||
|
import { displayRole, displayVerdict } from './roomDisplayLabels';
|
||||||
import {
|
import {
|
||||||
buildRoomThreadEntries,
|
buildRoomThreadEntries,
|
||||||
isWatcherRoomMessage,
|
isWatcherRoomMessage,
|
||||||
@@ -421,7 +422,9 @@ function RoomCurrentTurnSummary({
|
|||||||
return (
|
return (
|
||||||
<div className="room-current-turn">
|
<div className="room-current-turn">
|
||||||
<span>
|
<span>
|
||||||
<strong className={senderRoleClass(turn.role)}>{turn.role}</strong>
|
<strong className={senderRoleClass(turn.role)}>
|
||||||
|
{displayRole(turn.role, locale)}
|
||||||
|
</strong>
|
||||||
<em>{turn.intentKind}</em>
|
<em>{turn.intentKind}</em>
|
||||||
{turn.attemptNo > 1 ? (
|
{turn.attemptNo > 1 ? (
|
||||||
<small>
|
<small>
|
||||||
@@ -504,7 +507,9 @@ function CollapsedLiveTurn({
|
|||||||
<header>
|
<header>
|
||||||
<span className="live-dot" aria-hidden />
|
<span className="live-dot" aria-hidden />
|
||||||
<span className="live-label">LIVE</span>
|
<span className="live-label">LIVE</span>
|
||||||
<strong className={senderRoleClass(turn.role)}>{turn.role}</strong>
|
<strong className={senderRoleClass(turn.role)}>
|
||||||
|
{displayRole(turn.role, locale)}
|
||||||
|
</strong>
|
||||||
<em>{turn.intentKind}</em>
|
<em>{turn.intentKind}</em>
|
||||||
<span className="live-state pill pill-processing">{turn.state}</span>
|
<span className="live-state pill pill-processing">{turn.state}</span>
|
||||||
{turn.executorServiceId ? (
|
{turn.executorServiceId ? (
|
||||||
@@ -574,7 +579,7 @@ function CollapsedWatcherStrip({
|
|||||||
<span className="watcher-tag">워쳐 {watcherCount}</span>
|
<span className="watcher-tag">워쳐 {watcherCount}</span>
|
||||||
<span className="watcher-line">
|
<span className="watcher-line">
|
||||||
<strong className={senderRoleClass(lastWatcher.senderName)}>
|
<strong className={senderRoleClass(lastWatcher.senderName)}>
|
||||||
{lastWatcher.senderName}
|
{displayRole(lastWatcher.senderName, locale)}
|
||||||
</strong>
|
</strong>
|
||||||
<span>{lastWatcher.content.slice(0, 90)}</span>
|
<span>{lastWatcher.content.slice(0, 90)}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -764,7 +769,7 @@ function RoomTimelineEntry({
|
|||||||
<li className={`room-timeline-item ${sectionClass}`}>
|
<li className={`room-timeline-item ${sectionClass}`}>
|
||||||
<header className="room-timeline-header">
|
<header className="room-timeline-header">
|
||||||
<span className={`role-chip ${senderRoleClass(entry.senderName)}`}>
|
<span className={`role-chip ${senderRoleClass(entry.senderName)}`}>
|
||||||
{entry.senderName}
|
{displayRole(entry.senderName, locale)}
|
||||||
</span>
|
</span>
|
||||||
<time>{formatDate(entry.timestamp, locale)}</time>
|
<time>{formatDate(entry.timestamp, locale)}</time>
|
||||||
{entry.turnNumber !== undefined ? (
|
{entry.turnNumber !== undefined ? (
|
||||||
@@ -774,7 +779,7 @@ function RoomTimelineEntry({
|
|||||||
) : null}
|
) : null}
|
||||||
{verdict ? (
|
{verdict ? (
|
||||||
<span className={`parsed-marker parsed-marker-${verdictTone}`}>
|
<span className={`parsed-marker parsed-marker-${verdictTone}`}>
|
||||||
{verdict}
|
{displayVerdict(verdict, locale)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
@@ -818,7 +823,7 @@ function RoomLiveTimelineEntry({
|
|||||||
<span className="paused-dot" aria-hidden />
|
<span className="paused-dot" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<span className={`role-chip ${senderRoleClass(turn.role)}`}>
|
<span className={`role-chip ${senderRoleClass(turn.role)}`}>
|
||||||
{turn.role}
|
{displayRole(turn.role, locale)}
|
||||||
</span>
|
</span>
|
||||||
<time>
|
<time>
|
||||||
{formatDate(turn.progressUpdatedAt ?? turn.updatedAt, locale)}
|
{formatDate(turn.progressUpdatedAt ?? turn.updatedAt, locale)}
|
||||||
@@ -866,7 +871,7 @@ function RoomWatcherFold({
|
|||||||
<li className="room-watcher-item" key={message.id}>
|
<li className="room-watcher-item" key={message.id}>
|
||||||
<header>
|
<header>
|
||||||
<strong className={senderRoleClass(message.senderName)}>
|
<strong className={senderRoleClass(message.senderName)}>
|
||||||
{message.senderName}
|
{displayRole(message.senderName, locale)}
|
||||||
</strong>
|
</strong>
|
||||||
<time>{formatDate(message.timestamp, locale)}</time>
|
<time>{formatDate(message.timestamp, locale)}</time>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
34
apps/dashboard/src/roomDisplayLabels.ts
Normal file
34
apps/dashboard/src/roomDisplayLabels.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Locale } from './i18n';
|
||||||
|
|
||||||
|
const KO_ROLE_LABELS: Record<string, string> = {
|
||||||
|
arbiter: '중재자',
|
||||||
|
owner: '오너',
|
||||||
|
reviewer: '리뷰어',
|
||||||
|
};
|
||||||
|
|
||||||
|
const KO_VERDICT_LABELS: Record<string, string> = {
|
||||||
|
blocked: '차단',
|
||||||
|
continue: '계속',
|
||||||
|
done: '완료',
|
||||||
|
done_with_concerns: '우려 있음',
|
||||||
|
needs_context: '컨텍스트 필요',
|
||||||
|
proceed: '진행',
|
||||||
|
reset: '초기화',
|
||||||
|
revise: '수정 필요',
|
||||||
|
step_done: '단계 완료',
|
||||||
|
task_done: '작업 완료',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function displayRole(
|
||||||
|
value: string | null | undefined,
|
||||||
|
locale: Locale,
|
||||||
|
): string {
|
||||||
|
const raw = value ?? '';
|
||||||
|
if (locale !== 'ko') return raw;
|
||||||
|
return KO_ROLE_LABELS[raw.trim().toLowerCase()] ?? raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayVerdict(value: string, locale: Locale): string {
|
||||||
|
if (locale !== 'ko') return value;
|
||||||
|
return KO_VERDICT_LABELS[value.trim().toLowerCase()] ?? value;
|
||||||
|
}
|
||||||
@@ -77,6 +77,38 @@ function makeDeps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('web dashboard inbox action routes', () => {
|
describe('web dashboard inbox action routes', () => {
|
||||||
|
it('tracks timestamped and global inbox dismissals separately', () => {
|
||||||
|
const tracker = createInboxDismissTracker();
|
||||||
|
|
||||||
|
tracker.dismiss('ci:task-1', '2026-04-26T05:00:00.000Z');
|
||||||
|
expect(
|
||||||
|
tracker.isDismissed({
|
||||||
|
id: 'ci:task-1',
|
||||||
|
lastOccurredAt: '2026-04-26T05:00:00.000Z',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
tracker.isDismissed({
|
||||||
|
id: 'ci:task-1',
|
||||||
|
lastOccurredAt: '2026-04-26T05:05:00.000Z',
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
tracker.dismiss('ci:task-2', null);
|
||||||
|
expect(
|
||||||
|
tracker.isDismissed({
|
||||||
|
id: 'ci:task-2',
|
||||||
|
lastOccurredAt: '2026-04-26T05:00:00.000Z',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
tracker.isDismissed({
|
||||||
|
id: 'ci:task-2',
|
||||||
|
lastOccurredAt: '2026-04-26T05:05:00.000Z',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('dismisses inbox items and queues paired follow-up intents', async () => {
|
it('dismisses inbox items and queues paired follow-up intents', async () => {
|
||||||
const dismissTracker = createInboxDismissTracker();
|
const dismissTracker = createInboxDismissTracker();
|
||||||
const task = makePairedTask({ id: 'merge-1', status: 'merge_ready' });
|
const task = makePairedTask({ id: 'merge-1', status: 'merge_ready' });
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ function makeDeps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('web dashboard room message routes', () => {
|
describe('web dashboard room message routes', () => {
|
||||||
|
it('expires the oldest remembered room message ids after the cache limit', () => {
|
||||||
|
const remember = createRoomMessageIdCache(2);
|
||||||
|
|
||||||
|
expect(remember('room:message-1')).toBe(true);
|
||||||
|
expect(remember('room:message-1')).toBe(false);
|
||||||
|
expect(remember('room:message-2')).toBe(true);
|
||||||
|
expect(remember('room:message-3')).toBe(true);
|
||||||
|
expect(remember('room:message-1')).toBe(true);
|
||||||
|
expect(remember('room:message-2')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('injects messages and deduplicates repeated request ids', async () => {
|
it('injects messages and deduplicates repeated request ids', async () => {
|
||||||
const messages: NewMessage[] = [];
|
const messages: NewMessage[] = [];
|
||||||
const queued: Array<{ chatJid: string; groupFolder: string }> = [];
|
const queued: Array<{ chatJid: string; groupFolder: string }> = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user