| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- import { beforeEach, describe, expect, mock, test } from 'bun:test';
- import { MultiplexerSessionManager } from './session-manager';
- // Define the mock multiplexer
- const mockMultiplexer = {
- type: 'tmux' as const,
- isAvailable: mock(async () => true),
- isInsideSession: mock(() => true),
- spawnPane: mock(async () => ({
- success: true,
- paneId: '%mock-pane',
- })),
- closePane: mock(async () => true),
- applyLayout: mock(async () => {}),
- };
- // Mock the multiplexer module
- mock.module('../multiplexer', () => ({
- getMultiplexer: () => mockMultiplexer,
- isServerRunning: mock(async () => true),
- startAvailabilityCheck: () => {},
- }));
- // Mock the plugin context
- function createMockContext(overrides?: {
- sessionStatusResult?: { data?: Record<string, { type: string }> };
- directory?: string;
- }) {
- const defaultPort = process.env.OPENCODE_PORT ?? '4096';
- return {
- client: {
- session: {
- status: mock(
- async () => overrides?.sessionStatusResult ?? { data: {} },
- ),
- },
- },
- directory: overrides?.directory ?? '/test/directory',
- serverUrl: new URL(`http://localhost:${defaultPort}`),
- } as any;
- }
- const defaultMultiplexerConfig = {
- type: 'tmux' as const,
- layout: 'main-vertical' as const,
- main_pane_size: 60,
- };
- function createDeferred<T>() {
- let resolve!: (value: T) => void;
- let reject!: (reason?: unknown) => void;
- const promise = new Promise<T>((res, rej) => {
- resolve = res;
- reject = rej;
- });
- return { promise, resolve, reject };
- }
- describe('MultiplexerSessionManager', () => {
- beforeEach(() => {
- mockMultiplexer.spawnPane.mockReset();
- mockMultiplexer.spawnPane.mockResolvedValue({
- success: true,
- paneId: '%mock-pane',
- });
- mockMultiplexer.closePane.mockReset();
- mockMultiplexer.closePane.mockResolvedValue(true);
- mockMultiplexer.isInsideSession.mockReset();
- mockMultiplexer.isInsideSession.mockReturnValue(true);
- });
- describe('constructor', () => {
- test('initializes with config', () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- expect(manager).toBeDefined();
- });
- });
- describe('onSessionCreated', () => {
- test('spawns pane for child sessions', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'child-123',
- parentID: 'parent-456',
- title: 'Test Worker',
- },
- },
- });
- expect(mockMultiplexer.spawnPane).toHaveBeenCalled();
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledWith(
- 'child-123',
- 'Test Worker',
- `http://localhost:${process.env.OPENCODE_PORT ?? '4096'}/`,
- '/test/directory',
- );
- });
- test('ignores sessions without parentID', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'root-session',
- title: 'Main Chat',
- },
- },
- });
- expect(mockMultiplexer.spawnPane).not.toHaveBeenCalled();
- });
- test('prefers child session directory when present', async () => {
- const ctx = createMockContext({ directory: '/parent/directory' });
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'child-456',
- parentID: 'parent-456',
- title: 'Nested Worker',
- directory: '/child/directory',
- },
- },
- });
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledWith(
- 'child-456',
- 'Nested Worker',
- `http://localhost:${process.env.OPENCODE_PORT ?? '4096'}/`,
- '/child/directory',
- );
- });
- test('ignores if disabled in config', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(ctx, {
- ...defaultMultiplexerConfig,
- type: 'none',
- });
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: { id: 'child', parentID: 'parent' },
- },
- });
- expect(mockMultiplexer.spawnPane).not.toHaveBeenCalled();
- });
- test('does not spawn twice for duplicate create events while spawning', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- const deferred = createDeferred<{ success: true; paneId: string }>();
- mockMultiplexer.spawnPane.mockImplementationOnce(() => deferred.promise);
- const event = {
- type: 'session.created',
- properties: {
- info: {
- id: 'child-race',
- parentID: 'parent-race',
- title: 'Race Worker',
- },
- },
- };
- const firstCreate = manager.onSessionCreated(event);
- const secondCreate = manager.onSessionCreated(event);
- await Promise.resolve();
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
- deferred.resolve({ success: true, paneId: 'p-race' });
- await Promise.all([firstCreate, secondCreate]);
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
- });
- });
- describe('polling and closure', () => {
- test('closes pane when session becomes idle', async () => {
- const ctx = createMockContext();
- mockMultiplexer.spawnPane.mockResolvedValue({
- success: true,
- paneId: 'p-1',
- });
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- // Register session
- await manager.onSessionCreated({
- type: 'session.created',
- properties: { info: { id: 'c1', parentID: 'p1' } },
- });
- // Mock status
- ctx.client.session.status.mockResolvedValue({
- data: { c1: { type: 'idle' } },
- });
- await (manager as any).pollSessions();
- expect(mockMultiplexer.closePane).toHaveBeenCalledWith('p-1');
- });
- test('does not close on transient status absence', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionCreated({
- type: 'session.created',
- properties: { info: { id: 'c1', parentID: 'p1' } },
- });
- ctx.client.session.status.mockResolvedValue({ data: {} });
- await (manager as any).pollSessions();
- expect(mockMultiplexer.closePane).not.toHaveBeenCalled();
- });
- test('respawns pane on busy for known prior session', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- mockMultiplexer.spawnPane
- .mockResolvedValueOnce({
- success: true,
- paneId: 'p-1',
- })
- .mockResolvedValueOnce({
- success: true,
- paneId: 'p-2',
- });
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'child-789',
- parentID: 'parent-789',
- title: 'Worker',
- directory: '/task/dir',
- },
- },
- });
- ctx.client.session.status.mockResolvedValue({
- data: { 'child-789': { type: 'idle' } },
- });
- await (manager as any).pollSessions();
- await manager.onSessionStatus({
- type: 'session.status',
- properties: {
- sessionID: 'child-789',
- status: { type: 'busy' },
- },
- });
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(2);
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledWith(
- 'child-789',
- 'Worker',
- `http://localhost:${process.env.OPENCODE_PORT ?? '4096'}/`,
- '/task/dir',
- );
- expect(mockMultiplexer.closePane).toHaveBeenCalledWith('p-1');
- expect(mockMultiplexer.closePane).toHaveBeenCalledTimes(1);
- });
- test('does nothing on busy for unknown session', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionStatus({
- type: 'session.status',
- properties: {
- sessionID: 'unknown-session',
- status: { type: 'busy' },
- },
- });
- expect(mockMultiplexer.spawnPane).not.toHaveBeenCalled();
- });
- test('re-checks tracked sessions after async respawn guard', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- mockMultiplexer.spawnPane
- .mockResolvedValueOnce({ success: true, paneId: 'p-1' })
- .mockResolvedValueOnce({
- success: true,
- paneId: 'p-should-not-happen',
- });
- await manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'child-999',
- parentID: 'parent-999',
- title: 'Worker',
- directory: '/task/dir',
- },
- },
- });
- ctx.client.session.status.mockResolvedValue({
- data: { 'child-999': { type: 'idle' } },
- });
- await (manager as any).pollSessions();
- const respawnPromise = (manager as any).respawnIfKnown('child-999');
- (manager as any).sessions.set('child-999', {
- sessionId: 'child-999',
- paneId: 'p-existing',
- parentId: 'parent-999',
- title: 'Worker',
- directory: '/task/dir',
- createdAt: Date.now(),
- lastSeenAt: Date.now(),
- });
- await respawnPromise;
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
- expect((manager as any).sessions.get('child-999')?.paneId).toBe(
- 'p-existing',
- );
- });
- test('does not respawn while initial pane spawn is still in progress', async () => {
- const ctx = createMockContext();
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- const deferred = createDeferred<{ success: true; paneId: string }>();
- mockMultiplexer.spawnPane.mockImplementationOnce(() => deferred.promise);
- const createPromise = manager.onSessionCreated({
- type: 'session.created',
- properties: {
- info: {
- id: 'child-busy-race',
- parentID: 'parent-busy-race',
- title: 'Busy Worker',
- directory: '/task/dir',
- },
- },
- });
- await manager.onSessionStatus({
- type: 'session.status',
- properties: {
- sessionID: 'child-busy-race',
- status: { type: 'busy' },
- },
- });
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
- deferred.resolve({ success: true, paneId: 'p-busy-race' });
- await createPromise;
- expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
- });
- });
- describe('cleanup', () => {
- test('closes all tracked panes concurrently', async () => {
- const ctx = createMockContext();
- mockMultiplexer.spawnPane
- .mockResolvedValueOnce({ success: true, paneId: 'p1' })
- .mockResolvedValueOnce({ success: true, paneId: 'p2' });
- const manager = new MultiplexerSessionManager(
- ctx,
- defaultMultiplexerConfig,
- );
- await manager.onSessionCreated({
- type: 'session.created',
- properties: { info: { id: 's1', parentID: 'p1' } },
- });
- await manager.onSessionCreated({
- type: 'session.created',
- properties: { info: { id: 's2', parentID: 'p2' } },
- });
- await manager.cleanup();
- expect(mockMultiplexer.closePane).toHaveBeenCalledTimes(2);
- expect(mockMultiplexer.closePane).toHaveBeenCalledWith('p1');
- expect(mockMultiplexer.closePane).toHaveBeenCalledWith('p2');
- });
- });
- });
- // Backward compatibility test
- describe('TmuxSessionManager (backward compatibility)', () => {
- test('TmuxSessionManager is alias for MultiplexerSessionManager', async () => {
- const { TmuxSessionManager } = await import('./session-manager');
- expect(TmuxSessionManager).toBe(MultiplexerSessionManager);
- });
- });
|