Skip to content

Commit 594616a

Browse files
authored
Merge pull request #22 from bertheto/feat/structured-metadata
UP-17: Structured message metadata for agent orchestration
2 parents 3812e57 + 553f1a3 commit 594616a

4 files changed

Lines changed: 1457 additions & 921 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Tests for UP-17: Structured message metadata — handoff badge + stop_reason tag rendering.
3+
*
4+
* Tests the CSS class and badge logic that would be applied in index.html
5+
* when rendering messages with metadata.
6+
*/
7+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8+
9+
// ─────────────────────────────────────────────
10+
// Helpers — mirror the logic from index.html renderMessage()
11+
// ─────────────────────────────────────────────
12+
13+
function esc(str) {
14+
return String(str ?? '')
15+
.replace(/&/g, '&')
16+
.replace(/</g, '&lt;')
17+
.replace(/>/g, '&gt;')
18+
.replace(/"/g, '&quot;');
19+
}
20+
21+
function parseMetadata(raw) {
22+
if (!raw) return null;
23+
try {
24+
return typeof raw === 'string' ? JSON.parse(raw) : raw;
25+
} catch {
26+
return null;
27+
}
28+
}
29+
30+
function buildMetadataBadgesHtml(metaObj) {
31+
let html = '';
32+
if (!metaObj) return html;
33+
if (metaObj.handoff_target) {
34+
html += `<span class="handoff-badge" title="Handoff to: ${esc(metaObj.handoff_target)}">\u2192 ${esc(metaObj.handoff_target)}</span>`;
35+
}
36+
if (metaObj.stop_reason) {
37+
html += `<span class="stop-tag stop-tag-${esc(metaObj.stop_reason)}" title="Stop reason">${esc(metaObj.stop_reason)}</span>`;
38+
}
39+
return html;
40+
}
41+
42+
function renderMessageRow(message) {
43+
const container = document.createElement('div');
44+
const metaObj = parseMetadata(message.metadata);
45+
const badgesHtml = buildMetadataBadgesHtml(metaObj);
46+
container.innerHTML = `
47+
<div class="msg-header">
48+
<span class="msg-author-label">${esc(message.author)}</span>
49+
<span class="msg-time-label">seq ${message.seq}</span>
50+
${badgesHtml}
51+
</div>
52+
<div class="bubble-v2">${esc(message.content)}</div>
53+
`;
54+
return container;
55+
}
56+
57+
// ─────────────────────────────────────────────
58+
// Tests
59+
// ─────────────────────────────────────────────
60+
61+
describe('structured metadata — handoff badge', () => {
62+
it('displays handoff-badge when metadata.handoff_target is set', () => {
63+
const row = renderMessageRow({
64+
author: 'agent-a',
65+
content: 'Handing off',
66+
seq: 5,
67+
metadata: JSON.stringify({ handoff_target: 'agent-b' }),
68+
});
69+
const badge = row.querySelector('.handoff-badge');
70+
expect(badge).not.toBeNull();
71+
expect(badge.textContent.trim()).toContain('agent-b');
72+
});
73+
74+
it('does NOT render handoff-badge when no handoff_target', () => {
75+
const row = renderMessageRow({
76+
author: 'agent-a',
77+
content: 'Regular message',
78+
seq: 3,
79+
metadata: null,
80+
});
81+
expect(row.querySelector('.handoff-badge')).toBeNull();
82+
});
83+
84+
it('handoff-badge title attribute contains the target agent ID', () => {
85+
const row = renderMessageRow({
86+
author: 'agent-a',
87+
content: 'Directing you',
88+
seq: 7,
89+
metadata: { handoff_target: 'specific-target' },
90+
});
91+
const badge = row.querySelector('.handoff-badge');
92+
expect(badge).not.toBeNull();
93+
expect(badge.getAttribute('title')).toContain('specific-target');
94+
});
95+
});
96+
97+
describe('structured metadata — stop reason tag', () => {
98+
it('displays stop-tag when metadata.stop_reason is set', () => {
99+
const row = renderMessageRow({
100+
author: 'agent-a',
101+
content: 'Done',
102+
seq: 10,
103+
metadata: JSON.stringify({ stop_reason: 'convergence' }),
104+
});
105+
const tag = row.querySelector('.stop-tag');
106+
expect(tag).not.toBeNull();
107+
expect(tag.textContent.trim()).toBe('convergence');
108+
});
109+
110+
it('stop-tag has the reason-specific class', () => {
111+
const reasons = ['convergence', 'timeout', 'error', 'complete', 'impasse'];
112+
for (const reason of reasons) {
113+
const row = renderMessageRow({
114+
author: 'agent-a',
115+
content: 'Stopping',
116+
seq: 1,
117+
metadata: { stop_reason: reason },
118+
});
119+
const tag = row.querySelector(`.stop-tag-${reason}`);
120+
expect(tag).not.toBeNull();
121+
}
122+
});
123+
124+
it('does NOT render stop-tag when no stop_reason', () => {
125+
const row = renderMessageRow({
126+
author: 'agent-a',
127+
content: 'Regular',
128+
seq: 2,
129+
metadata: null,
130+
});
131+
expect(row.querySelector('.stop-tag')).toBeNull();
132+
});
133+
});
134+
135+
describe('structured metadata — combined', () => {
136+
it('shows both handoff-badge and stop-tag when both are set', () => {
137+
const row = renderMessageRow({
138+
author: 'agent-a',
139+
content: 'Done and passing',
140+
seq: 8,
141+
metadata: { handoff_target: 'agent-b', stop_reason: 'complete' },
142+
});
143+
expect(row.querySelector('.handoff-badge')).not.toBeNull();
144+
expect(row.querySelector('.stop-tag')).not.toBeNull();
145+
});
146+
147+
it('shows no badges when metadata is empty object', () => {
148+
const row = renderMessageRow({
149+
author: 'agent-a',
150+
content: 'Nothing special',
151+
seq: 9,
152+
metadata: {},
153+
});
154+
expect(row.querySelector('.handoff-badge')).toBeNull();
155+
expect(row.querySelector('.stop-tag')).toBeNull();
156+
});
157+
});

0 commit comments

Comments
 (0)