Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
deec765
[Symphony] Start contribution for #511
May 22, 2026
d5975c4
MAESTRO: Add CLI server discovery module
May 22, 2026
46603d6
MAESTRO: Auto-start CLI web server discovery
May 22, 2026
d71783c
MAESTRO: Clean up CLI server discovery on shutdown
May 22, 2026
9837da5
MAESTRO: Add CLI server discovery tests
May 22, 2026
aeeabbb
MAESTRO: Add open file tab websocket message
May 22, 2026
d8442b0
MAESTRO: Add refresh file tree websocket IPC
May 22, 2026
ec2b9b8
MAESTRO: Add refresh auto run docs WebSocket message
May 22, 2026
9233732
MAESTRO: Add remote preload listener declarations
May 22, 2026
933baf4
MAESTRO: Wire renderer CLI IPC handlers
May 22, 2026
071441d
MAESTRO: Focus desktop on select session requests
May 22, 2026
db400cb
MAESTRO: Add CLI WebSocket client service
May 22, 2026
d836184
MAESTRO: add CLI open-file command
May 22, 2026
447cf9d
MAESTRO: add refresh-files CLI command
May 22, 2026
15faf59
MAESTRO: add refresh-auto-run CLI command
May 22, 2026
4b546a0
MAESTRO: Add send tab focus flag
May 22, 2026
ad9e6d3
MAESTRO: Add CLI status command
May 22, 2026
32cee44
MAESTRO: Add CLI Maestro client tests
May 22, 2026
cded1ac
MAESTRO: Add configure auto-run IPC plumbing
May 22, 2026
4c16fa2
MAESTRO: Wire remote auto-run renderer handler
May 22, 2026
07f0093
MAESTRO: Add auto-run CLI command
May 22, 2026
0116530
MAESTRO: Document CLI IPC commands in system prompt
May 22, 2026
f844243
MAESTRO: Add auto-run CLI IPC tests
May 22, 2026
d5a098c
Fix CLI IPC review issues
May 22, 2026
cad6fd5
Merge rc into CLI IPC branch
Jun 12, 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
165 changes: 165 additions & 0 deletions src/__tests__/cli/commands/auto-run.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* @file auto-run.test.ts
* @description Tests for the auto-run CLI command
*/

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

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

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

vi.mock('../../../cli/output/formatter', () => ({
formatError: vi.fn((message: string) => `Error: ${message}`),
}));

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

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);
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isFile: () => true } as fs.Stats);
vi.mocked(resolveSessionId).mockReturnValue('target-session');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const client = {
sendCommand: vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
}),
};
return action(client as never);
});
});

it('configures Auto Run with valid document paths', async () => {
await autoRun(['docs/first.md', 'docs/second.md'], { session: 'target-session' });

expect(resolveSessionId).toHaveBeenCalledWith({ session: 'target-session' });
expect(withMaestroClient).toHaveBeenCalledTimes(1);
const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
});
await action({ sendCommand } as never);

expect(sendCommand).toHaveBeenCalledWith(
{
type: 'configure_auto_run',
sessionId: 'target-session',
documents: [
{ filename: 'first.md', resetOnCompletion: false },
{ filename: 'second.md', resetOnCompletion: false },
],
prompt: undefined,
loopEnabled: false,
maxLoops: undefined,
saveAsPlaybook: undefined,
launch: false,
},
'configure_auto_run_result'
);
expect(consoleSpy).toHaveBeenCalledWith('Auto-run configured with 2 documents');
expect(processExitSpy).not.toHaveBeenCalled();
});

