Zwei Maschinen liessen sich nicht auseinanderhalten. Der Watchdog-Fix von 6fa9411 ging ohne Versions-Bump raus, also meldeten beide Hubs 0.10.2, waehrend nur einer den Fix hatte. Zusaetzlich faehrt der Hub aus dist/ — ein Neustart ohne Build zieht still den alten Code wieder hoch. Beides zusammen hat heute eine Diagnose in die falsche Richtung geschickt. - Version auf 0.11.0 (Feature + Fix seit 0.10.2). - version.ts liefert zusaetzlich buildRef(): den kurzen Commit, aus dem der laufende Code stammt. Bewusst LAZY und gecacht — VERSION wird von jedem CLI-Aufruf importiert, ein git-Aufruf beim Import wuerde Befehle verteuern, die ihn nie anzeigen. Bei Nicht-git-Installs faellt versionLabel() sauber auf die blosse Version zurueck. - Board-Header (appHeader) und der aeltere pageHeader zeigen "v0.11.0 · <commit>", mit Tooltip warum die Nummer allein nicht reicht. - /health liefert buildRef, damit sich zwei Maschinen ueber die Leitung vergleichen lassen, ohne das Board zu oeffnen. 318 Tests gruen, tsc --noEmit sauber. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { agentAvatar, designTokensCss, escapeHtml, liveTimerJs, statusPill, appHeader, appHeaderCss, appHeaderOnlyCss } from '../src/server/ui-shared.js';
|
|
import { VERSION, buildRef, versionLabel } from '../src/version.js';
|
|
|
|
describe('ui-shared helpers', () => {
|
|
it('designTokensCss returns the dark palette variables', () => {
|
|
const css = designTokensCss();
|
|
expect(css).toContain('--bg: #0F172A');
|
|
expect(css).toContain('--status-in_progress: #58A6FF');
|
|
expect(css).toContain('prefers-reduced-motion');
|
|
});
|
|
|
|
it('agentAvatar renders initials without emojis', () => {
|
|
const html = agentAvatar('kimi');
|
|
expect(html).toContain('K');
|
|
expect(html).toContain('#7C3AED');
|
|
expect(html).not.toContain('emoji');
|
|
});
|
|
|
|
it('agentAvatar renders deterministic fallback for unknown agents', () => {
|
|
const html = agentAvatar('robo-agent-42');
|
|
expect(html).toContain('R');
|
|
expect(html).toContain('agent-avatar');
|
|
});
|
|
|
|
it('agentAvatar adds architect ring when requested', () => {
|
|
const html = agentAvatar('claude', { architectRing: true });
|
|
expect(html).toContain('box-shadow');
|
|
});
|
|
|
|
it('statusPill renders each known status', () => {
|
|
for (const status of ['open', 'in_progress', 'review', 'done', 'cancelled'] as const) {
|
|
const html = statusPill(status);
|
|
expect(html).toContain(`data-status="${status}"`);
|
|
expect(html).toContain('status-pill');
|
|
}
|
|
});
|
|
|
|
it('escapeHtml escapes dangerous characters', () => {
|
|
expect(escapeHtml('<script>alert("x")</script>')).toBe(
|
|
'<script>alert("x")</script>',
|
|
);
|
|
});
|
|
|
|
it('liveTimerJs contains the setInterval updater', () => {
|
|
const js = liveTimerJs();
|
|
expect(js).toContain('setInterval');
|
|
expect(js).toContain('data-live-timer');
|
|
expect(js).toContain('claimed');
|
|
});
|
|
});
|
|
|
|
describe('appHeader (v2 shared header)', () => {
|
|
it('renders logo, project cell, nav links and status mounts', () => {
|
|
const h = appHeader('demo', 'team');
|
|
expect(h).toContain('/logo.svg');
|
|
expect(h).toContain('id="projectName"');
|
|
expect(h).toContain('b2-proj');
|
|
expect(h).toContain('id="newTaskBtn"');
|
|
expect(h).toContain('id="sseStatus"');
|
|
for (const path of ['/board', '/team', '/activity', '/messages', '/decisions']) {
|
|
expect(h).toContain(`href="${path}"`);
|
|
}
|
|
});
|
|
it('marks the current page active with aria-current', () => {
|
|
const team = appHeader('demo', 'team');
|
|
expect(team).toContain('active" href="/team" aria-current="page"');
|
|
const board = appHeader('demo', 'board');
|
|
expect(board).toContain('active" href="/board" aria-current="page"');
|
|
expect(board).not.toContain('active" href="/team"');
|
|
});
|
|
it('escapes the project name', () => {
|
|
expect(appHeader('<b>x</b>', 'board')).not.toContain('<b>x</b>');
|
|
});
|
|
it('appHeaderOnlyCss is a prefix of appHeaderCss', () => {
|
|
expect(appHeaderCss().startsWith(appHeaderOnlyCss())).toBe(true);
|
|
expect(appHeaderOnlyCss()).toContain('.app-header');
|
|
expect(appHeaderCss()).toContain('.modal-backdrop');
|
|
});
|
|
it('shows version + build commit in the header so two machines can be compared', () => {
|
|
// On 2026-08-09 a fix shipped without a version bump: both hubs reported
|
|
// 0.10.2 while running different code, and the mismatch was invisible.
|
|
// The header therefore carries the commit next to the version.
|
|
const html = appHeader('proj', 'board');
|
|
expect(html).toContain(versionLabel());
|
|
expect(html).toContain(`v${VERSION}`);
|
|
expect(appHeaderOnlyCss()).toContain('.app-header .b2-build');
|
|
});
|
|
|
|
it('versionLabel falls back to the bare version when the commit is unknown', () => {
|
|
// Non-git installs must still render a header — never an empty badge.
|
|
const label = versionLabel();
|
|
expect(label.startsWith(`v${VERSION}`)).toBe(true);
|
|
const ref = buildRef();
|
|
if (ref) expect(label).toBe(`v${VERSION} · ${ref}`);
|
|
else expect(label).toBe(`v${VERSION}`);
|
|
});
|
|
|
|
it('makes the mobile nav horizontally scrollable with 44px tap targets', () => {
|
|
const css = appHeaderOnlyCss();
|
|
expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav \{[^}]*overflow-x: auto/);
|
|
expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav a \{[^}]*min-height: 44px/);
|
|
expect(css).toContain('mask-image: linear-gradient');
|
|
});
|
|
});
|