import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { parseRosterAgents, buildFleetCommsBlock, renderPeerReach, readFleetCommsBlock, type CommsPeer, } from './comms-onboarding.js'; const ROSTER = [ 'version: 1', 'transport: tmux', 'agents:', ' - name: orchestrator', ' runtime: claude', ' class: orchestrator', ' - name: enhancer', ' runtime: claude', ' class: enhancer', ' - name: coder0', ' runtime: pi', ' class: implementer', ' # a manually-listed cross-host peer (pre-federation stopgap)', ' - name: coder0-0', ' runtime: claude', ' class: implementer', ' host: 10.1.10.37', ' ssh: jwoltje@10.1.10.37', '', ].join('\n'); describe('parseRosterAgents', () => { it('parses name + class + optional host/ssh', () => { const peers = parseRosterAgents(ROSTER); expect(peers.map((p) => p.name)).toEqual(['orchestrator', 'enhancer', 'coder0', 'coder0-0']); expect(peers.find((p) => p.name === 'coder0')).toMatchObject({ className: 'implementer' }); expect(peers.find((p) => p.name === 'coder0-0')).toMatchObject({ className: 'implementer', host: '10.1.10.37', ssh: 'jwoltje@10.1.10.37', }); // local agents have no host/ssh expect(peers.find((p) => p.name === 'orchestrator')!.host).toBeUndefined(); }); it('parses an optional per-agent socket', () => { const peers = parseRosterAgents( ['agents:', ' - name: a', ' class: worker', ' socket: mosaic-factory'].join('\n'), ); expect(peers[0]).toMatchObject({ name: 'a', socket: 'mosaic-factory' }); }); it('stops at the next top-level key', () => { const peers = parseRosterAgents( ['agents:', ' - name: a', ' class: worker', 'defaults:', ' working_directory: ~'].join( '\n', ), ); expect(peers.map((p) => p.name)).toEqual(['a']); }); }); describe('renderPeerReach — same-host vs cross-host', () => { const send = '/home/u/.config/mosaic/tools/tmux/agent-send.sh'; it('renders the short form for a same-host peer', () => { const peer: CommsPeer = { name: 'enhancer', className: 'enhancer' }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s enhancer -m "…"`); }); it('renders the -H form for a cross-host peer using ssh', () => { const peer: CommsPeer = { name: 'coder0-0', className: 'implementer', host: '10.1.10.37', ssh: 'jwoltje@10.1.10.37', }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe( `${send} -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`, ); }); it('falls back to host when a cross-host peer has no ssh', () => { const peer: CommsPeer = { name: 'x', className: 'worker', host: '10.0.0.9' }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -H 10.0.0.9 -s x -m "…"`); }); it('treats a peer whose host equals the fleet host as same-host', () => { const peer: CommsPeer = { name: 'y', className: 'worker', host: 'w-jarvis' }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s y -m "…"`); }); it('emits NO -L for an unset/default socket', () => { const peer: CommsPeer = { name: 'lead', className: 'orchestrator' }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s lead -m "…"`); }); it('emits -L for a named socket', () => { const peer: CommsPeer = { name: 'coder0', className: 'implementer', socket: 'mosaic-factory' }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe( `${send} -L mosaic-factory -s coder0 -m "…"`, ); }); it('combines -L (named socket) and -H (cross-host) in order', () => { const peer: CommsPeer = { name: 'coder0-0', className: 'implementer', host: '10.1.10.37', ssh: 'jwoltje@10.1.10.37', socket: 'mosaic-factory', }; expect(renderPeerReach(peer, 'w-jarvis', send)).toBe( `${send} -L mosaic-factory -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`, ); }); }); describe('buildFleetCommsBlock', () => { const send = '/h/.config/mosaic/tools/tmux/agent-send.sh'; const agents = parseRosterAgents(ROSTER); it('excludes self, lists peers, flags the orchestrator, and emits both address forms', () => { const block = buildFleetCommsBlock({ selfName: 'enhancer', agents, fleetHost: 'w-jarvis', agentSendPath: send, }); expect(block).toContain('# Fleet Comms'); expect(block).toContain('You are **enhancer**'); // criterion 1: agent's own [host:session] identity expect(block).toContain('`[w-jarvis:enhancer]`'); // self excluded expect(block).not.toMatch(/\|\s*enhancer\s*\|/); // peers present expect(block).toContain('| orchestrator |'); expect(block).toContain('point of contact'); // same-host peer short form expect(block).toContain(`${send} -s coder0 -m "…"`); // cross-host peer -H form + host annotation expect(block).toContain(`${send} -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`); expect(block).toContain('host `10.1.10.37`'); // conventions expect(block).toContain('FLIP the preamble'); expect(block).toContain('ACCEPTED'); }); it('returns empty when the agent has no peers', () => { expect( buildFleetCommsBlock({ selfName: 'solo', agents: [{ name: 'solo', className: 'orchestrator' }], fleetHost: 'h', agentSendPath: send, }), ).toBe(''); }); }); describe('readFleetCommsBlock — situational (the context a spawned agent gets)', () => { let home: string; beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'mosaic-comms-')); mkdirSync(join(home, 'fleet'), { recursive: true }); writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER); }); afterEach(() => rmSync(home, { recursive: true, force: true })); it('builds the cheat-sheet with correct peer addresses for a fleet member', () => { const block = readFleetCommsBlock(home, 'orchestrator', 'w-jarvis'); expect(block).toContain('# Fleet Comms'); expect(block).toContain('| enhancer |'); expect(block).toContain(`${join(home, 'tools', 'tmux', 'agent-send.sh')} -s coder0 -m "…"`); expect(block).toContain('-H jwoltje@10.1.10.37 -s coder0-0'); expect(block).not.toMatch(/\|\s*orchestrator\s*\|/); // self excluded }); it('returns empty when MOSAIC_AGENT_NAME is unset, no roster, or agent not a member', () => { expect(readFleetCommsBlock(home, undefined, 'w-jarvis')).toBe(''); expect(readFleetCommsBlock(home, 'stranger', 'w-jarvis')).toBe(''); expect(readFleetCommsBlock(mkdtempSync(join(tmpdir(), 'noroster-')), 'orchestrator')).toBe(''); }); });