Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3618b78
[Symphony] Start contribution for #511
chr1syy Mar 7, 2026
6f169fc
MAESTRO: Add shared CLI server discovery module
chr1syy Mar 7, 2026
1c01ecd
MAESTRO: Auto-start CLI server on app launch with discovery file
chr1syy Mar 7, 2026
9a8300c
MAESTRO: Delete CLI server discovery file on app shutdown and server …
chr1syy Mar 7, 2026
41478af
MAESTRO: Add tests for CLI server discovery module
chr1syy Mar 7, 2026
d5ef1b0
MAESTRO: Add open_file_tab WebSocket message type
chr1syy Mar 7, 2026
946f5a1
MAESTRO: Add refresh_file_tree WebSocket message type
chr1syy Mar 7, 2026
bdaedc8
MAESTRO: Add refresh_auto_run_docs WebSocket message type
chr1syy Mar 7, 2026
0f64999
MAESTRO: Add preload listeners and type declarations for new remote e…
chr1syy Mar 7, 2026
66b231c
MAESTRO: Wire up renderer handlers for remote open_file_tab, refresh_…
chr1syy Mar 7, 2026
d749f95
MAESTRO: Add focus field to select_session WebSocket message for wind…
chr1syy Mar 7, 2026
9461bdc
MAESTRO: Add tests for new WebSocket message handlers (open_file_tab,…
chr1syy Mar 7, 2026
aa08342
MAESTRO: Add CLI WebSocket client service for Maestro desktop IPC
chr1syy Mar 7, 2026
174172f
MAESTRO: Add open-file CLI command and shared resolveSessionId helper
chr1syy Mar 7, 2026
9bbb17c
MAESTRO: Add refresh-files CLI command for remote file tree refresh
chr1syy Mar 7, 2026
1ceb6eb
MAESTRO: Add refresh-auto-run CLI command for Auto Run document refresh
chr1syy Mar 7, 2026
2a8b8a0
MAESTRO: Add --tab flag to send command for session tab focus
chr1syy Mar 7, 2026
2fa284a
MAESTRO: Add status CLI command for Maestro connectivity diagnostics
chr1syy Mar 7, 2026
90ab987
MAESTRO: Add tests for CLI WebSocket client and open-file command
chr1syy Mar 7, 2026
a24ae97
MAESTRO: Add configure_auto_run WebSocket message type for CLI IPC
chr1syy Mar 7, 2026
4e52b8d
MAESTRO: Wire up renderer handler for remote:configureAutoRun event
chr1syy Mar 7, 2026
d94378e
MAESTRO: Add auto-run CLI command for configuring auto-run sessions
chr1syy Mar 7, 2026
e8e0704
MAESTRO: Add CLI IPC commands documentation to agent system prompt
chr1syy Mar 7, 2026
844d9f9
MAESTRO: Add tests for auto-run CLI command and configure_auto_run We…
chr1syy Mar 7, 2026
de8feeb
MAESTRO: Address PR review feedback for CLI auto-run feature
chr1syy Mar 8, 2026
be4eeeb
MAESTRO: Fix CI failures - add missing test mocks and run prettier
chr1syy Mar 8, 2026
1927cf3
MAESTRO: Fix TS1345 lint error in preload callback error handler
chr1syy Mar 8, 2026
6c1a30f
MAESTRO: Run prettier on preload/process.ts
chr1syy Mar 8, 2026
d23e60e
MAESTRO: Fix PR review findings - robustness and correctness improvem…
chr1syy Mar 8, 2026
56c1c23
MAESTRO: Fix empty documents validation, file tab session targeting, …
chr1syy Mar 8, 2026
6a6a143
MAESTRO: Add unit test for requestId-based WebSocket response resolution
chr1syy Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions src/__tests__/cli/commands/auto-run.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/**
* @file auto-run.test.ts
* @description Tests for the auto-run CLI command
*
* Tests the auto-run command functionality including:
* - Configuring auto-run with valid document paths
* - Error handling for non-existent documents
* - Error handling for non-.md files
* - --save-as flag sends saveAsPlaybook in message
* - --launch flag sends launch: true
* - --loop and --max-loops send loop config
*/

import { describe, it, expect, vi, beforeEach, type MockInstance } from 'vitest';

// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
}));

// Mock maestro-client
vi.mock('../../../cli/services/maestro-client', () => ({
withMaestroClient: vi.fn(),
resolveSessionId: vi.fn(),
}));

import { autoRun } from '../../../cli/commands/auto-run';
import { withMaestroClient, resolveSessionId } from '../../../cli/services/maestro-client';
import { existsSync } from 'fs';

describe('auto-run command', () => {
let consoleSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processExitSpy: MockInstance;

beforeEach(() => {
vi.clearAllMocks();
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
});

it('should configure auto-run with valid document paths', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc1.md', '/path/to/doc2.md'], { session: 'session-123' });

expect(resolveSessionId).toHaveBeenCalledWith({ session: 'session-123' });
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Auto-run configured with 2 documents')
);
expect(processExitSpy).not.toHaveBeenCalled();
});

it('should error with no documents', async () => {
await autoRun([], {});

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('At least one document path is required')
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should error when document does not exist', async () => {
vi.mocked(existsSync).mockReturnValue(false);

await autoRun(['/nonexistent/doc.md'], {});

expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('File not found'));
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should error when document is not a .md file', async () => {
vi.mocked(existsSync).mockReturnValue(true);

await autoRun(['/path/to/file.txt'], {});

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('File must be a .md file')
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should send saveAsPlaybook when --save-as is provided', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');

