import { describe, expect, it, vi } from 'vitest'; import type { CommandResult, CommandRunner, FleetRoster } from '../commands/fleet.js'; import { TmuxPromotionTransport } from './promotion-transport.js'; const sessionId = 'a'.repeat(64); const roster: FleetRoster = { agents: [{ className: 'worker', name: 'claude-seat', runtime: 'claude' }], defaults: { workingDirectory: '~/src' }, runtimes: {}, tmux: { holderSession: '_holder', socketName: 'mosaic-fleet' }, transport: 'tmux', version: 1, }; function result(stdout = '', exitCode = 0, stderr = ''): CommandResult { return { exitCode, stderr, stdout }; } describe('TmuxPromotionTransport', () => { it('resolves the exact roster seat and sends the registered command literally', async () => { const runner = vi .fn() .mockResolvedValueOnce(result('1234 claude 0 0 0 0\n')) .mockResolvedValueOnce(result()) .mockResolvedValueOnce(result()); const environmentReader = vi.fn(async () => `MOSAIC_LEASE_SESSION_ID=${sessionId}\0`); const transport = new TmuxPromotionTransport({ environmentReader, mosaicHome: '/mosaic', rosterLoader: async () => roster, runner, }); const target = await transport.resolve('claude-seat'); await transport.sendPromotion(target); expect(target).toEqual({ bundle: 'mosaic-fleet', seat: 'claude-seat', sessionId, }); expect(environmentReader).toHaveBeenCalledWith(1234); expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [ '-L', 'mosaic-fleet', 'send-keys', '-t', '=claude-seat:0.0', '-l', '/mosaic-promote', ]); expect(runner).toHaveBeenNthCalledWith(3, 'tmux', [ '-L', 'mosaic-fleet', 'send-keys', '-t', '=claude-seat:0.0', 'Enter', ]); }); // Regression for #1124: the launcher runs claude as a spawnSync CHILD of // node(mosaic), so the lease env is on the child, not the tmux pane pid. The // transport must WALK the subtree. This test exercises the real walk (no // full mock of the resolution) — the seam the original unit test hid. it('walks the pane subtree to the claude child that carries the lease id', async () => { const runner = vi.fn().mockResolvedValueOnce(result('1234 node 0 0 0 0\n')); // pane pid 1234 = node(mosaic): NO lease env. child 5678 = claude: carries it. const environmentReader = vi.fn(async (pid: number) => pid === 5678 ? `FOO=bar\0MOSAIC_LEASE_SESSION_ID=${sessionId}\0` : `FOO=bar\0`, ); const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : [])); const transport = new TmuxPromotionTransport({ environmentReader, childrenReader, mosaicHome: '/mosaic', rosterLoader: async () => roster, runner, }); const target = await transport.resolve('claude-seat'); expect(target.sessionId).toBe(sessionId); expect(environmentReader).toHaveBeenCalledWith(1234); // pane pid: no lease expect(environmentReader).toHaveBeenCalledWith(5678); // walked to the child expect(childrenReader).toHaveBeenCalledWith(1234); // walk actually ran }); it('fails closed when no process in the pane subtree carries a lease id', async () => { const runner = vi.fn().mockResolvedValueOnce(result('1234 node 0 0 0 0\n')); const environmentReader = vi.fn(async () => `FOO=bar\0`); const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : [])); const transport = new TmuxPromotionTransport({ environmentReader, childrenReader, mosaicHome: '/mosaic', rosterLoader: async () => roster, runner, }); await expect(transport.resolve('claude-seat')).rejects.toThrow('no readable lease session'); expect(childrenReader).toHaveBeenCalledWith(1234); }); });