mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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.
664 lines
20 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|