Files
Maestro/src/__tests__/main/ipc/handlers/tabNaming.test.ts
Pedram Amini 88e04d2f8e Merge main into 0.15.0-rc: fix context window calculation
Key changes:
- Accept main's fix for context usage calculation (returns null for
  accumulated multi-tool turn values instead of capping at 100%)
- Adopt main's refactored structure:
  - agent-detector.ts → agents/detector.ts + definitions.ts + capabilities.ts
  - stats-db.ts → stats/*.ts modules
  - agent-session-storage types → agents/index.ts
- Port factory-droid agent to new agents/definitions.ts structure
- Remove obsolete shared/contextUsage.ts (logic now in renderer/utils)
- Update all import paths to reference new module locations
- Preserve all RC features: Symphony, File Preview Tabs, TabNaming, etc.

The context window fix is critical: main's approach correctly handles
when Claude Code reports accumulated token values from multi-tool turns
by returning null, causing the UI to preserve the last valid percentage.
RC's approach masked this by capping at 100%, hiding the issue.
2026-02-02 18:03:05 -06:00

664 lines
20 KiB
TypeScript

/**
* Tests for Tab Naming IPC Handlers
*
* Tests the IPC handlers for automatic tab naming:
* - tabNaming:generateTabName
*/
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { ipcMain } from 'electron';
import { registerTabNamingHandlers } from '../../../../main/ipc/handlers/tabNaming';
import type { ProcessManager } from '../../../../main/process-manager';
import type { AgentDetector, AgentConfig } from '../../../../main/agents';
// Mock the logger
vi.mock('../../../../main/utils/logger', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
// Mock electron's ipcMain
vi.mock('electron', () => ({
ipcMain: {
handle: vi.fn(),
},
}));
// Mock uuid
vi.mock('uuid', () => ({
v4: vi.fn(() => 'mock-uuid-1234'),
}));
// Mock the prompts
vi.mock('../../../../prompts', () => ({
tabNamingPrompt: 'You are a tab naming assistant. Generate a concise tab name.',
}));
// Mock the agent args utilities
vi.mock('../../../../main/utils/agent-args', () => ({
buildAgentArgs: vi.fn((agent, options) => options.baseArgs || []),
applyAgentConfigOverrides: vi.fn((agent, args, overrides) => ({
args,
effectiveCustomEnvVars: undefined,
customArgsSource: 'none' as const,
customEnvSource: 'none' as const,
modelSource: 'default' as const,
})),
}));
// Mock SSH utilities
vi.mock('../../../../main/utils/ssh-remote-resolver', () => ({
getSshRemoteConfig: vi.fn(() => ({ config: null, source: 'none' })),
createSshRemoteStoreAdapter: vi.fn(() => ({
getSshRemotes: vi.fn(() => []),
})),
}));
vi.mock('../../../../main/utils/ssh-command-builder', () => ({
buildSshCommand: vi.fn(),
}));
// Capture registered handlers
const registeredHandlers: Map<string, (...args: unknown[]) => Promise<unknown>> = new Map();
describe('Tab Naming IPC Handlers', () => {
let mockProcessManager: {
spawn: Mock;
kill: Mock;
on: Mock;
off: Mock;
};
let mockAgentDetector: {
getAgent: Mock;
};
let mockAgentConfigsStore: {
get: Mock;
set: Mock;
};
let mockSettingsStore: {
get: Mock;
set: Mock;
};
const mockClaudeAgent: AgentConfig = {
id: 'claude-code',
name: 'Claude Code',
command: 'claude',
path: '/usr/local/bin/claude',
args: ['--print'],
batchModeArgs: ['--print'],
readOnlyArgs: ['--read-only'],
};
beforeEach(() => {
vi.clearAllMocks();
registeredHandlers.clear();
// Capture handler registrations
(ipcMain.handle as Mock).mockImplementation(
(channel: string, handler: (...args: unknown[]) => Promise<unknown>) => {
registeredHandlers.set(channel, handler);
}
);
// Create mock process manager
mockProcessManager = {
spawn: vi.fn(),
kill: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};
// Create mock agent detector
mockAgentDetector = {
getAgent: vi.fn().mockResolvedValue(mockClaudeAgent),
};
// Create mock stores
mockAgentConfigsStore = {
get: vi.fn().mockReturnValue({}),
set: vi.fn(),
};
mockSettingsStore = {
get: vi.fn().mockReturnValue({}),
set: vi.fn(),
};
// Register handlers
registerTabNamingHandlers({
getProcessManager: () => mockProcessManager as unknown as ProcessManager,
getAgentDetector: () => mockAgentDetector as unknown as AgentDetector,
agentConfigsStore: mockAgentConfigsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['agentConfigsStore'],
settingsStore: mockSettingsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['settingsStore'],
});
});
// Helper to invoke a registered handler
async function invokeHandler(channel: string, ...args: unknown[]): Promise<unknown> {
const handler = registeredHandlers.get(channel);
if (!handler) {
throw new Error(`No handler registered for channel: ${channel}`);
}
// IPC handlers receive (event, ...args), but our wrapper strips the event
return handler({}, ...args);
}
describe('handler registration', () => {
it('registers the tabNaming:generateTabName handler', () => {
expect(ipcMain.handle).toHaveBeenCalledWith(
'tabNaming:generateTabName',
expect.any(Function)
);
});
});
describe('tabNaming:generateTabName', () => {
it('returns null when agent is not found', async () => {
mockAgentDetector.getAgent.mockResolvedValue(null);
const result = await invokeHandler('tabNaming:generateTabName', {
userMessage: 'Help me implement a login form',
agentType: 'unknown-agent',
cwd: '/test/project',
});
expect(result).toBeNull();
});
it('spawns a process with the correct configuration', async () => {
// Simulate process events
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
// Start the handler but don't await it yet
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Help me implement a login form',
agentType: 'claude-code',
cwd: '/test/project',
});
// Wait for spawn to be called
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Verify spawn was called with correct config
expect(mockProcessManager.spawn).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: expect.stringContaining('tab-naming-'),
toolType: 'claude-code',
cwd: '/test/project',
prompt: expect.stringContaining('Help me implement a login form'),
})
);
// Simulate process output and exit
onDataCallback?.('tab-naming-mock-uuid-1234', 'Login Form Implementation');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Login Form Implementation');
});
it('extracts tab name from agent output with ANSI codes', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Fix the authentication bug',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate output with ANSI escape codes
onDataCallback?.('tab-naming-mock-uuid-1234', '\x1B[32mAuth Bug Fix\x1B[0m');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Auth Bug Fix');
});
it('extracts tab name from output with markdown formatting', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Add a dark mode toggle',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate output with markdown formatting
onDataCallback?.('tab-naming-mock-uuid-1234', '**Dark Mode Toggle**');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Dark Mode Toggle');
});
it('returns null for empty output', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Hello',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate empty output
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBeNull();
});
it('returns null on timeout', async () => {
vi.useFakeTimers();
mockProcessManager.on.mockImplementation(() => {});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Help me with something',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Advance time past the timeout (30 seconds)
vi.advanceTimersByTime(31000);
const result = await resultPromise;
expect(result).toBeNull();
expect(mockProcessManager.kill).toHaveBeenCalledWith('tab-naming-mock-uuid-1234');
vi.useRealTimers();
});
it('cleans up listeners on completion', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test cleanup',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate process completion
onDataCallback?.('tab-naming-mock-uuid-1234', 'Test Tab');
onExitCallback?.('tab-naming-mock-uuid-1234');
await resultPromise;
// Verify listeners were cleaned up
expect(mockProcessManager.off).toHaveBeenCalledWith('data', expect.any(Function));
expect(mockProcessManager.off).toHaveBeenCalledWith('exit', expect.any(Function));
});
it('ignores events from other sessions', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'My specific request',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Send data from a different session (should be ignored)
onDataCallback?.('other-session-id', 'Wrong Tab Name');
// Send data from the correct session
onDataCallback?.('tab-naming-mock-uuid-1234', 'Correct Tab Name');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Correct Tab Name');
});
it('truncates very long tab names', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Something complex',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate a very long tab name (over 50 chars)
const longName = 'This Is A Very Long Tab Name That Should Be Truncated Because It Exceeds The Maximum Length';
onDataCallback?.('tab-naming-mock-uuid-1234', longName);
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).not.toBeNull();
expect((result as string).length).toBeLessThanOrEqual(50);
expect((result as string).endsWith('...')).toBe(true);
});
it('removes quotes from tab names', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Something with quotes',
agentType: 'claude-code',
cwd: '/test/project',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Simulate output with quotes
onDataCallback?.('tab-naming-mock-uuid-1234', '"Quoted Tab Name"');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Quoted Tab Name');
});
it('handles process manager not available', async () => {
// Re-register with null process manager
registeredHandlers.clear();
(ipcMain.handle as Mock).mockImplementation(
(channel: string, handler: (...args: unknown[]) => Promise<unknown>) => {
registeredHandlers.set(channel, handler);
}
);
registerTabNamingHandlers({
getProcessManager: () => null,
getAgentDetector: () => mockAgentDetector as unknown as AgentDetector,
agentConfigsStore: mockAgentConfigsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['agentConfigsStore'],
settingsStore: mockSettingsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['settingsStore'],
});
await expect(
invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test',
agentType: 'claude-code',
cwd: '/test',
})
).rejects.toThrow('Process manager');
});
});
});
describe('extractTabName utility', () => {
// Test the extractTabName function indirectly through the handler
// Since it's not exported, we test its behavior through the IPC handler
describe('edge cases', () => {
let mockProcessManager: {
spawn: Mock;
kill: Mock;
on: Mock;
off: Mock;
};
let mockAgentDetector: {
getAgent: Mock;
};
let mockAgentConfigsStore: {
get: Mock;
set: Mock;
};
let mockSettingsStore: {
get: Mock;
set: Mock;
};
const mockAgent: AgentConfig = {
id: 'claude-code',
name: 'Claude Code',
command: 'claude',
path: '/usr/local/bin/claude',
args: [],
};
beforeEach(() => {
vi.clearAllMocks();
registeredHandlers.clear();
(ipcMain.handle as Mock).mockImplementation(
(channel: string, handler: (...args: unknown[]) => Promise<unknown>) => {
registeredHandlers.set(channel, handler);
}
);
mockProcessManager = {
spawn: vi.fn(),
kill: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};
mockAgentDetector = {
getAgent: vi.fn().mockResolvedValue(mockAgent),
};
mockAgentConfigsStore = {
get: vi.fn().mockReturnValue({}),
set: vi.fn(),
};
mockSettingsStore = {
get: vi.fn().mockReturnValue({}),
set: vi.fn(),
};
registerTabNamingHandlers({
getProcessManager: () => mockProcessManager as unknown as ProcessManager,
getAgentDetector: () => mockAgentDetector as unknown as AgentDetector,
agentConfigsStore: mockAgentConfigsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['agentConfigsStore'],
settingsStore: mockSettingsStore as unknown as Parameters<
typeof registerTabNamingHandlers
>[0]['settingsStore'],
});
});
async function invokeHandler(channel: string, ...args: unknown[]): Promise<unknown> {
const handler = registeredHandlers.get(channel);
if (!handler) {
throw new Error(`No handler registered for channel: ${channel}`);
}
return handler({}, ...args);
}
it('returns null for whitespace-only output', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test',
agentType: 'claude-code',
cwd: '/test',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
onDataCallback?.('tab-naming-mock-uuid-1234', ' \n\t \n ');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBeNull();
});
it('returns null for single character output', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test',
agentType: 'claude-code',
cwd: '/test',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
onDataCallback?.('tab-naming-mock-uuid-1234', 'A');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBeNull();
});
it('handles multiple lines and uses the last meaningful one', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test',
agentType: 'claude-code',
cwd: '/test',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
// Agent might output explanatory text before the actual name
onDataCallback?.('tab-naming-mock-uuid-1234', 'Here is a suggested tab name.\nActual Tab Name');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Actual Tab Name');
});
it('removes backticks from code-formatted output', async () => {
let onDataCallback: ((sessionId: string, data: string) => void) | undefined;
let onExitCallback: ((sessionId: string) => void) | undefined;
mockProcessManager.on.mockImplementation((event: string, callback: (...args: any[]) => void) => {
if (event === 'data') onDataCallback = callback;
if (event === 'exit') onExitCallback = callback;
});
const resultPromise = invokeHandler('tabNaming:generateTabName', {
userMessage: 'Test',
agentType: 'claude-code',
cwd: '/test',
});
await vi.waitFor(() => {
expect(mockProcessManager.spawn).toHaveBeenCalled();
});
onDataCallback?.('tab-naming-mock-uuid-1234', '`Code Style Name`');
onExitCallback?.('tab-naming-mock-uuid-1234');
const result = await resultPromise;
expect(result).toBe('Code Style Name');
});
});
});