// @vitest-environment happy-dom // // A card an agent posted has to be distinguishable at a glance from one a // person typed into the form — that is the whole point of storing postedByAgent // separately from postedByName. The meta line joins up to three facts with // ' · ', and any of the first two can be missing. import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('../public/modules/ws.js', () => ({ send: vi.fn(() => true) })); vi.mock('../public/modules/terminal.js', () => ({ switchToSession: vi.fn(), restartAgent: vi.fn(), restartPending: () => false })); import { jobs, agents, repos, setActiveSession } from '../public/modules/state.js'; import { handleJobsList, showJobBoard, setupJobBoard } from '../public/modules/jobs.js'; const BOARD_HTML = `
`; const JOB = (over = {}) => ({ id: 'job-1', title: 'Add rate limiting', detail: 'Token bucket.', repoPath: '/repos/alpha', state: 'todo', postedByName: null, postedByAgent: null, postedAt: new Date().toISOString(), agentSessionId: null, agentName: null, startedAt: null, branchName: null, prUrl: null, prNumber: null, lastError: null, ...over, }); beforeEach(() => { document.body.innerHTML = BOARD_HTML; jobs.clear(); agents.clear(); repos.clear(); repos.set('/repos/alpha', { slug: 'alpha', exists: true }); setActiveSession(null); vi.clearAllMocks(); setupJobBoard(); showJobBoard(); }); const postedLine = () => document.querySelector('.job-card-posted'); describe('a card an agent posted', () => { it('names the person and the agent, in that order', () => { handleJobsList({ jobs: [JOB({ postedByName: 'Bill', postedByAgent: 'Onyx' })], settings: {} }); const text = postedLine().textContent; expect(text).toContain('Bill'); expect(text).toContain('via Onyx'); expect(text.indexOf('Bill')).toBeLessThan(text.indexOf('via Onyx')); }); it('tints the agent so a machine-queued card reads differently', () => { handleJobsList({ jobs: [JOB({ postedByName: 'Bill', postedByAgent: 'Onyx' })], settings: {} }); expect(postedLine().querySelector('.job-card-via').textContent).toBe('via Onyx'); }); it('names the agent as the author when no human owns the terminal yet', () => { // Auth is off by default, so a session has no owner and postedByName is // null: the card reads "posted by Mirage", not "via Mirage" with no one // in front of it. handleJobsList({ jobs: [JOB({ postedByAgent: 'Mirage' })], settings: {} }); const text = postedLine().textContent; expect(text.startsWith('posted by Mirage · ')).toBe(true); expect(text).not.toContain('via'); }); it('says who posted it in words', () => { handleJobsList({ jobs: [JOB({ postedByName: 'Bill', postedByAgent: 'Onyx' })], settings: {} }); expect(postedLine().textContent.startsWith('posted by Bill via Onyx · ')).toBe(true); }); it('says when an agent rewrote the card, not only who queued it', () => { // An edit through the board's edit_job tool replaces the detail that // becomes the next agent's prompt. The card must not go on reading as // though the person who queued it wrote what is there now. handleJobsList({ jobs: [JOB({ postedByName: 'Bill', editedByAgent: 'Onyx', editedAt: new Date().toISOString(), })], settings: {} }); const text = postedLine().textContent; expect(text).toContain('Bill'); expect(text).toContain('edited by Onyx'); }); it('escapes the name of an agent that edited, the same as one that posted', () => { handleJobsList({ jobs: [JOB({ editedByAgent: '', editedAt: new Date().toISOString(), })], settings: {} }); expect(postedLine().querySelector('img')).toBeNull(); expect(postedLine().textContent).toContain(''); }); it('escapes an agent name rather than rendering it as markup', () => { handleJobsList({ jobs: [JOB({ postedByAgent: '' })], settings: {} }); expect(postedLine().querySelector('img')).toBeNull(); expect(postedLine().textContent).toContain(''); }); }); describe('a card a person typed', () => { it('is unchanged — no "via", no stray separator', () => { handleJobsList({ jobs: [JOB({ postedByName: 'Bill' })], settings: {} }); const text = postedLine().textContent; expect(text).toContain('Bill'); expect(text).not.toContain('via'); expect(postedLine().querySelector('.job-card-via')).toBeNull(); }); it('shows only the time when nothing else is known', () => { handleJobsList({ jobs: [JOB()], settings: {} }); expect(postedLine().textContent).not.toContain('·'); }); });