Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/__tests__/cli/services/agent-spawner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,50 @@ vi.mock('child_process', async (importOriginal) => {
});

// Mock fs module
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
vi.mock('fs', async () => {
const actual = await vi.importActual<typeof import('fs')>('fs');
const mocked = {
...actual,
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
existsSync: vi.fn(() => false),
readdirSync: vi.fn(() => []),
mkdirSync: vi.fn(),
createWriteStream: vi.fn(
() =>
({
write: vi.fn(),
end: vi.fn(),
}) as any
),
promises: {
...actual.promises,
stat: vi.fn(),
access: vi.fn(),
},
constants: {
X_OK: 1,
},
};
return {
...mocked,
default: mocked,
};
});

// Mock os module
vi.mock('os', () => ({
homedir: vi.fn(() => '/Users/testuser'),
}));
vi.mock('os', async () => {
const actual = await vi.importActual<typeof import('os')>('os');
const mocked = {
...actual,
homedir: vi.fn(() => '/Users/testuser'),
tmpdir: vi.fn(() => '/tmp'),
};
return {
...mocked,
default: mocked,
};
});

// Mock storage service
const mockGetAgentCustomPath = vi.fn();
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/main/agents/session-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vi.mock('os', async () => {
const mocked = {
...actual,
homedir: vi.fn(() => '/tmp/maestro-session-storage-home'),
tmpdir: vi.fn(() => '/tmp'),
};
return {
...mocked,
Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/shared/pathUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import {
expandTilde,
parseVersion,
Expand All @@ -28,6 +30,7 @@ vi.mock('os', async () => {
return {
...actual,
homedir: vi.fn(() => '/Users/testuser'),
tmpdir: () => '/tmp',
};
});

Expand Down Expand Up @@ -276,6 +279,35 @@ describe('buildExpandedPath', () => {
expect(homebrewIndex).toBeLessThan(customIndex);
});

it('should prepend detected Node version manager bin paths', () => {
process.env.PATH = '/usr/bin';
const originalNvmDir = process.env.NVM_DIR;
const tempNvmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'maestro-nvm-'));
process.env.NVM_DIR = tempNvmDir;
fs.mkdirSync(path.join(tempNvmDir, 'current', 'bin'), { recursive: true });
fs.mkdirSync(path.join(tempNvmDir, 'versions', 'node', 'v22.10.0', 'bin'), {
recursive: true,
});

try {
const result = buildExpandedPath();
const pathParts = result.split(':');
const currentBin = path.join(tempNvmDir, 'current', 'bin');
const versionedBin = path.join(tempNvmDir, 'versions', 'node', 'v22.10.0', 'bin');

expect(pathParts[0]).toBe(currentBin);
expect(pathParts).toContain(versionedBin);
expect(pathParts.indexOf(currentBin)).toBeLessThan(pathParts.indexOf(versionedBin));
} finally {
if (originalNvmDir === undefined) {
delete process.env.NVM_DIR;
} else {
process.env.NVM_DIR = originalNvmDir;
}
fs.rmSync(tempNvmDir, { recursive: true, force: true });
}
});

it('should accept custom paths that are prepended first', () => {
process.env.PATH = '/usr/bin';
const result = buildExpandedPath(['/my/custom/bin', '/another/path']);
Expand Down
32 changes: 32 additions & 0 deletions src/main/process-manager/utils/__tests__/envBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { buildChildProcessEnv, buildPtyTerminalEnv } from '../envBuilder';
Expand Down Expand Up @@ -304,6 +305,37 @@ describe('envBuilder - Global Environment Variables', () => {
// The actual value depends on the system, but it should exist
expect((env.PATH as string).length).toBeGreaterThan(0);
});

it('should include detected Node version manager bins in PATH', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'darwin' });
const originalNvmDir = process.env.NVM_DIR;
const tempNvmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'maestro-nvm-'));
process.env.NVM_DIR = tempNvmDir;
fs.mkdirSync(path.join(tempNvmDir, 'current', 'bin'), { recursive: true });
fs.mkdirSync(path.join(tempNvmDir, 'versions', 'node', 'v22.10.0', 'bin'), {
recursive: true,
});

try {
const env = buildChildProcessEnv();
const pathParts = env.PATH?.split(path.delimiter) || [];
const currentBin = path.join(tempNvmDir, 'current', 'bin');
const versionedBin = path.join(tempNvmDir, 'versions', 'node', 'v22.10.0', 'bin');

expect(pathParts[0]).toBe(currentBin);
expect(pathParts).toContain(versionedBin);
expect(pathParts.indexOf(currentBin)).toBeLessThan(pathParts.indexOf(versionedBin));
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform });
if (originalNvmDir === undefined) {
delete process.env.NVM_DIR;
} else {
process.env.NVM_DIR = originalNvmDir;
}
fs.rmSync(tempNvmDir, { recursive: true, force: true });
}
});
});

describe('Test 2.6: Complex Precedence Chain', () => {
Expand Down
11 changes: 8 additions & 3 deletions src/shared/pathUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export function detectNodeVersionManagerBinPaths(): string[] {
export function buildExpandedPath(customPaths?: string[]): string {
const delimiter = path.delimiter;
const home = os.homedir();
const versionManagerPaths = detectNodeVersionManagerBinPaths();

// Start with current PATH
const currentPath = process.env.PATH || '';
Expand Down Expand Up @@ -370,6 +371,7 @@ export function buildExpandedPath(customPaths?: string[]): string {
} else {
// Unix-like paths (macOS/Linux)
additionalPaths = [
...versionManagerPaths,
'/opt/homebrew/bin', // Homebrew on Apple Silicon
'/opt/homebrew/sbin',
'/usr/local/bin', // Homebrew on Intel, common install location
Expand All @@ -386,17 +388,20 @@ export function buildExpandedPath(customPaths?: string[]): string {
];
}

// Add custom paths first (if provided)
// Iterate in reverse because each entry is prepended with unshift().
// This preserves the caller's intended left-to-right path precedence.
if (customPaths && customPaths.length > 0) {
for (const p of customPaths) {
for (let i = customPaths.length - 1; i >= 0; i--) {
const p = customPaths[i];
if (!pathParts.includes(p)) {
pathParts.unshift(p);
}
}
}

// Add standard additional paths
for (const p of additionalPaths) {
for (let i = additionalPaths.length - 1; i >= 0; i--) {
const p = additionalPaths[i];
if (!pathParts.includes(p)) {
pathParts.unshift(p);
}
Expand Down
Loading