mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
refactor: consolidate agents module and reorganize test structure
Create src/main/agents/ directory with barrel exports: - Move agent-detector.ts → agents/detector.ts - Move agent-definitions.ts → agents/definitions.ts - Move agent-capabilities.ts → agents/capabilities.ts - Move agent-session-storage.ts → agents/session-storage.ts - Move path-prober.ts → agents/path-prober.ts - Add index.ts with centralized re-exports Reorganize tests to mirror source structure: - Move agent tests to __tests__/main/agents/ - Move process-listeners inline tests to __tests__/main/process-listeners/ - Move debug-package inline tests to __tests__/main/debug-package/ Add new tests for coverage gaps: - storage/claude-session-storage.test.ts (32 tests) - web-server/managers/CallbackRegistry.test.ts (29 tests) - web-server/managers/LiveSessionManager.test.ts (39 tests) Update all imports across codebase to use new agents/ module path. Test count: 16,201 → 16,301 (+100 tests)
This commit is contained in:
@@ -26,7 +26,7 @@ import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { getAgentCapabilities } from '../../main/agent-capabilities';
|
||||
import { getAgentCapabilities } from '../../main/agents';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '../../main/group-chat/group-chat-moderator';
|
||||
import { addParticipant } from '../../main/group-chat/group-chat-agent';
|
||||
import { routeUserMessage } from '../../main/group-chat/group-chat-router';
|
||||
import { AgentDetector } from '../../main/agent-detector';
|
||||
import { AgentDetector } from '../../main/agents';
|
||||
import {
|
||||
selectTestAgents,
|
||||
waitForAgentResponse,
|
||||
|
||||
@@ -29,7 +29,7 @@ import { exec } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { getAgentCapabilities } from '../../main/agent-capabilities';
|
||||
import { getAgentCapabilities } from '../../main/agents';
|
||||
import { buildSshCommand, buildRemoteCommand } from '../../main/utils/ssh-command-builder';
|
||||
import type { SshRemoteConfig } from '../../shared/types';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AGENT_CAPABILITIES,
|
||||
getAgentCapabilities,
|
||||
hasCapability,
|
||||
} from '../../main/agent-capabilities';
|
||||
} from '../../../main/agents';
|
||||
|
||||
describe('agent-capabilities', () => {
|
||||
describe('AgentCapabilities interface', () => {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getVisibleAgentDefinitions,
|
||||
type AgentDefinition,
|
||||
type AgentConfigOption,
|
||||
} from '../../main/agent-definitions';
|
||||
} from '../../../main/agents';
|
||||
|
||||
describe('agent-definitions', () => {
|
||||
describe('AGENT_DEFINITIONS', () => {
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
AgentConfig,
|
||||
AgentConfigOption,
|
||||
AgentCapabilities,
|
||||
} from '../../main/agent-detector';
|
||||
} from '../../../main/agents';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../main/utils/execFile', () => ({
|
||||
vi.mock('../../../main/utils/execFile', () => ({
|
||||
execFileNoThrow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../main/utils/logger', () => ({
|
||||
vi.mock('../../../main/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -21,8 +21,8 @@ vi.mock('../../main/utils/logger', () => ({
|
||||
}));
|
||||
|
||||
// Get mocked modules
|
||||
import { execFileNoThrow } from '../../main/utils/execFile';
|
||||
import { logger } from '../../main/utils/logger';
|
||||
import { execFileNoThrow } from '../../../main/utils/execFile';
|
||||
import { logger } from '../../../main/utils/logger';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
@@ -8,11 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Mock dependencies before importing the module
|
||||
vi.mock('../../main/utils/execFile', () => ({
|
||||
vi.mock('../../../main/utils/execFile', () => ({
|
||||
execFileNoThrow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../main/utils/logger', () => ({
|
||||
vi.mock('../../../main/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -21,7 +21,7 @@ vi.mock('../../main/utils/logger', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../shared/pathUtils', () => ({
|
||||
vi.mock('../../../shared/pathUtils', () => ({
|
||||
expandTilde: vi.fn((p: string) => p.replace(/^~/, '/Users/testuser')),
|
||||
detectNodeVersionManagerBinPaths: vi.fn(() => []),
|
||||
}));
|
||||
@@ -34,9 +34,9 @@ import {
|
||||
probeWindowsPaths,
|
||||
probeUnixPaths,
|
||||
type BinaryDetectionResult,
|
||||
} from '../../main/path-prober';
|
||||
import { execFileNoThrow } from '../../main/utils/execFile';
|
||||
import { logger } from '../../main/utils/logger';
|
||||
} from '../../../main/agents';
|
||||
import { execFileNoThrow } from '../../../main/utils/execFile';
|
||||
import { logger } from '../../../main/utils/logger';
|
||||
|
||||
describe('path-prober', () => {
|
||||
beforeEach(() => {
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type Store from 'electron-store';
|
||||
import type { ClaudeSessionOriginsData } from '../../../main/storage/claude-session-storage';
|
||||
import {
|
||||
AgentSessionStorage,
|
||||
AgentSessionInfo,
|
||||
@@ -11,8 +13,8 @@ import {
|
||||
hasSessionStorage,
|
||||
getAllSessionStorages,
|
||||
clearStorageRegistry,
|
||||
} from '../../main/agent-session-storage';
|
||||
import type { ToolType } from '../../shared/types';
|
||||
} from '../../../main/agents';
|
||||
import type { ToolType } from '../../../shared/types';
|
||||
|
||||
// Mock storage implementation for testing
|
||||
class MockSessionStorage implements AgentSessionStorage {
|
||||
@@ -198,12 +200,12 @@ describe('ClaudeSessionStorage', () => {
|
||||
// For now, we test that the class can be imported
|
||||
it('should be importable', async () => {
|
||||
// Dynamic import to test module loading
|
||||
const { ClaudeSessionStorage } = await import('../../main/storage/claude-session-storage');
|
||||
const { ClaudeSessionStorage } = await import('../../../main/storage/claude-session-storage');
|
||||
expect(ClaudeSessionStorage).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have claude-code as agentId', async () => {
|
||||
const { ClaudeSessionStorage } = await import('../../main/storage/claude-session-storage');
|
||||
const { ClaudeSessionStorage } = await import('../../../main/storage/claude-session-storage');
|
||||
|
||||
// Create instance without store (it will create its own)
|
||||
// Note: In a real test, we'd mock electron-store
|
||||
@@ -214,18 +216,21 @@ describe('ClaudeSessionStorage', () => {
|
||||
|
||||
describe('OpenCodeSessionStorage', () => {
|
||||
it('should be importable', async () => {
|
||||
const { OpenCodeSessionStorage } = await import('../../main/storage/opencode-session-storage');
|
||||
const { OpenCodeSessionStorage } =
|
||||
await import('../../../main/storage/opencode-session-storage');
|
||||
expect(OpenCodeSessionStorage).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have opencode as agentId', async () => {
|
||||
const { OpenCodeSessionStorage } = await import('../../main/storage/opencode-session-storage');
|
||||
const { OpenCodeSessionStorage } =
|
||||
await import('../../../main/storage/opencode-session-storage');
|
||||
const storage = new OpenCodeSessionStorage();
|
||||
expect(storage.agentId).toBe('opencode');
|
||||
});
|
||||
|
||||
it('should return empty results for non-existent projects', async () => {
|
||||
const { OpenCodeSessionStorage } = await import('../../main/storage/opencode-session-storage');
|
||||
const { OpenCodeSessionStorage } =
|
||||
await import('../../../main/storage/opencode-session-storage');
|
||||
const storage = new OpenCodeSessionStorage();
|
||||
|
||||
// Non-existent project should return empty results
|
||||
@@ -245,7 +250,8 @@ describe('OpenCodeSessionStorage', () => {
|
||||
});
|
||||
|
||||
it('should return message directory path for getSessionPath', async () => {
|
||||
const { OpenCodeSessionStorage } = await import('../../main/storage/opencode-session-storage');
|
||||
const { OpenCodeSessionStorage } =
|
||||
await import('../../../main/storage/opencode-session-storage');
|
||||
const storage = new OpenCodeSessionStorage();
|
||||
|
||||
// getSessionPath returns the message directory for the session
|
||||
@@ -257,7 +263,8 @@ describe('OpenCodeSessionStorage', () => {
|
||||
});
|
||||
|
||||
it('should fail gracefully when deleting from non-existent session', async () => {
|
||||
const { OpenCodeSessionStorage } = await import('../../main/storage/opencode-session-storage');
|
||||
const { OpenCodeSessionStorage } =
|
||||
await import('../../../main/storage/opencode-session-storage');
|
||||
const storage = new OpenCodeSessionStorage();
|
||||
|
||||
const deleteResult = await storage.deleteMessagePair(
|
||||
@@ -272,18 +279,18 @@ describe('OpenCodeSessionStorage', () => {
|
||||
|
||||
describe('CodexSessionStorage', () => {
|
||||
it('should be importable', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
expect(CodexSessionStorage).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have codex as agentId', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
const storage = new CodexSessionStorage();
|
||||
expect(storage.agentId).toBe('codex');
|
||||
});
|
||||
|
||||
it('should return empty results for non-existent sessions directory', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
const storage = new CodexSessionStorage();
|
||||
|
||||
// Non-existent project should return empty results (since ~/.codex/sessions/ likely doesn't exist in test)
|
||||
@@ -306,7 +313,7 @@ describe('CodexSessionStorage', () => {
|
||||
});
|
||||
|
||||
it('should return null for getSessionPath (async operation required)', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
const storage = new CodexSessionStorage();
|
||||
|
||||
// getSessionPath is synchronous and always returns null for Codex
|
||||
@@ -316,7 +323,7 @@ describe('CodexSessionStorage', () => {
|
||||
});
|
||||
|
||||
it('should fail gracefully when deleting from non-existent session', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
const storage = new CodexSessionStorage();
|
||||
|
||||
const deleteResult = await storage.deleteMessagePair(
|
||||
@@ -329,7 +336,7 @@ describe('CodexSessionStorage', () => {
|
||||
});
|
||||
|
||||
it('should handle empty search query', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/codex-session-storage');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/codex-session-storage');
|
||||
const storage = new CodexSessionStorage();
|
||||
|
||||
const search = await storage.searchSessions('/test/project', '', 'all');
|
||||
@@ -342,12 +349,12 @@ describe('CodexSessionStorage', () => {
|
||||
|
||||
describe('Storage Module Initialization', () => {
|
||||
it('should export initializeSessionStorages function', async () => {
|
||||
const { initializeSessionStorages } = await import('../../main/storage/index');
|
||||
const { initializeSessionStorages } = await import('../../../main/storage/index');
|
||||
expect(typeof initializeSessionStorages).toBe('function');
|
||||
});
|
||||
|
||||
it('should export CodexSessionStorage', async () => {
|
||||
const { CodexSessionStorage } = await import('../../main/storage/index');
|
||||
const { CodexSessionStorage } = await import('../../../main/storage/index');
|
||||
expect(CodexSessionStorage).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -355,7 +362,7 @@ describe('Storage Module Initialization', () => {
|
||||
// This tests that ClaudeSessionStorage can receive an external store
|
||||
// This prevents the dual-store bug where IPC handlers and storage class
|
||||
// use different electron-store instances
|
||||
const { ClaudeSessionStorage } = await import('../../main/storage/claude-session-storage');
|
||||
const { ClaudeSessionStorage } = await import('../../../main/storage/claude-session-storage');
|
||||
|
||||
// Create a mock store
|
||||
const mockStore = {
|
||||
@@ -366,14 +373,14 @@ describe('Storage Module Initialization', () => {
|
||||
|
||||
// Should be able to create with external store (no throw)
|
||||
const storage = new ClaudeSessionStorage(
|
||||
mockStore as unknown as import('electron-store').default
|
||||
mockStore as unknown as Store<ClaudeSessionOriginsData>
|
||||
);
|
||||
expect(storage.agentId).toBe('claude-code');
|
||||
});
|
||||
|
||||
it('should export InitializeSessionStoragesOptions interface', async () => {
|
||||
// This tests that the options interface is exported for type-safe initialization
|
||||
const storageModule = await import('../../main/storage/index');
|
||||
const storageModule = await import('../../../main/storage/index');
|
||||
// The function should accept options object
|
||||
expect(typeof storageModule.initializeSessionStorages).toBe('function');
|
||||
// Function should accept undefined options (backward compatible)
|
||||
@@ -383,9 +390,8 @@ describe('Storage Module Initialization', () => {
|
||||
it('should accept claudeSessionOriginsStore in options', async () => {
|
||||
// This tests the fix for the dual-store bug
|
||||
// When a shared store is passed, it should be used instead of creating a new one
|
||||
const { initializeSessionStorages } = await import('../../main/storage/index');
|
||||
const { getSessionStorage, clearStorageRegistry } =
|
||||
await import('../../main/agent-session-storage');
|
||||
const { initializeSessionStorages } = await import('../../../main/storage/index');
|
||||
const { getSessionStorage, clearStorageRegistry } = await import('../../../main/agents');
|
||||
|
||||
// Clear registry first
|
||||
clearStorageRegistry();
|
||||
@@ -402,7 +408,7 @@ describe('Storage Module Initialization', () => {
|
||||
// Initialize with the shared store
|
||||
// This mimics what main/index.ts does
|
||||
initializeSessionStorages({
|
||||
claudeSessionOriginsStore: mockStore as unknown as import('electron-store').default,
|
||||
claudeSessionOriginsStore: mockStore as unknown as Store<ClaudeSessionOriginsData>,
|
||||
});
|
||||
|
||||
// Verify ClaudeSessionStorage was registered
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as path from 'path';
|
||||
import { createZipPackage, PackageContents } from '../packager';
|
||||
import { createZipPackage, PackageContents } from '../../../main/debug-package/packager';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
// Use the native node:fs module to avoid any vitest mocks
|
||||
@@ -51,7 +51,7 @@ describe('Debug Package Sanitization', () => {
|
||||
describe('sanitizePath', () => {
|
||||
describe('home directory replacement', () => {
|
||||
it('should replace home directory with ~', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const testPath = `${homeDir}/Projects/MyApp`;
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should replace home directory at any position in path', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const testPath = `${homeDir}/deeply/nested/folder/file.txt`;
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle home directory with trailing slash', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const testPath = `${homeDir}/`;
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle path that is exactly the home directory', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const result = sanitizePath(homeDir);
|
||||
@@ -91,7 +91,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should not modify paths that do not contain home directory', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const testPath = '/usr/local/bin/app';
|
||||
|
||||
const result = sanitizePath(testPath);
|
||||
@@ -100,7 +100,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle empty string', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
const result = sanitizePath('');
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('Windows path handling', () => {
|
||||
it('should normalize backslashes to forward slashes', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const testPath = 'C:\\Users\\testuser\\Documents\\Project';
|
||||
|
||||
const result = sanitizePath(testPath);
|
||||
@@ -120,7 +120,8 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle Windows-style home directory', async () => {
|
||||
const { sanitizePath: _sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath: _sanitizePath } =
|
||||
await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
// Mock homedir to return Windows-style path
|
||||
const originalHomedir = os.homedir();
|
||||
@@ -128,7 +129,8 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
// Re-import to get fresh module with mocked homedir
|
||||
vi.resetModules();
|
||||
const { sanitizePath: freshSanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath: freshSanitizePath } =
|
||||
await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
const testPath = 'C:\\Users\\testuser\\Documents\\Project';
|
||||
const result = freshSanitizePath(testPath);
|
||||
@@ -139,7 +141,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle mixed slash styles', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const testPath = '/path/to\\mixed\\slashes/file.txt';
|
||||
|
||||
const result = sanitizePath(testPath);
|
||||
@@ -152,7 +154,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('edge cases and type handling', () => {
|
||||
it('should return null when given null', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
// @ts-expect-error - Testing runtime behavior with wrong type
|
||||
const result = sanitizePath(null);
|
||||
@@ -161,7 +163,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should return undefined when given undefined', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
// @ts-expect-error - Testing runtime behavior with wrong type
|
||||
const result = sanitizePath(undefined);
|
||||
@@ -170,7 +172,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should return numbers unchanged', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
// @ts-expect-error - Testing runtime behavior with wrong type
|
||||
const result = sanitizePath(12345);
|
||||
@@ -179,7 +181,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should return objects unchanged', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const obj = { path: '/some/path' };
|
||||
|
||||
// @ts-expect-error - Testing runtime behavior with wrong type
|
||||
@@ -189,7 +191,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle paths with spaces', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const testPath = `${homeDir}/My Documents/Project Files/app.tsx`;
|
||||
|
||||
@@ -199,7 +201,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle paths with special characters', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const testPath = `${homeDir}/Projects/@company/app-v2.0#beta`;
|
||||
|
||||
@@ -209,7 +211,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle very long paths', async () => {
|
||||
const { sanitizePath } = await import('../collectors/settings');
|
||||
const { sanitizePath } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
const longPath = `${homeDir}/` + 'a/'.repeat(100) + 'file.txt';
|
||||
|
||||
@@ -228,7 +230,7 @@ describe('Debug Package Sanitization', () => {
|
||||
describe('API key redaction', () => {
|
||||
describe('sensitive key detection', () => {
|
||||
it('should redact apiKey', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -242,7 +244,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact api_key (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -255,7 +257,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact authToken', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -268,7 +270,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact auth_token (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -281,7 +283,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact clientToken', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -294,7 +296,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact client_token (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -307,7 +309,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact password', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -320,7 +322,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact secret', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -333,7 +335,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact credential', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -346,7 +348,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact accessToken', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -359,7 +361,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact access_token (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -372,7 +374,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact refreshToken', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -385,7 +387,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact refresh_token (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -398,7 +400,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact privateKey', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -411,7 +413,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact private_key (snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -426,7 +428,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('case insensitivity', () => {
|
||||
it('should redact APIKEY (uppercase)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -439,7 +441,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact ApiKey (mixed case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -452,7 +454,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact API_KEY (uppercase snake_case)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -465,7 +467,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact PASSWORD (uppercase)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -478,7 +480,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact Secret (capitalized)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -493,7 +495,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('key name patterns containing sensitive words', () => {
|
||||
it('should redact myApiKeyValue (key within name)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -506,7 +508,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact userPassword (password in name)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -519,7 +521,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact adminSecret (secret in name)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -532,7 +534,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact bearerAccessToken (accesstoken in name)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -545,7 +547,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact dbCredential (credential in name)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -560,7 +562,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('nested object handling', () => {
|
||||
it('should redact sensitive keys in nested objects', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -577,7 +579,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact deeply nested sensitive keys', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -602,7 +604,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should track sanitized fields with full path', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -621,7 +623,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should redact multiple sensitive keys at different levels', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -646,7 +648,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('array handling', () => {
|
||||
it('should process arrays containing objects with sensitive keys', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -667,7 +669,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle empty arrays', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -680,7 +682,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should handle arrays of primitives', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -695,7 +697,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('preservation of non-sensitive data', () => {
|
||||
it('should preserve boolean values', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -709,7 +711,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should preserve number values', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -724,7 +726,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should preserve string values without sensitive keywords', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -739,7 +741,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should preserve null values', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const mockStore = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
@@ -760,7 +762,7 @@ describe('Debug Package Sanitization', () => {
|
||||
describe('environment variable filtering', () => {
|
||||
describe('custom env vars masking', () => {
|
||||
it('should not expose custom env var values in agents collector', async () => {
|
||||
const { collectAgents } = await import('../collectors/agents');
|
||||
const { collectAgents } = await import('../../../main/debug-package/collectors/agents');
|
||||
|
||||
const mockAgentDetector = {
|
||||
detectAgents: vi.fn().mockResolvedValue([
|
||||
@@ -786,7 +788,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should indicate env vars are set without showing values', async () => {
|
||||
const { collectAgents } = await import('../collectors/agents');
|
||||
const { collectAgents } = await import('../../../main/debug-package/collectors/agents');
|
||||
|
||||
const mockAgentDetector = {
|
||||
detectAgents: vi.fn().mockResolvedValue([
|
||||
@@ -812,7 +814,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('custom args masking', () => {
|
||||
it('should not expose custom args values containing secrets', async () => {
|
||||
const { collectAgents } = await import('../collectors/agents');
|
||||
const { collectAgents } = await import('../../../main/debug-package/collectors/agents');
|
||||
|
||||
const mockAgentDetector = {
|
||||
detectAgents: vi.fn().mockResolvedValue([
|
||||
@@ -836,7 +838,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('path-based environment variables', () => {
|
||||
it('should sanitize custom path settings', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const mockStore = {
|
||||
@@ -855,7 +857,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should sanitize folderPath settings', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const mockStore = {
|
||||
@@ -879,7 +881,7 @@ describe('Debug Package Sanitization', () => {
|
||||
|
||||
describe('comprehensive sanitization', () => {
|
||||
it('should sanitize complex settings object with mixed sensitive data', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const mockStore = {
|
||||
@@ -931,7 +933,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should track all sanitized fields', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const mockStore = {
|
||||
@@ -952,7 +954,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should produce output that contains no home directory paths for recognized path keys', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const mockStore = {
|
||||
@@ -980,7 +982,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should not sanitize paths in array values (by design)', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
const homeDir = os.homedir();
|
||||
|
||||
// Note: Arrays of string paths are NOT sanitized by design
|
||||
@@ -1002,7 +1004,7 @@ describe('Debug Package Sanitization', () => {
|
||||
});
|
||||
|
||||
it('should produce output that contains no API keys or secrets', async () => {
|
||||
const { collectSettings } = await import('../collectors/settings');
|
||||
const { collectSettings } = await import('../../../main/debug-package/collectors/settings');
|
||||
|
||||
const secrets = [
|
||||
'sk-1234567890abcdef',
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
GroupChatParticipant,
|
||||
} from '../../../main/group-chat/group-chat-storage';
|
||||
import { readLog } from '../../../main/group-chat/group-chat-log';
|
||||
import { AgentDetector } from '../../../main/agent-detector';
|
||||
import { AgentDetector } from '../../../main/agents';
|
||||
|
||||
describe('group-chat-router', () => {
|
||||
let mockProcessManager: IProcessManager;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ipcMain } from 'electron';
|
||||
import { registerAgentSessionsHandlers } from '../../../../main/ipc/handlers/agentSessions';
|
||||
import * as agentSessionStorage from '../../../../main/agent-session-storage';
|
||||
import * as agentSessionStorage from '../../../../main/agents';
|
||||
|
||||
// Mock electron's ipcMain
|
||||
vi.mock('electron', () => ({
|
||||
@@ -18,8 +18,8 @@ vi.mock('electron', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the agent-session-storage module
|
||||
vi.mock('../../../../main/agent-session-storage', () => ({
|
||||
// Mock the agents module (session storage exports)
|
||||
vi.mock('../../../../main/agents', () => ({
|
||||
getSessionStorage: vi.fn(),
|
||||
hasSessionStorage: vi.fn(),
|
||||
getAllSessionStorages: vi.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
registerAgentsHandlers,
|
||||
AgentsHandlerDependencies,
|
||||
} from '../../../../main/ipc/handlers/agents';
|
||||
import * as agentCapabilities from '../../../../main/agent-capabilities';
|
||||
import * as agentCapabilities from '../../../../main/agents';
|
||||
|
||||
// Mock electron's ipcMain
|
||||
vi.mock('electron', () => ({
|
||||
@@ -20,8 +20,8 @@ vi.mock('electron', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock agent-capabilities module
|
||||
vi.mock('../../../../main/agent-capabilities', () => ({
|
||||
// Mock agents module (capabilities exports)
|
||||
vi.mock('../../../../main/agents', () => ({
|
||||
getAgentCapabilities: vi.fn(),
|
||||
DEFAULT_CAPABILITIES: {
|
||||
supportsResume: false,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DebugHandlerDependencies,
|
||||
} from '../../../../main/ipc/handlers/debug';
|
||||
import * as debugPackage from '../../../../main/debug-package';
|
||||
import { AgentDetector } from '../../../../main/agent-detector';
|
||||
import { AgentDetector } from '../../../../main/agents';
|
||||
import { ProcessManager } from '../../../../main/process-manager';
|
||||
import { WebServer } from '../../../../main/web-server';
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupDataListener } from '../data-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { SafeSendFn } from '../../utils/safe-send';
|
||||
import type { ProcessListenerDependencies } from '../types';
|
||||
import { setupDataListener } from '../../../main/process-listeners/data-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { SafeSendFn } from '../../../main/utils/safe-send';
|
||||
import type { ProcessListenerDependencies } from '../../../main/process-listeners/types';
|
||||
|
||||
describe('Data Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupErrorListener } from '../error-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { SafeSendFn } from '../../utils/safe-send';
|
||||
import { setupErrorListener } from '../../../main/process-listeners/error-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { SafeSendFn } from '../../../main/utils/safe-send';
|
||||
import type { AgentError } from '../../../shared/types';
|
||||
import type { ProcessListenerDependencies } from '../types';
|
||||
import type { ProcessListenerDependencies } from '../../../main/process-listeners/types';
|
||||
|
||||
describe('Error Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupExitListener } from '../exit-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { ProcessListenerDependencies } from '../types';
|
||||
import { setupExitListener } from '../../../main/process-listeners/exit-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { ProcessListenerDependencies } from '../../../main/process-listeners/types';
|
||||
|
||||
describe('Exit Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupForwardingListeners } from '../forwarding-listeners';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { SafeSendFn } from '../../utils/safe-send';
|
||||
import { setupForwardingListeners } from '../../../main/process-listeners/forwarding-listeners';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { SafeSendFn } from '../../../main/utils/safe-send';
|
||||
|
||||
describe('Forwarding Listeners', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupSessionIdListener } from '../session-id-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import { setupSessionIdListener } from '../../../main/process-listeners/session-id-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
|
||||
describe('Session ID Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupStatsListener } from '../stats-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { SafeSendFn } from '../../utils/safe-send';
|
||||
import type { QueryCompleteData } from '../../process-manager/types';
|
||||
import type { StatsDB } from '../../stats-db';
|
||||
import type { ProcessListenerDependencies } from '../types';
|
||||
import { setupStatsListener } from '../../../main/process-listeners/stats-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { SafeSendFn } from '../../../main/utils/safe-send';
|
||||
import type { QueryCompleteData } from '../../../main/process-manager/types';
|
||||
import type { StatsDB } from '../../../main/stats-db';
|
||||
import type { ProcessListenerDependencies } from '../../../main/process-listeners/types';
|
||||
|
||||
describe('Stats Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setupUsageListener } from '../usage-listener';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { UsageStats } from '../types';
|
||||
import { setupUsageListener } from '../../../main/process-listeners/usage-listener';
|
||||
import type { ProcessManager } from '../../../main/process-manager';
|
||||
import type { UsageStats } from '../../../main/process-listeners/types';
|
||||
|
||||
describe('Usage Listener', () => {
|
||||
let mockProcessManager: ProcessManager;
|
||||
416
src/__tests__/main/storage/claude-session-storage.test.ts
Normal file
416
src/__tests__/main/storage/claude-session-storage.test.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Tests for ClaudeSessionStorage
|
||||
*
|
||||
* Verifies:
|
||||
* - Session origin registration and retrieval
|
||||
* - Session naming and starring
|
||||
* - Context usage tracking
|
||||
* - Origin info attachment to sessions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ClaudeSessionStorage } from '../../../main/storage/claude-session-storage';
|
||||
import type { SshRemoteConfig } from '../../../shared/types';
|
||||
import type Store from 'electron-store';
|
||||
import type { ClaudeSessionOriginsData } from '../../../main/storage/claude-session-storage';
|
||||
|
||||
// Mock electron-store
|
||||
const mockStoreData: Record<string, unknown> = {};
|
||||
vi.mock('electron-store', () => {
|
||||
return {
|
||||
default: vi.fn().mockImplementation(() => ({
|
||||
get: vi.fn((key: string, defaultValue?: unknown) => {
|
||||
return mockStoreData[key] ?? defaultValue;
|
||||
}),
|
||||
set: vi.fn((key: string, value: unknown) => {
|
||||
mockStoreData[key] = value;
|
||||
}),
|
||||
store: mockStoreData,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock logger
|
||||
vi.mock('../../../main/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('fs/promises', () => ({
|
||||
default: {
|
||||
access: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock remote-fs utilities
|
||||
vi.mock('../../../main/utils/remote-fs', () => ({
|
||||
readDirRemote: vi.fn(),
|
||||
readFileRemote: vi.fn(),
|
||||
statRemote: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock statsCache
|
||||
vi.mock('../../../main/utils/statsCache', () => ({
|
||||
encodeClaudeProjectPath: vi.fn((projectPath: string) => {
|
||||
// Simple encoding for tests - replace / with -
|
||||
return projectPath.replace(/\//g, '-').replace(/^-/, '');
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock pricing
|
||||
vi.mock('../../../main/utils/pricing', () => ({
|
||||
calculateClaudeCost: vi.fn(() => 0.05),
|
||||
}));
|
||||
|
||||
describe('ClaudeSessionStorage', () => {
|
||||
let storage: ClaudeSessionStorage;
|
||||
let mockStore: {
|
||||
get: ReturnType<typeof vi.fn>;
|
||||
set: ReturnType<typeof vi.fn>;
|
||||
store: Record<string, unknown>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset mock store data
|
||||
Object.keys(mockStoreData).forEach((key) => delete mockStoreData[key]);
|
||||
mockStoreData['origins'] = {};
|
||||
|
||||
mockStore = {
|
||||
get: vi.fn((key: string, defaultValue?: unknown) => {
|
||||
return mockStoreData[key] ?? defaultValue;
|
||||
}),
|
||||
set: vi.fn((key: string, value: unknown) => {
|
||||
mockStoreData[key] = value;
|
||||
}),
|
||||
store: mockStoreData,
|
||||
};
|
||||
|
||||
// Create storage with mock store
|
||||
storage = new ClaudeSessionStorage(mockStore as unknown as Store<ClaudeSessionOriginsData>);
|
||||
});
|
||||
|
||||
describe('Origin Management', () => {
|
||||
describe('registerSessionOrigin', () => {
|
||||
it('should register a user session origin', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123']).toEqual({ origin: 'user' });
|
||||
});
|
||||
|
||||
it('should register an auto session origin', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-456', 'auto');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-456']).toEqual({ origin: 'auto' });
|
||||
});
|
||||
|
||||
it('should register origin with session name', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-789', 'user', 'My Session');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-789']).toEqual({
|
||||
origin: 'user',
|
||||
sessionName: 'My Session',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple sessions for same project', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-1', 'user');
|
||||
storage.registerSessionOrigin('/project/path', 'session-2', 'auto');
|
||||
storage.registerSessionOrigin('/project/path', 'session-3', 'user', 'Named');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(Object.keys(origins)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle multiple projects', () => {
|
||||
storage.registerSessionOrigin('/project/a', 'session-a', 'user');
|
||||
storage.registerSessionOrigin('/project/b', 'session-b', 'auto');
|
||||
|
||||
expect(storage.getSessionOrigins('/project/a')['session-a']).toBeDefined();
|
||||
expect(storage.getSessionOrigins('/project/b')['session-b']).toBeDefined();
|
||||
expect(storage.getSessionOrigins('/project/a')['session-b']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should persist to store', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
|
||||
expect(mockStore.set).toHaveBeenCalledWith(
|
||||
'origins',
|
||||
expect.objectContaining({
|
||||
'/project/path': expect.objectContaining({
|
||||
'session-123': 'user',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSessionName', () => {
|
||||
it('should update name for existing session with string origin', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionName('/project/path', 'session-123', 'New Name');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123']).toEqual({
|
||||
origin: 'user',
|
||||
sessionName: 'New Name',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update name for existing session with object origin', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user', 'Old Name');
|
||||
storage.updateSessionName('/project/path', 'session-123', 'New Name');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].sessionName).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should create origin entry if session not registered', () => {
|
||||
storage.updateSessionName('/project/path', 'new-session', 'Session Name');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['new-session']).toEqual({
|
||||
origin: 'user',
|
||||
sessionName: 'Session Name',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing starred status', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
storage.updateSessionName('/project/path', 'session-123', 'Named');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].starred).toBe(true);
|
||||
expect(origins['session-123'].sessionName).toBe('Named');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSessionStarred', () => {
|
||||
it('should star a session', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].starred).toBe(true);
|
||||
});
|
||||
|
||||
it('should unstar a session', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
storage.updateSessionStarred('/project/path', 'session-123', false);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].starred).toBe(false);
|
||||
});
|
||||
|
||||
it('should create origin entry if session not registered', () => {
|
||||
storage.updateSessionStarred('/project/path', 'new-session', true);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['new-session']).toEqual({
|
||||
origin: 'user',
|
||||
starred: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing session name', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user', 'My Session');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].sessionName).toBe('My Session');
|
||||
expect(origins['session-123'].starred).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSessionContextUsage', () => {
|
||||
it('should store context usage percentage', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 75);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].contextUsage).toBe(75);
|
||||
});
|
||||
|
||||
it('should create origin entry if session not registered', () => {
|
||||
storage.updateSessionContextUsage('/project/path', 'new-session', 50);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['new-session']).toEqual({
|
||||
origin: 'user',
|
||||
contextUsage: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing origin data', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'auto', 'Named');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 80);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123']).toEqual({
|
||||
origin: 'auto',
|
||||
sessionName: 'Named',
|
||||
starred: true,
|
||||
contextUsage: 80,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update context usage on subsequent calls', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user');
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 25);
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 50);
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 75);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123'].contextUsage).toBe(75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSessionOrigins', () => {
|
||||
it('should return empty object for project with no sessions', () => {
|
||||
const origins = storage.getSessionOrigins('/nonexistent/project');
|
||||
expect(origins).toEqual({});
|
||||
});
|
||||
|
||||
it('should normalize string origins to SessionOriginInfo format', () => {
|
||||
// Simulate legacy string-only origin stored directly
|
||||
mockStoreData['origins'] = {
|
||||
'/project/path': {
|
||||
'session-123': 'user',
|
||||
},
|
||||
};
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123']).toEqual({ origin: 'user' });
|
||||
});
|
||||
|
||||
it('should return full SessionOriginInfo for object origins', () => {
|
||||
storage.registerSessionOrigin('/project/path', 'session-123', 'user', 'Named');
|
||||
storage.updateSessionStarred('/project/path', 'session-123', true);
|
||||
storage.updateSessionContextUsage('/project/path', 'session-123', 60);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project/path');
|
||||
expect(origins['session-123']).toEqual({
|
||||
origin: 'user',
|
||||
sessionName: 'Named',
|
||||
starred: true,
|
||||
contextUsage: 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Path', () => {
|
||||
describe('getSessionPath', () => {
|
||||
it('should return correct local path', () => {
|
||||
const sessionPath = storage.getSessionPath('/project/path', 'session-123');
|
||||
|
||||
expect(sessionPath).toBeDefined();
|
||||
expect(sessionPath).toContain('session-123.jsonl');
|
||||
expect(sessionPath).toContain('.claude');
|
||||
expect(sessionPath).toContain('projects');
|
||||
});
|
||||
|
||||
it('should return remote path when sshConfig provided', () => {
|
||||
const sshConfig: SshRemoteConfig = {
|
||||
id: 'test-remote',
|
||||
name: 'Test Remote',
|
||||
host: 'remote.example.com',
|
||||
port: 22,
|
||||
username: 'testuser',
|
||||
privateKeyPath: '~/.ssh/id_rsa',
|
||||
enabled: true,
|
||||
useSshConfig: false,
|
||||
};
|
||||
const sessionPath = storage.getSessionPath('/project/path', 'session-123', sshConfig);
|
||||
|
||||
expect(sessionPath).toBeDefined();
|
||||
expect(sessionPath).toContain('session-123.jsonl');
|
||||
expect(sessionPath).toContain('~/.claude/projects');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agent ID', () => {
|
||||
it('should have correct agent ID', () => {
|
||||
expect(storage.agentId).toBe('claude-code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle special characters in project path', () => {
|
||||
storage.registerSessionOrigin('/path/with spaces/and-dashes', 'session-1', 'user');
|
||||
|
||||
const origins = storage.getSessionOrigins('/path/with spaces/and-dashes');
|
||||
expect(origins['session-1']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle special characters in session ID', () => {
|
||||
storage.registerSessionOrigin('/project', 'session-with-dashes-123', 'user');
|
||||
storage.registerSessionOrigin('/project', 'session_with_underscores', 'auto');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project');
|
||||
expect(origins['session-with-dashes-123']).toBeDefined();
|
||||
expect(origins['session_with_underscores']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty session name', () => {
|
||||
storage.registerSessionOrigin('/project', 'session-123', 'user', '');
|
||||
|
||||
const origins = storage.getSessionOrigins('/project');
|
||||
// Empty string is falsy, so sessionName is not stored when empty
|
||||
expect(origins['session-123']).toEqual({ origin: 'user' });
|
||||
});
|
||||
|
||||
it('should handle zero context usage', () => {
|
||||
storage.updateSessionContextUsage('/project', 'session-123', 0);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project');
|
||||
expect(origins['session-123'].contextUsage).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle 100% context usage', () => {
|
||||
storage.updateSessionContextUsage('/project', 'session-123', 100);
|
||||
|
||||
const origins = storage.getSessionOrigins('/project');
|
||||
expect(origins['session-123'].contextUsage).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Storage Persistence', () => {
|
||||
it('should call store.set on every origin update', () => {
|
||||
storage.registerSessionOrigin('/project', 'session-1', 'user');
|
||||
expect(mockStore.set).toHaveBeenCalledTimes(1);
|
||||
|
||||
storage.updateSessionName('/project', 'session-1', 'Name');
|
||||
expect(mockStore.set).toHaveBeenCalledTimes(2);
|
||||
|
||||
storage.updateSessionStarred('/project', 'session-1', true);
|
||||
expect(mockStore.set).toHaveBeenCalledTimes(3);
|
||||
|
||||
storage.updateSessionContextUsage('/project', 'session-1', 50);
|
||||
expect(mockStore.set).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should always call store.set with origins key', () => {
|
||||
storage.registerSessionOrigin('/project', 'session-1', 'user');
|
||||
|
||||
expect(mockStore.set).toHaveBeenCalledWith('origins', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
327
src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
Normal file
327
src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Tests for CallbackRegistry
|
||||
*
|
||||
* Verifies:
|
||||
* - Callback registration and retrieval
|
||||
* - Default return values when callbacks not set
|
||||
* - Proper delegation to registered callbacks
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CallbackRegistry } from '../../../../main/web-server/managers/CallbackRegistry';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../../../../main/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CallbackRegistry', () => {
|
||||
let registry: CallbackRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
registry = new CallbackRegistry();
|
||||
});
|
||||
|
||||
describe('Default Return Values', () => {
|
||||
it('should return empty array for getSessions when no callback set', () => {
|
||||
const result = registry.getSessions();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return null for getSessionDetail when no callback set', () => {
|
||||
const result = registry.getSessionDetail('session-123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for getTheme when no callback set', () => {
|
||||
const result = registry.getTheme();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return empty array for getCustomCommands when no callback set', () => {
|
||||
const result = registry.getCustomCommands();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return false for writeToSession when no callback set', () => {
|
||||
const result = registry.writeToSession('session-123', 'data');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for executeCommand when no callback set', async () => {
|
||||
const result = await registry.executeCommand('session-123', 'ls -la');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for interruptSession when no callback set', async () => {
|
||||
const result = await registry.interruptSession('session-123');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for switchMode when no callback set', async () => {
|
||||
const result = await registry.switchMode('session-123', 'ai');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for selectSession when no callback set', async () => {
|
||||
const result = await registry.selectSession('session-123');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for selectTab when no callback set', async () => {
|
||||
const result = await registry.selectTab('session-123', 'tab-1');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return null for newTab when no callback set', async () => {
|
||||
const result = await registry.newTab('session-123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false for closeTab when no callback set', async () => {
|
||||
const result = await registry.closeTab('session-123', 'tab-1');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for renameTab when no callback set', async () => {
|
||||
const result = await registry.renameTab('session-123', 'tab-1', 'New Name');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return empty array for getHistory when no callback set', () => {
|
||||
const result = registry.getHistory();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Callback Registration and Execution', () => {
|
||||
it('should call registered getSessions callback', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue([
|
||||
{ id: 'session-1', name: 'Session 1' },
|
||||
{ id: 'session-2', name: 'Session 2' },
|
||||
]);
|
||||
registry.setGetSessionsCallback(mockCallback);
|
||||
|
||||
const result = registry.getSessions();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe('session-1');
|
||||
});
|
||||
|
||||
it('should call registered getSessionDetail callback with arguments', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue({
|
||||
id: 'session-123',
|
||||
tabs: [{ id: 'tab-1' }],
|
||||
});
|
||||
registry.setGetSessionDetailCallback(mockCallback);
|
||||
|
||||
const result = registry.getSessionDetail('session-123', 'tab-1');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'tab-1');
|
||||
expect(result?.id).toBe('session-123');
|
||||
});
|
||||
|
||||
it('should call registered getTheme callback', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue({ name: 'dark', colors: {} });
|
||||
registry.setGetThemeCallback(mockCallback);
|
||||
|
||||
const result = registry.getTheme();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
expect(result?.name).toBe('dark');
|
||||
});
|
||||
|
||||
it('should call registered getCustomCommands callback', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue([{ name: 'cmd1', command: 'echo 1' }]);
|
||||
registry.setGetCustomCommandsCallback(mockCallback);
|
||||
|
||||
const result = registry.getCustomCommands();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call registered writeToSession callback', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue(true);
|
||||
registry.setWriteToSessionCallback(mockCallback);
|
||||
|
||||
const result = registry.writeToSession('session-123', 'test data');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'test data');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered executeCommand callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setExecuteCommandCallback(mockCallback);
|
||||
|
||||
const result = await registry.executeCommand('session-123', 'ls -la', 'ai');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'ls -la', 'ai');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered interruptSession callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setInterruptSessionCallback(mockCallback);
|
||||
|
||||
const result = await registry.interruptSession('session-123');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered switchMode callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setSwitchModeCallback(mockCallback);
|
||||
|
||||
const result = await registry.switchMode('session-123', 'terminal');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'terminal');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered selectSession callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setSelectSessionCallback(mockCallback);
|
||||
|
||||
const result = await registry.selectSession('session-123', 'tab-1');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'tab-1');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered selectTab callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setSelectTabCallback(mockCallback);
|
||||
|
||||
const result = await registry.selectTab('session-123', 'tab-1');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'tab-1');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered newTab callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({ tabId: 'new-tab-123' });
|
||||
registry.setNewTabCallback(mockCallback);
|
||||
|
||||
const result = await registry.newTab('session-123');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123');
|
||||
expect(result?.tabId).toBe('new-tab-123');
|
||||
});
|
||||
|
||||
it('should call registered closeTab callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setCloseTabCallback(mockCallback);
|
||||
|
||||
const result = await registry.closeTab('session-123', 'tab-1');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'tab-1');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered renameTab callback', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(true);
|
||||
registry.setRenameTabCallback(mockCallback);
|
||||
|
||||
const result = await registry.renameTab('session-123', 'tab-1', 'New Name');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('session-123', 'tab-1', 'New Name');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call registered getHistory callback with optional parameters', () => {
|
||||
const mockCallback = vi.fn().mockReturnValue([{ command: 'ls', timestamp: 123 }]);
|
||||
registry.setGetHistoryCallback(mockCallback);
|
||||
|
||||
const result = registry.getHistory('/project', 'session-123');
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith('/project', 'session-123');
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCallback', () => {
|
||||
it('should return false for unset callbacks', () => {
|
||||
expect(registry.hasCallback('getSessions')).toBe(false);
|
||||
expect(registry.hasCallback('getTheme')).toBe(false);
|
||||
expect(registry.hasCallback('executeCommand')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for set callbacks', () => {
|
||||
registry.setGetSessionsCallback(vi.fn());
|
||||
registry.setGetThemeCallback(vi.fn());
|
||||
registry.setExecuteCommandCallback(vi.fn());
|
||||
|
||||
expect(registry.hasCallback('getSessions')).toBe(true);
|
||||
expect(registry.hasCallback('getTheme')).toBe(true);
|
||||
expect(registry.hasCallback('executeCommand')).toBe(true);
|
||||
});
|
||||
|
||||
it('should check all callback types', () => {
|
||||
// Initially all should be false
|
||||
const callbackTypes = [
|
||||
'getSessions',
|
||||
'getSessionDetail',
|
||||
'getTheme',
|
||||
'getCustomCommands',
|
||||
'writeToSession',
|
||||
'executeCommand',
|
||||
'interruptSession',
|
||||
'switchMode',
|
||||
'selectSession',
|
||||
'selectTab',
|
||||
'newTab',
|
||||
'closeTab',
|
||||
'renameTab',
|
||||
'getHistory',
|
||||
] as const;
|
||||
|
||||
for (const type of callbackTypes) {
|
||||
expect(registry.hasCallback(type)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Callback Replacement', () => {
|
||||
it('should replace existing callback with new one', () => {
|
||||
const firstCallback = vi.fn().mockReturnValue([{ id: '1' }]);
|
||||
const secondCallback = vi.fn().mockReturnValue([{ id: '2' }]);
|
||||
|
||||
registry.setGetSessionsCallback(firstCallback);
|
||||
expect(registry.getSessions()[0].id).toBe('1');
|
||||
|
||||
registry.setGetSessionsCallback(secondCallback);
|
||||
expect(registry.getSessions()[0].id).toBe('2');
|
||||
|
||||
expect(firstCallback).toHaveBeenCalledTimes(1);
|
||||
expect(secondCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async Callback Handling', () => {
|
||||
it('should handle async executeCommand callback returning false', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue(false);
|
||||
registry.setExecuteCommandCallback(mockCallback);
|
||||
|
||||
const result = await registry.executeCommand('session-123', 'command');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle async switchMode callback rejection gracefully', async () => {
|
||||
const mockCallback = vi.fn().mockRejectedValue(new Error('Switch failed'));
|
||||
registry.setSwitchModeCallback(mockCallback);
|
||||
|
||||
await expect(registry.switchMode('session-123', 'ai')).rejects.toThrow('Switch failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* Tests for LiveSessionManager
|
||||
*
|
||||
* Verifies:
|
||||
* - Live session tracking (setLive, setOffline, isLive)
|
||||
* - AutoRun state management
|
||||
* - Broadcast callback integration
|
||||
* - Memory leak prevention (cleanup on offline)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
LiveSessionManager,
|
||||
LiveSessionBroadcastCallbacks,
|
||||
} from '../../../../main/web-server/managers/LiveSessionManager';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../../../../main/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LiveSessionManager', () => {
|
||||
let manager: LiveSessionManager;
|
||||
let mockBroadcastCallbacks: LiveSessionBroadcastCallbacks;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
manager = new LiveSessionManager();
|
||||
mockBroadcastCallbacks = {
|
||||
broadcastSessionLive: vi.fn(),
|
||||
broadcastSessionOffline: vi.fn(),
|
||||
broadcastAutoRunState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Live Session Tracking', () => {
|
||||
describe('setSessionLive', () => {
|
||||
it('should mark a session as live', () => {
|
||||
manager.setSessionLive('session-123');
|
||||
|
||||
expect(manager.isSessionLive('session-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('should store agent session ID when provided', () => {
|
||||
manager.setSessionLive('session-123', 'agent-session-abc');
|
||||
|
||||
const info = manager.getLiveSessionInfo('session-123');
|
||||
expect(info?.agentSessionId).toBe('agent-session-abc');
|
||||
});
|
||||
|
||||
it('should record enabledAt timestamp', () => {
|
||||
const before = Date.now();
|
||||
manager.setSessionLive('session-123');
|
||||
const after = Date.now();
|
||||
|
||||
const info = manager.getLiveSessionInfo('session-123');
|
||||
expect(info?.enabledAt).toBeGreaterThanOrEqual(before);
|
||||
expect(info?.enabledAt).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
it('should broadcast session live when callbacks set', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
manager.setSessionLive('session-123', 'agent-session-abc');
|
||||
|
||||
expect(mockBroadcastCallbacks.broadcastSessionLive).toHaveBeenCalledWith(
|
||||
'session-123',
|
||||
'agent-session-abc'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not broadcast when callbacks not set', () => {
|
||||
// No error should occur when broadcasting without callbacks
|
||||
manager.setSessionLive('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('should update existing session info when called again', () => {
|
||||
manager.setSessionLive('session-123', 'agent-1');
|
||||
const firstInfo = manager.getLiveSessionInfo('session-123');
|
||||
|
||||
manager.setSessionLive('session-123', 'agent-2');
|
||||
const secondInfo = manager.getLiveSessionInfo('session-123');
|
||||
|
||||
expect(secondInfo?.agentSessionId).toBe('agent-2');
|
||||
expect(secondInfo?.enabledAt).toBeGreaterThanOrEqual(firstInfo!.enabledAt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSessionOffline', () => {
|
||||
it('should mark a session as offline', () => {
|
||||
manager.setSessionLive('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(true);
|
||||
|
||||
manager.setSessionOffline('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(false);
|
||||
});
|
||||
|
||||
it('should broadcast session offline when callbacks set', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
manager.setSessionLive('session-123');
|
||||
manager.setSessionOffline('session-123');
|
||||
|
||||
expect(mockBroadcastCallbacks.broadcastSessionOffline).toHaveBeenCalledWith('session-123');
|
||||
});
|
||||
|
||||
it('should not broadcast if session was not live', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
manager.setSessionOffline('never-existed');
|
||||
|
||||
expect(mockBroadcastCallbacks.broadcastSessionOffline).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clean up associated AutoRun state (memory leak prevention)', () => {
|
||||
manager.setSessionLive('session-123');
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 5,
|
||||
currentTask: 'Task 5',
|
||||
});
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toBeDefined();
|
||||
|
||||
manager.setSessionOffline('session-123');
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSessionLive', () => {
|
||||
it('should return false for non-existent session', () => {
|
||||
expect(manager.isSessionLive('non-existent')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for live session', () => {
|
||||
manager.setSessionLive('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false after session goes offline', () => {
|
||||
manager.setSessionLive('session-123');
|
||||
manager.setSessionOffline('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLiveSessionInfo', () => {
|
||||
it('should return undefined for non-existent session', () => {
|
||||
expect(manager.getLiveSessionInfo('non-existent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return complete session info', () => {
|
||||
manager.setSessionLive('session-123', 'agent-session-abc');
|
||||
|
||||
const info = manager.getLiveSessionInfo('session-123');
|
||||
|
||||
expect(info).toEqual({
|
||||
sessionId: 'session-123',
|
||||
agentSessionId: 'agent-session-abc',
|
||||
enabledAt: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLiveSessions', () => {
|
||||
it('should return empty array when no sessions', () => {
|
||||
expect(manager.getLiveSessions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all live sessions', () => {
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setSessionLive('session-2');
|
||||
manager.setSessionLive('session-3');
|
||||
|
||||
const sessions = manager.getLiveSessions();
|
||||
|
||||
expect(sessions).toHaveLength(3);
|
||||
expect(sessions.map((s) => s.sessionId)).toContain('session-1');
|
||||
expect(sessions.map((s) => s.sessionId)).toContain('session-2');
|
||||
expect(sessions.map((s) => s.sessionId)).toContain('session-3');
|
||||
});
|
||||
|
||||
it('should not include offline sessions', () => {
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setSessionLive('session-2');
|
||||
manager.setSessionOffline('session-1');
|
||||
|
||||
const sessions = manager.getLiveSessions();
|
||||
|
||||
expect(sessions).toHaveLength(1);
|
||||
expect(sessions[0].sessionId).toBe('session-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLiveSessionIds', () => {
|
||||
it('should return iterable of session IDs', () => {
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setSessionLive('session-2');
|
||||
|
||||
const ids = Array.from(manager.getLiveSessionIds());
|
||||
|
||||
expect(ids).toHaveLength(2);
|
||||
expect(ids).toContain('session-1');
|
||||
expect(ids).toContain('session-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLiveSessionCount', () => {
|
||||
it('should return 0 when no sessions', () => {
|
||||
expect(manager.getLiveSessionCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return correct count', () => {
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setSessionLive('session-2');
|
||||
manager.setSessionLive('session-3');
|
||||
|
||||
expect(manager.getLiveSessionCount()).toBe(3);
|
||||
|
||||
manager.setSessionOffline('session-2');
|
||||
|
||||
expect(manager.getLiveSessionCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AutoRun State Management', () => {
|
||||
describe('setAutoRunState', () => {
|
||||
it('should store running AutoRun state', () => {
|
||||
const state = {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 3',
|
||||
};
|
||||
|
||||
manager.setAutoRunState('session-123', state);
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toEqual(state);
|
||||
});
|
||||
|
||||
it('should remove state when isRunning is false', () => {
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 3',
|
||||
});
|
||||
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: false,
|
||||
totalTasks: 10,
|
||||
completedTasks: 10,
|
||||
currentTask: 'Complete',
|
||||
});
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove state when null is passed', () => {
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 3',
|
||||
});
|
||||
|
||||
manager.setAutoRunState('session-123', null);
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should broadcast AutoRun state when callbacks set', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
const state = {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 3',
|
||||
};
|
||||
|
||||
manager.setAutoRunState('session-123', state);
|
||||
|
||||
expect(mockBroadcastCallbacks.broadcastAutoRunState).toHaveBeenCalledWith(
|
||||
'session-123',
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('should broadcast null state when clearing', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 3',
|
||||
});
|
||||
|
||||
manager.setAutoRunState('session-123', null);
|
||||
|
||||
expect(mockBroadcastCallbacks.broadcastAutoRunState).toHaveBeenLastCalledWith(
|
||||
'session-123',
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRunState', () => {
|
||||
it('should return undefined for non-existent state', () => {
|
||||
expect(manager.getAutoRunState('non-existent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return stored state', () => {
|
||||
const state = {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 2,
|
||||
currentTask: 'Task 2',
|
||||
};
|
||||
manager.setAutoRunState('session-123', state);
|
||||
|
||||
expect(manager.getAutoRunState('session-123')).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRunStates', () => {
|
||||
it('should return empty map when no states', () => {
|
||||
const states = manager.getAutoRunStates();
|
||||
expect(states.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return all stored states', () => {
|
||||
manager.setAutoRunState('session-1', {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 1,
|
||||
currentTask: 'Task 1',
|
||||
});
|
||||
manager.setAutoRunState('session-2', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 5,
|
||||
currentTask: 'Task 5',
|
||||
});
|
||||
|
||||
const states = manager.getAutoRunStates();
|
||||
|
||||
expect(states.size).toBe(2);
|
||||
expect(states.get('session-1')?.totalTasks).toBe(5);
|
||||
expect(states.get('session-2')?.totalTasks).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAll', () => {
|
||||
it('should mark all live sessions as offline', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setSessionLive('session-2');
|
||||
manager.setSessionLive('session-3');
|
||||
|
||||
manager.clearAll();
|
||||
|
||||
expect(manager.getLiveSessionCount()).toBe(0);
|
||||
expect(mockBroadcastCallbacks.broadcastSessionOffline).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should clear all AutoRun states', () => {
|
||||
manager.setSessionLive('session-1');
|
||||
manager.setAutoRunState('session-1', {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 1,
|
||||
currentTask: 'Task 1',
|
||||
});
|
||||
manager.setSessionLive('session-2');
|
||||
manager.setAutoRunState('session-2', {
|
||||
isRunning: true,
|
||||
totalTasks: 10,
|
||||
completedTasks: 5,
|
||||
currentTask: 'Task 5',
|
||||
});
|
||||
|
||||
manager.clearAll();
|
||||
|
||||
expect(manager.getAutoRunStates().size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle being called when already empty', () => {
|
||||
// Should not throw
|
||||
manager.clearAll();
|
||||
expect(manager.getLiveSessionCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Scenarios', () => {
|
||||
it('should handle full session lifecycle', () => {
|
||||
manager.setBroadcastCallbacks(mockBroadcastCallbacks);
|
||||
|
||||
// Session comes online
|
||||
manager.setSessionLive('session-123', 'agent-abc');
|
||||
expect(manager.isSessionLive('session-123')).toBe(true);
|
||||
expect(mockBroadcastCallbacks.broadcastSessionLive).toHaveBeenCalled();
|
||||
|
||||
// AutoRun starts
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 0,
|
||||
currentTask: 'Task 1',
|
||||
});
|
||||
expect(mockBroadcastCallbacks.broadcastAutoRunState).toHaveBeenCalled();
|
||||
|
||||
// AutoRun progresses
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 3,
|
||||
currentTask: 'Task 4',
|
||||
});
|
||||
|
||||
// AutoRun completes
|
||||
manager.setAutoRunState('session-123', {
|
||||
isRunning: false,
|
||||
totalTasks: 5,
|
||||
completedTasks: 5,
|
||||
currentTask: 'Complete',
|
||||
});
|
||||
expect(manager.getAutoRunState('session-123')).toBeUndefined();
|
||||
|
||||
// Session goes offline
|
||||
manager.setSessionOffline('session-123');
|
||||
expect(manager.isSessionLive('session-123')).toBe(false);
|
||||
expect(mockBroadcastCallbacks.broadcastSessionOffline).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle multiple concurrent sessions', () => {
|
||||
manager.setSessionLive('session-1', 'agent-1');
|
||||
manager.setSessionLive('session-2', 'agent-2');
|
||||
manager.setSessionLive('session-3', 'agent-3');
|
||||
|
||||
manager.setAutoRunState('session-1', {
|
||||
isRunning: true,
|
||||
totalTasks: 3,
|
||||
completedTasks: 1,
|
||||
currentTask: 'Task 1',
|
||||
});
|
||||
manager.setAutoRunState('session-3', {
|
||||
isRunning: true,
|
||||
totalTasks: 5,
|
||||
completedTasks: 2,
|
||||
currentTask: 'Task 2',
|
||||
});
|
||||
|
||||
expect(manager.getLiveSessionCount()).toBe(3);
|
||||
expect(manager.getAutoRunStates().size).toBe(2);
|
||||
|
||||
// Session 2 goes offline (no AutoRun state to clean)
|
||||
manager.setSessionOffline('session-2');
|
||||
expect(manager.getLiveSessionCount()).toBe(2);
|
||||
expect(manager.getAutoRunStates().size).toBe(2);
|
||||
|
||||
// Session 1 goes offline (has AutoRun state)
|
||||
manager.setSessionOffline('session-1');
|
||||
expect(manager.getLiveSessionCount()).toBe(1);
|
||||
expect(manager.getAutoRunStates().size).toBe(1);
|
||||
expect(manager.getAutoRunState('session-1')).toBeUndefined();
|
||||
expect(manager.getAutoRunState('session-3')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,9 @@
|
||||
*
|
||||
* Contains the configuration definitions for all supported AI agents.
|
||||
* This includes CLI arguments, configuration options, and default settings.
|
||||
*
|
||||
* Separated from agent-detector.ts for better maintainability.
|
||||
*/
|
||||
|
||||
import type { AgentCapabilities } from './agent-capabilities';
|
||||
import type { AgentCapabilities } from './capabilities';
|
||||
|
||||
// ============ Configuration Types ============
|
||||
|
||||
@@ -6,30 +6,14 @@
|
||||
* - Manages custom paths for agents
|
||||
* - Caches detection results for performance
|
||||
* - Discovers available models for agents that support model selection
|
||||
*
|
||||
* Architecture:
|
||||
* - Agent definitions are in agent-definitions.ts
|
||||
* - Path probing logic is in path-prober.ts
|
||||
* - Agent capabilities are in agent-capabilities.ts
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { execFileNoThrow } from './utils/execFile';
|
||||
import { logger } from './utils/logger';
|
||||
import { getAgentCapabilities } from './agent-capabilities';
|
||||
import { execFileNoThrow } from '../utils/execFile';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getAgentCapabilities } from './capabilities';
|
||||
import { checkBinaryExists, checkCustomPath, getExpandedEnv } from './path-prober';
|
||||
import { AGENT_DEFINITIONS, type AgentConfig } from './agent-definitions';
|
||||
|
||||
// ============ Re-exports for API Compatibility ============
|
||||
// These exports maintain backwards compatibility with existing code
|
||||
|
||||
export { AgentCapabilities } from './agent-capabilities';
|
||||
export {
|
||||
AGENT_DEFINITIONS,
|
||||
type AgentConfig,
|
||||
type AgentConfigOption,
|
||||
type AgentDefinition,
|
||||
} from './agent-definitions';
|
||||
import { AGENT_DEFINITIONS, type AgentConfig } from './definitions';
|
||||
|
||||
const LOG_CONTEXT = 'AgentDetector';
|
||||
|
||||
68
src/main/agents/index.ts
Normal file
68
src/main/agents/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Agents Module
|
||||
*
|
||||
* This module consolidates all agent-related functionality:
|
||||
* - Agent detection and configuration
|
||||
* - Agent definitions and types
|
||||
* - Agent capabilities
|
||||
* - Session storage interface
|
||||
* - Binary path probing
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { AgentDetector, AGENT_DEFINITIONS, getAgentCapabilities } from './agents';
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ============ Capabilities ============
|
||||
export {
|
||||
type AgentCapabilities,
|
||||
DEFAULT_CAPABILITIES,
|
||||
AGENT_CAPABILITIES,
|
||||
getAgentCapabilities,
|
||||
hasCapability,
|
||||
} from './capabilities';
|
||||
|
||||
// ============ Definitions ============
|
||||
export {
|
||||
type AgentConfigOption,
|
||||
type AgentConfig,
|
||||
type AgentDefinition,
|
||||
AGENT_DEFINITIONS,
|
||||
getAgentDefinition,
|
||||
getAgentIds,
|
||||
getVisibleAgentDefinitions,
|
||||
} from './definitions';
|
||||
|
||||
// ============ Detector ============
|
||||
export { AgentDetector } from './detector';
|
||||
|
||||
// ============ Path Prober ============
|
||||
export {
|
||||
type BinaryDetectionResult,
|
||||
getExpandedEnv,
|
||||
checkCustomPath,
|
||||
probeWindowsPaths,
|
||||
probeUnixPaths,
|
||||
checkBinaryExists,
|
||||
} from './path-prober';
|
||||
|
||||
// ============ Session Storage ============
|
||||
export {
|
||||
type AgentSessionOrigin,
|
||||
type SessionMessage,
|
||||
type AgentSessionInfo,
|
||||
type PaginatedSessionsResult,
|
||||
type SessionMessagesResult,
|
||||
type SessionSearchResult,
|
||||
type SessionSearchMode,
|
||||
type SessionListOptions,
|
||||
type SessionReadOptions,
|
||||
type SessionOriginInfo,
|
||||
type AgentSessionStorage,
|
||||
registerSessionStorage,
|
||||
getSessionStorage,
|
||||
hasSessionStorage,
|
||||
getAllSessionStorages,
|
||||
clearStorageRegistry,
|
||||
} from './session-storage';
|
||||
@@ -4,16 +4,14 @@
|
||||
* Handles detection of agent binaries on Windows and Unix-like systems.
|
||||
* Packaged Electron apps don't inherit shell environment, so we need to
|
||||
* probe known installation paths directly.
|
||||
*
|
||||
* Separated from agent-detector.ts for better maintainability and testability.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execFileNoThrow } from './utils/execFile';
|
||||
import { logger } from './utils/logger';
|
||||
import { expandTilde, detectNodeVersionManagerBinPaths } from '../shared/pathUtils';
|
||||
import { execFileNoThrow } from '../utils/execFile';
|
||||
import { logger } from '../utils/logger';
|
||||
import { expandTilde, detectNodeVersionManagerBinPaths } from '../../shared/pathUtils';
|
||||
|
||||
const LOG_CONTEXT = 'PathProber';
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { ToolType, SshRemoteConfig } from '../shared/types';
|
||||
import { logger } from './utils/logger';
|
||||
import type { ToolType, SshRemoteConfig } from '../../shared/types';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const LOG_CONTEXT = '[AgentSessionStorage]';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* - Custom args/env vars show only whether they're set, not values
|
||||
*/
|
||||
|
||||
import { AgentDetector, AgentCapabilities } from '../../agent-detector';
|
||||
import { AgentDetector, type AgentCapabilities } from '../../agents';
|
||||
import { sanitizePath } from './settings';
|
||||
|
||||
export interface AgentInfo {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from './collectors/windows-diagnostics';
|
||||
import { createZipPackage, PackageContents } from './packager';
|
||||
import { logger } from '../utils/logger';
|
||||
import { AgentDetector } from '../agent-detector';
|
||||
import { AgentDetector } from '../agents';
|
||||
import { ProcessManager } from '../process-manager';
|
||||
import { WebServer } from '../web-server';
|
||||
import Store from 'electron-store';
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './group-chat-storage';
|
||||
import { appendToLog } from './group-chat-log';
|
||||
import { IProcessManager, isModeratorActive } from './group-chat-moderator';
|
||||
import type { AgentDetector } from '../agent-detector';
|
||||
import type { AgentDetector } from '../agents';
|
||||
import {
|
||||
buildAgentArgs,
|
||||
applyAgentConfigOverrides,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
getModeratorSynthesisPrompt,
|
||||
} from './group-chat-moderator';
|
||||
import { addParticipant } from './group-chat-agent';
|
||||
import { AgentDetector } from '../agent-detector';
|
||||
import { AgentDetector } from '../agents';
|
||||
import { powerManager } from '../power-manager';
|
||||
import {
|
||||
buildAgentArgs,
|
||||
|
||||
@@ -5,7 +5,7 @@ import crypto from 'crypto';
|
||||
// which causes "Cannot read properties of undefined (reading 'getAppPath')" errors
|
||||
import { ProcessManager } from './process-manager';
|
||||
import { WebServer } from './web-server';
|
||||
import { AgentDetector } from './agent-detector';
|
||||
import { AgentDetector } from './agents';
|
||||
import { logger } from './utils/logger';
|
||||
import { tunnelManager } from './tunnel-manager';
|
||||
import { powerManager } from './power-manager';
|
||||
|
||||
@@ -21,11 +21,7 @@ import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { withIpcErrorLogging } from '../../utils/ipcHandler';
|
||||
import {
|
||||
getSessionStorage,
|
||||
hasSessionStorage,
|
||||
getAllSessionStorages,
|
||||
} from '../../agent-session-storage';
|
||||
import { getSessionStorage, hasSessionStorage, getAllSessionStorages } from '../../agents';
|
||||
import { calculateClaudeCost } from '../../utils/pricing';
|
||||
import {
|
||||
loadGlobalStatsCache,
|
||||
@@ -42,7 +38,7 @@ import type {
|
||||
SessionSearchMode,
|
||||
SessionListOptions,
|
||||
SessionReadOptions,
|
||||
} from '../../agent-session-storage';
|
||||
} from '../../agents';
|
||||
import type { GlobalAgentStats, ProviderStats, SshRemoteConfig } from '../../../shared/types';
|
||||
import type { MaestroSettings } from './persistence';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { AgentDetector, AGENT_DEFINITIONS } from '../../agent-detector';
|
||||
import { getAgentCapabilities } from '../../agent-capabilities';
|
||||
import { AgentDetector, AGENT_DEFINITIONS, getAgentCapabilities } from '../../agents';
|
||||
import { execFileNoThrow } from '../../utils/execFile';
|
||||
import { logger } from '../../utils/logger';
|
||||
import {
|
||||
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
requireDependency,
|
||||
CreateHandlerOptions,
|
||||
} from '../../utils/ipcHandler';
|
||||
import { getSessionStorage, type SessionMessagesResult } from '../../agent-session-storage';
|
||||
import { getSessionStorage, type SessionMessagesResult } from '../../agents';
|
||||
import { groomContext, cancelAllGroomingSessions } from '../../utils/context-groomer';
|
||||
import type { ProcessManager } from '../../process-manager';
|
||||
import type { AgentDetector } from '../../agent-detector';
|
||||
import type { AgentDetector } from '../../agents';
|
||||
|
||||
const LOG_CONTEXT = '[ContextMerge]';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
DebugPackageOptions,
|
||||
DebugPackageDependencies,
|
||||
} from '../../debug-package';
|
||||
import { AgentDetector } from '../../agent-detector';
|
||||
import { AgentDetector } from '../../agents';
|
||||
import { ProcessManager } from '../../process-manager';
|
||||
import { WebServer } from '../../web-server';
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
import { routeUserMessage } from '../../group-chat/group-chat-router';
|
||||
|
||||
// Agent detector import
|
||||
import { AgentDetector } from '../../agent-detector';
|
||||
import { AgentDetector } from '../../agents';
|
||||
import { groomContext } from '../../utils/context-groomer';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import { registerWebHandlers, WebHandlerDependencies } from './web';
|
||||
import { registerLeaderboardHandlers, LeaderboardHandlerDependencies } from './leaderboard';
|
||||
import { registerNotificationsHandlers } from './notifications';
|
||||
import { registerAgentErrorHandlers } from './agent-error';
|
||||
import { AgentDetector } from '../../agent-detector';
|
||||
import { AgentDetector } from '../../agents';
|
||||
import { ProcessManager } from '../../process-manager';
|
||||
import { WebServer } from '../../web-server';
|
||||
import { tunnelManager as tunnelManagerInstance } from '../../tunnel-manager';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ipcMain, BrowserWindow } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import * as os from 'os';
|
||||
import { ProcessManager } from '../../process-manager';
|
||||
import { AgentDetector } from '../../agent-detector';
|
||||
import { AgentDetector } from '../../agents';
|
||||
import { logger } from '../../utils/logger';
|
||||
import {
|
||||
buildAgentArgs,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { ProcessManager } from '../process-manager';
|
||||
import type { WebServer } from '../web-server';
|
||||
import type { AgentDetector } from '../agent-detector';
|
||||
import type { AgentDetector } from '../agents';
|
||||
import type { SafeSendFn } from '../utils/safe-send';
|
||||
import type { StatsDB } from '../stats-db';
|
||||
import type { GroupChat, GroupChatParticipant } from '../group-chat/group-chat-storage';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EventEmitter } from 'events';
|
||||
import * as path from 'path';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getOutputParser } from '../../parsers';
|
||||
import { getAgentCapabilities } from '../../agent-capabilities';
|
||||
import { getAgentCapabilities } from '../../agents';
|
||||
import type { ProcessConfig, ManagedProcess, SpawnResult } from '../types';
|
||||
import type { DataBufferManager } from '../handlers/DataBufferManager';
|
||||
import { StdoutHandler } from '../handlers/StdoutHandler';
|
||||
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
AgentSessionOrigin,
|
||||
SessionOriginInfo,
|
||||
SessionMessage,
|
||||
} from '../agent-session-storage';
|
||||
} from '../agents';
|
||||
import type { ToolType, SshRemoteConfig } from '../../shared/types';
|
||||
|
||||
const LOG_CONTEXT = '[ClaudeSessionStorage]';
|
||||
|
||||
@@ -35,7 +35,7 @@ import type {
|
||||
SessionListOptions,
|
||||
SessionReadOptions,
|
||||
SessionMessage,
|
||||
} from '../agent-session-storage';
|
||||
} from '../agents';
|
||||
import type { ToolType } from '../../shared/types';
|
||||
|
||||
const LOG_CONTEXT = '[CodexSessionStorage]';
|
||||
|
||||
@@ -10,7 +10,7 @@ export { OpenCodeSessionStorage } from './opencode-session-storage';
|
||||
export { CodexSessionStorage } from './codex-session-storage';
|
||||
|
||||
import Store from 'electron-store';
|
||||
import { registerSessionStorage } from '../agent-session-storage';
|
||||
import { registerSessionStorage } from '../agents';
|
||||
import { ClaudeSessionStorage, ClaudeSessionOriginsData } from './claude-session-storage';
|
||||
import { OpenCodeSessionStorage } from './opencode-session-storage';
|
||||
import { CodexSessionStorage } from './codex-session-storage';
|
||||
|
||||
@@ -33,7 +33,7 @@ import type {
|
||||
SessionListOptions,
|
||||
SessionReadOptions,
|
||||
SessionMessage,
|
||||
} from '../agent-session-storage';
|
||||
} from '../agents';
|
||||
import type { ToolType } from '../../shared/types';
|
||||
|
||||
const LOG_CONTEXT = '[OpenCodeSessionStorage]';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentConfig } from '../agent-detector';
|
||||
import type { AgentConfig } from '../agents';
|
||||
|
||||
type BuildAgentArgsOptions = {
|
||||
baseArgs: string[];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from './logger';
|
||||
import { buildAgentArgs } from './agent-args';
|
||||
import type { AgentDetector } from '../agent-detector';
|
||||
import type { AgentDetector } from '../agents';
|
||||
|
||||
const LOG_CONTEXT = '[ContextGroomer]';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user