let sentMessage: Record<string, unknown> | undefined;
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
sentMessage = msg;
return Promise.resolve({
type: 'configure_auto_run_result',
success: true,
playbookId: 'pb-456',
});
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], { saveAs: 'My Playbook', session: 'session-123' });

expect(sentMessage).toBeDefined();
expect(sentMessage!.saveAsPlaybook).toBe('My Playbook');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Playbook 'My Playbook' saved")
);
});

it('should send launch: true when --launch is provided', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');

let sentMessage: Record<string, unknown> | undefined;
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
sentMessage = msg;
return Promise.resolve({
type: 'configure_auto_run_result',
success: true,
});
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], { launch: true, session: 'session-123' });

expect(sentMessage).toBeDefined();
expect(sentMessage!.launch).toBe(true);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Auto-run launched with 1 document')
);
});

it('should send loop config when --loop is provided', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');

let sentMessage: Record<string, unknown> | undefined;
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
sentMessage = msg;
return Promise.resolve({
type: 'configure_auto_run_result',
success: true,
});
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], { loop: true, session: 'session-123' });

expect(sentMessage).toBeDefined();
expect(sentMessage!.loopEnabled).toBe(true);
});

it('should send loop config with --max-loops', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');

let sentMessage: Record<string, unknown> | undefined;
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
sentMessage = msg;
return Promise.resolve({
type: 'configure_auto_run_result',
success: true,
});
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], { maxLoops: '5', session: 'session-123' });

expect(sentMessage).toBeDefined();
expect(sentMessage!.loopEnabled).toBe(true);
expect(sentMessage!.maxLoops).toBe(5);
});

it('should error with invalid --max-loops value', async () => {
vi.mocked(existsSync).mockReturnValue(true);

await autoRun(['/path/to/doc.md'], { maxLoops: 'abc' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--max-loops must be a positive integer')
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should set resetOnCompletion on documents when flag is provided', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');

let sentMessage: Record<string, unknown> | undefined;
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
sentMessage = msg;
return Promise.resolve({
type: 'configure_auto_run_result',
success: true,
});
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], {
resetOnCompletion: true,
session: 'session-123',
});

expect(sentMessage).toBeDefined();
const docs = sentMessage!.documents as Array<{ filename: string; resetOnCompletion: boolean }>;
expect(docs[0].resetOnCompletion).toBe(true);
});

it('should error gracefully when Maestro app is not running', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockRejectedValue(
new Error('Maestro desktop app is not running')
);

await autoRun(['/path/to/doc.md'], { session: 'session-123' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Maestro desktop app is not running')
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should error when server returns failure', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: false,
error: 'Session not found',
}),
};
return action(mockClient as never);
});

await autoRun(['/path/to/doc.md'], { session: 'session-123' });

expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Session not found'));
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
121 changes: 121 additions & 0 deletions src/__tests__/cli/commands/open-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @file open-file.test.ts
* @description Tests for the open-file CLI command
*
* Tests the open-file command functionality including:
* - Opening a valid file with explicit session
* - Opening a valid file with default session resolution
* - Error handling for non-existent files
* - Error handling when Maestro app is not running
*/

import { describe, it, expect, vi, beforeEach, type MockInstance } from 'vitest';
import * as path from 'path';

// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
}));

// Mock maestro-client
vi.mock('../../../cli/services/maestro-client', () => ({
withMaestroClient: vi.fn(),
resolveSessionId: vi.fn(),
}));

import { openFile } from '../../../cli/commands/open-file';
import { withMaestroClient, resolveSessionId } from '../../../cli/services/maestro-client';
import { existsSync } from 'fs';

describe('open-file command', () => {
let consoleSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let processExitSpy: MockInstance;

beforeEach(() => {
vi.clearAllMocks();
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
});

it('should open a valid file with explicit session', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockResolvedValue({ type: 'open_file_tab_result', success: true }),
};
return action(mockClient as never);
});

await openFile('/path/to/file.ts', { session: 'session-123' });

expect(resolveSessionId).toHaveBeenCalledWith({ session: 'session-123' });
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Opened file.ts in Maestro'));
expect(processExitSpy).not.toHaveBeenCalled();
});

it('should resolve relative file paths to absolute', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockImplementation((msg) => {
// Verify absolute path was sent
expect(path.isAbsolute(msg.filePath)).toBe(true);
return Promise.resolve({ type: 'open_file_tab_result', success: true });
}),
};
return action(mockClient as never);
});

await openFile('relative/file.ts', { session: 'session-123' });

expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Opened file.ts in Maestro'));
});

it('should error when file does not exist', async () => {
vi.mocked(existsSync).mockReturnValue(false);

await openFile('/nonexistent/file.ts', {});

expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('File not found'));
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should error gracefully when Maestro app is not running', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockRejectedValue(
new Error('Maestro desktop app is not running')
);

await openFile('/path/to/file.ts', { session: 'session-123' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Maestro desktop app is not running')
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('should error when server returns failure', async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('session-123');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const mockClient = {
sendCommand: vi.fn().mockResolvedValue({
type: 'open_file_tab_result',
success: false,
error: 'Session not found',
}),
};
return action(mockClient as never);
});

await openFile('/path/to/file.ts', { session: 'session-123' });

expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Session not found'));
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
Loading
Loading