* 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
151 lines
4.0 KiB
TypeScript
151 lines
4.0 KiB
TypeScript
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { DashboardOverview, StatusSnapshot } from './api';
|
|
import { messages, type Messages } from './i18n';
|
|
import { ServicePanel, type ServicePanelProps } from './ServicePanel';
|
|
|
|
const t = messages.en;
|
|
|
|
function formatDuration(value: number | null, t: Messages): string {
|
|
if (value === null) return '-';
|
|
const seconds = Math.floor(value / 1000);
|
|
if (seconds < 60) return `${seconds}${t.units.second}`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes}${t.units.minute}`;
|
|
const hours = Math.floor(minutes / 60);
|
|
return `${hours}${t.units.hour} ${minutes % 60}${t.units.minute}`;
|
|
}
|
|
|
|
function overview(
|
|
services: DashboardOverview['services'],
|
|
overrides: Partial<DashboardOverview> = {},
|
|
): DashboardOverview {
|
|
return {
|
|
generatedAt: '2026-04-28T04:15:00.000Z',
|
|
inbox: [],
|
|
operations: { serviceRestarts: [] },
|
|
rooms: { active: 0, inactive: 0, total: 0, waiting: 0 },
|
|
services,
|
|
tasks: {
|
|
active: 0,
|
|
completed: 0,
|
|
paused: 0,
|
|
total: 0,
|
|
watchers: { active: 0, completed: 0, paused: 0 },
|
|
},
|
|
usage: { fetchedAt: '2026-04-28T04:15:00.000Z', rows: [] },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const snapshots: StatusSnapshot[] = [
|
|
{
|
|
agentType: 'codex',
|
|
assistantName: 'owner',
|
|
entries: [
|
|
{
|
|
agentType: 'codex',
|
|
elapsedMs: null,
|
|
folder: 'eyejokerdb',
|
|
jid: 'room-1',
|
|
name: 'eyejokerdb-main',
|
|
pendingMessages: true,
|
|
pendingTasks: 3,
|
|
status: 'waiting',
|
|
},
|
|
],
|
|
serviceId: 'svc-owner',
|
|
updatedAt: '2026-04-28T04:14:00.000Z',
|
|
},
|
|
];
|
|
|
|
const baseProps: ServicePanelProps = {
|
|
formatDuration,
|
|
locale: 'en',
|
|
onRestartStack: () => {},
|
|
overview: overview(
|
|
[
|
|
{
|
|
activeRooms: 2,
|
|
agentType: 'codex',
|
|
assistantName: 'owner',
|
|
serviceId: 'svc-owner',
|
|
totalRooms: 3,
|
|
updatedAt: '2026-04-28T04:14:00.000Z',
|
|
},
|
|
{
|
|
activeRooms: 0,
|
|
agentType: 'claude',
|
|
assistantName: 'reviewer',
|
|
serviceId: 'svc-reviewer',
|
|
totalRooms: 1,
|
|
updatedAt: '2026-04-28T04:08:00.000Z',
|
|
},
|
|
],
|
|
{
|
|
inbox: [
|
|
{
|
|
createdAt: '2026-04-28T04:10:00.000Z',
|
|
groupFolder: 'eyejokerdb',
|
|
groupKey: 'ci',
|
|
id: 'ci-1',
|
|
kind: 'ci-failure',
|
|
lastOccurredAt: '2026-04-28T04:12:00.000Z',
|
|
occurrences: 2,
|
|
occurredAt: '2026-04-28T04:10:00.000Z',
|
|
severity: 'error',
|
|
source: 'status-snapshot',
|
|
summary: 'CI failed',
|
|
title: 'CI failed',
|
|
},
|
|
],
|
|
operations: {
|
|
serviceRestarts: [
|
|
{
|
|
completedAt: null,
|
|
id: 'restart-1',
|
|
requestedAt: '2026-04-28T04:13:00.000Z',
|
|
services: ['owner', 'reviewer'],
|
|
status: 'running',
|
|
target: 'stack',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
),
|
|
serviceActionKey: null,
|
|
snapshots,
|
|
t,
|
|
};
|
|
|
|
describe('ServicePanel', () => {
|
|
it('renders service health, queue signals, and restart log', () => {
|
|
const html = renderToStaticMarkup(createElement(ServicePanel, baseProps));
|
|
|
|
expect(html).toContain(t.health.levels.stale);
|
|
expect(html).toContain(t.health.ciFailures);
|
|
expect(html).toContain('2');
|
|
expect(html).toContain('3');
|
|
expect(html).toContain('reviewer');
|
|
expect(html).toContain(t.health.restartLog);
|
|
expect(html).toContain('owner, reviewer');
|
|
});
|
|
|
|
it('renders restart pending state and empty services state', () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(ServicePanel, {
|
|
...baseProps,
|
|
overview: overview([]),
|
|
serviceActionKey: 'stack:restart',
|
|
snapshots: [],
|
|
}),
|
|
);
|
|
|
|
expect(html).toContain(t.health.restarting);
|
|
expect(html).toContain('disabled=""');
|
|
expect(html).toContain(t.service.empty);
|
|
});
|
|
});
|