it('exits with an error for a non-existent document', async () => {
const missingPath = path.resolve('docs/missing.md');
vi.mocked(fs.existsSync).mockReturnValue(false);

await autoRun(['docs/missing.md'], { session: 'target-session' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error: Failed to configure Auto Run: Document not found: ${missingPath}`
);
expect(withMaestroClient).not.toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('sends saveAsPlaybook when saving as a playbook', async () => {
await autoRun(['docs/play.md'], { session: 'target-session', saveAs: 'Daily Review' });

const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
playbookId: 'playbook-1',
});
await action({ sendCommand } as never);

expect(sendCommand).toHaveBeenCalledWith(
expect.objectContaining({
type: 'configure_auto_run',
saveAsPlaybook: 'Daily Review',
launch: false,
}),
'configure_auto_run_result'
);
expect(consoleSpy).toHaveBeenCalledWith("Playbook 'Daily Review' saved");
});

it('sends launch true when launching Auto Run', async () => {
await autoRun(['docs/launch.md'], { session: 'target-session', launch: true });

const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
});
await action({ sendCommand } as never);

expect(sendCommand).toHaveBeenCalledWith(
expect.objectContaining({
type: 'configure_auto_run',
launch: true,
}),
'configure_auto_run_result'
);
expect(consoleSpy).toHaveBeenCalledWith('Auto-run launched with 1 documents');
});

it('sends loop configuration for --loop and --max-loops', async () => {
await autoRun(['docs/loop.md'], {
session: 'target-session',
loop: true,
maxLoops: '3',
resetOnCompletion: true,
prompt: 'Keep going until clean',
});

const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'configure_auto_run_result',
success: true,
});
await action({ sendCommand } as never);

expect(sendCommand).toHaveBeenCalledWith(
expect.objectContaining({
type: 'configure_auto_run',
documents: [{ filename: 'loop.md', resetOnCompletion: true }],
prompt: 'Keep going until clean',
loopEnabled: true,
maxLoops: 3,
}),
'configure_auto_run_result'
);
});
});
103 changes: 103 additions & 0 deletions src/__tests__/cli/commands/open-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @file open-file.test.ts
* @description Tests for the open-file CLI command
*/

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

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

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

vi.mock('../../../cli/output/formatter', () => ({
formatError: vi.fn((message: string) => `Error: ${message}`),
}));

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

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);
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(resolveSessionId).mockReturnValue('target-session');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const client = {
sendCommand: vi.fn().mockResolvedValue({
type: 'open_file_tab_result',
success: true,
}),
};
return action(client as never);
});
});

it('opens an existing file with an explicit session', async () => {
const filePath = path.resolve('README.md');

await openFile('README.md', { session: 'target-session' });

expect(resolveSessionId).toHaveBeenCalledWith({ session: 'target-session' });
expect(withMaestroClient).toHaveBeenCalledTimes(1);
const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({ type: 'open_file_tab_result', success: true });
await action({ sendCommand } as never);
expect(sendCommand).toHaveBeenCalledWith(
{ type: 'open_file_tab', sessionId: 'target-session', filePath },
'open_file_tab_result'
);
expect(consoleSpy).toHaveBeenCalledWith('Opened README.md in Maestro');
expect(processExitSpy).not.toHaveBeenCalled();
});

it('opens an existing file with the resolved default session', async () => {
vi.mocked(resolveSessionId).mockReturnValue('resolved-session');

await openFile('/tmp/example.txt', {});

const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({ type: 'open_file_tab_result', success: true });
await action({ sendCommand } as never);
expect(sendCommand).toHaveBeenCalledWith(
{ type: 'open_file_tab', sessionId: 'resolved-session', filePath: '/tmp/example.txt' },
'open_file_tab_result'
);
});

it('exits with an error for a missing file', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);

await openFile('/tmp/missing.txt', { session: 'target-session' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error: Failed to open file: File not found: /tmp/missing.txt'
);
expect(withMaestroClient).not.toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('exits with an error when Maestro is not reachable', async () => {
vi.mocked(withMaestroClient).mockRejectedValue(new Error('Maestro desktop app is not running'));

await openFile('/tmp/example.txt', { session: 'target-session' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error: Failed to open file: Maestro desktop app is not running'
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
108 changes: 108 additions & 0 deletions src/__tests__/cli/commands/refresh-auto-run.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @file refresh-auto-run.test.ts
* @description Tests for the refresh-auto-run CLI command
*/

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

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

vi.mock('../../../cli/output/formatter', () => ({
formatError: vi.fn((message: string) => `Error: ${message}`),
}));

import { refreshAutoRun } from '../../../cli/commands/refresh-auto-run';
import { resolveSessionId, withMaestroClient } from '../../../cli/services/maestro-client';

describe('refresh-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);
vi.mocked(resolveSessionId).mockReturnValue('target-session');
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const client = {
sendCommand: vi.fn().mockResolvedValue({
type: 'refresh_auto_run_docs_result',
success: true,
}),
};
return action(client as never);
});
});

it('refreshes Auto Run documents with an explicit session', async () => {
await refreshAutoRun({ session: 'target-session' });

expect(resolveSessionId).toHaveBeenCalledWith({ session: 'target-session' });
expect(withMaestroClient).toHaveBeenCalledTimes(1);
const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'refresh_auto_run_docs_result',
success: true,
});
await action({ sendCommand } as never);
expect(sendCommand).toHaveBeenCalledWith(
{ type: 'refresh_auto_run_docs', sessionId: 'target-session' },
'refresh_auto_run_docs_result'
);
expect(consoleSpy).toHaveBeenCalledWith('Auto Run documents refreshed');
expect(processExitSpy).not.toHaveBeenCalled();
});

it('refreshes Auto Run documents with the resolved default session', async () => {
vi.mocked(resolveSessionId).mockReturnValue('resolved-session');

await refreshAutoRun({});

const action = vi.mocked(withMaestroClient).mock.calls[0][0];
const sendCommand = vi.fn().mockResolvedValue({
type: 'refresh_auto_run_docs_result',
success: true,
});
await action({ sendCommand } as never);
expect(sendCommand).toHaveBeenCalledWith(
{ type: 'refresh_auto_run_docs', sessionId: 'resolved-session' },
'refresh_auto_run_docs_result'
);
});

it('exits with an error when Maestro is not reachable', async () => {
vi.mocked(withMaestroClient).mockRejectedValue(new Error('Maestro desktop app is not running'));

await refreshAutoRun({ session: 'target-session' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error: Failed to refresh Auto Run documents: Maestro desktop app is not running'
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});

it('exits with an error when Maestro rejects the refresh', async () => {
vi.mocked(withMaestroClient).mockImplementation(async (action) => {
const client = {
sendCommand: vi.fn().mockResolvedValue({
type: 'refresh_auto_run_docs_result',
success: false,
error: 'Session not found',
}),
};
return action(client as never);
});

await refreshAutoRun({ session: 'target-session' });

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error: Failed to refresh Auto Run documents: Session not found'
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
Loading