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:
Raza Rauf
2026-01-29 22:20:58 +05:00
parent e95ef0c369
commit 454cdefd44
50 changed files with 1464 additions and 194 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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';

View File

@@ -5,7 +5,7 @@ import {
AGENT_CAPABILITIES,
getAgentCapabilities,
hasCapability,
} from '../../main/agent-capabilities';
} from '../../../main/agents';
describe('agent-capabilities', () => {
describe('AgentCapabilities interface', () => {

View File

@@ -12,7 +12,7 @@ import {
getVisibleAgentDefinitions,
type AgentDefinition,
type AgentConfigOption,
} from '../../main/agent-definitions';
} from '../../../main/agents';
describe('agent-definitions', () => {
describe('AGENT_DEFINITIONS', () => {

View File

@@ -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';

View File

@@ -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(() => {

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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));
});
});
});

View 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');
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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 ============

View File

@@ -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
View 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';

View File

@@ -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';

View File

@@ -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]';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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]';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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]';

View File

@@ -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]';

View File

@@ -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';

View File

@@ -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]';

View File

@@ -1,4 +1,4 @@
import type { AgentConfig } from '../agent-detector';
import type { AgentConfig } from '../agents';
type BuildAgentArgsOptions = {
baseArgs: string[];

View File

@@ -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]';