161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { CommandResult, CommandRunner, FleetRoster } from '../commands/fleet.js';
|
|
import { FleetTmuxRuntimeTransport } from './tmux-runtime-transport.js';
|
|
import type { FleetRuntimeTransportError } from './tmux-runtime-transport.js';
|
|
|
|
const roster: FleetRoster = {
|
|
version: 1,
|
|
transport: 'tmux',
|
|
tmux: { socketName: 'tess-fleet', holderSession: '_holder' },
|
|
defaults: { workingDirectory: '~/src' },
|
|
runtimes: { codex: { resetCommand: '/clear' } },
|
|
agents: [{ name: 'coder0', runtime: 'codex', className: 'code' }],
|
|
};
|
|
|
|
function commandResult(stdout = '', exitCode = 0, stderr = ''): CommandResult {
|
|
return { stdout, stderr, exitCode };
|
|
}
|
|
|
|
describe('FleetTmuxRuntimeTransport security boundary', (): void => {
|
|
it('rejects a prefix target before it invokes tmux', async (): Promise<void> => {
|
|
const runner = vi.fn<CommandRunner>(async (): Promise<CommandResult> => commandResult());
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await expect(transport.verifySession('coder')).rejects.toMatchObject({
|
|
code: 'not_found',
|
|
} satisfies Partial<FleetRuntimeTransportError>);
|
|
expect(runner).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses the roster socket and exact pane target while verifying peer identity', async (): Promise<void> => {
|
|
const runner = vi.fn<CommandRunner>(
|
|
async (): Promise<CommandResult> => commandResult('111 codex 0 0 0 0\n'),
|
|
);
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await expect(transport.verifySession('coder0')).resolves.toEqual({
|
|
id: 'coder0',
|
|
runtimeId: 'codex',
|
|
socketName: 'tess-fleet',
|
|
});
|
|
expect(runner).toHaveBeenCalledWith('tmux', [
|
|
'-L',
|
|
'tess-fleet',
|
|
'list-panes',
|
|
'-t',
|
|
'=coder0:0.0',
|
|
'-F',
|
|
'#{pane_pid} #{pane_current_command} #{pane_dead} #{pane_activity} #{window_activity} #{session_activity}',
|
|
]);
|
|
});
|
|
|
|
it('denies a live pane whose runtime does not match the roster identity', async (): Promise<void> => {
|
|
const runner = vi.fn<CommandRunner>(
|
|
async (): Promise<CommandResult> => commandResult('111 python3 0 0 0 0\n'),
|
|
);
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await expect(transport.verifySession('coder0')).rejects.toMatchObject({
|
|
code: 'forbidden',
|
|
} satisfies Partial<FleetRuntimeTransportError>);
|
|
expect(runner).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('surfaces a runtime identity mismatch instead of hiding it from fleet status', async (): Promise<void> => {
|
|
const rosterWithMismatchedPeer: FleetRoster = {
|
|
...roster,
|
|
agents: [...roster.agents, { name: 'coder1', runtime: 'codex', className: 'code' }],
|
|
};
|
|
const runner = vi.fn<CommandRunner>(
|
|
async (_command: string, args: string[]): Promise<CommandResult> =>
|
|
commandResult(
|
|
args.includes('=coder1:0.0') ? '222 python3 0 0 0 0\n' : '111 codex 0 0 0 0\n',
|
|
),
|
|
);
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => rosterWithMismatchedPeer,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await expect(transport.listSessions()).rejects.toMatchObject({
|
|
code: 'forbidden',
|
|
} satisfies Partial<FleetRuntimeTransportError>);
|
|
});
|
|
|
|
it('denies sending when the roster socket does not contain the exact target', async (): Promise<void> => {
|
|
const runner = vi.fn<CommandRunner>(
|
|
async (): Promise<CommandResult> => commandResult('', 1, "can't find session: coder0"),
|
|
);
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await expect(transport.sendMessage('coder0', 'hello', 'tess')).rejects.toMatchObject({
|
|
code: 'unavailable',
|
|
} satisfies Partial<FleetRuntimeTransportError>);
|
|
expect(runner).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('sends only through the maintained sender after exact target and identity verification', async (): Promise<void> => {
|
|
const runner = vi
|
|
.fn<CommandRunner>()
|
|
.mockResolvedValueOnce(commandResult('111 codex 0 0 0 0\n'))
|
|
.mockResolvedValueOnce(commandResult());
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await transport.sendMessage('coder0', 'hello', 'tess');
|
|
|
|
expect(runner).toHaveBeenNthCalledWith(2, '/mosaic/tools/tmux/agent-send.sh', [
|
|
'-L',
|
|
'tess-fleet',
|
|
'-S',
|
|
'tess',
|
|
'-s',
|
|
'coder0',
|
|
'-m',
|
|
'hello',
|
|
]);
|
|
});
|
|
|
|
it('terminates only the exact roster target after identity verification', async (): Promise<void> => {
|
|
const runner = vi
|
|
.fn<CommandRunner>()
|
|
.mockResolvedValueOnce(commandResult('111 codex 0 0 0 0\n'))
|
|
.mockResolvedValueOnce(commandResult());
|
|
const transport = new FleetTmuxRuntimeTransport({
|
|
rosterLoader: async (): Promise<FleetRoster> => roster,
|
|
runner,
|
|
mosaicHome: '/mosaic',
|
|
});
|
|
|
|
await transport.terminate('coder0');
|
|
|
|
expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [
|
|
'-L',
|
|
'tess-fleet',
|
|
'kill-session',
|
|
'-t',
|
|
'=coder0',
|
|
]);
|
|
});
|
|
});
|