Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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 @@ -189,7 +189,7 @@

// nvm: Check for ~/.nvm and find installed node versions
const nvmDir = process.env.NVM_DIR || path.join(home, '.nvm');
if (fs.existsSync(nvmDir)) {

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ buildExpandedEnv src/shared/pathUtils.ts:431:13 ❯ src/cli/services/agent-spawner.ts:180:15 ❯ spawnClaudeAgent src/cli/services/agent-spawner.ts:179:9 ❯ Module.spawnAgent src/cli/services/agent-spawner.ts:469:10 ❯ src/__tests__/cli/services/agent-spawner.test.ts:835:26 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should parse result from stdout". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ buildExpandedEnv src/shared/pathUtils.ts:431:13 ❯ src/cli/services/agent-spawner.ts:180:15 ❯ spawnClaudeAgent src/cli/services/agent-spawner.ts:179:9 ❯ Module.spawnAgent src/cli/services/agent-spawner.ts:469:10 ❯ src/__tests__/cli/services/agent-spawner.test.ts:811:26 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should use --resume for existing session". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ buildExpandedEnv src/shared/pathUtils.ts:431:13 ❯ src/cli/services/agent-spawner.ts:180:15 ❯ spawnClaudeAgent src/cli/services/agent-spawner.ts:179:9 ❯ Module.spawnAgent src/cli/services/agent-spawner.ts:469:10 ❯ src/__tests__/cli/services/agent-spawner.test.ts:775:26 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should spawn Claude with correct arguments". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 ❯ src/__tests__/cli/services/agent-spawner.test.ts:728:26 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should return unavailable when agent is not found". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should fall back to PATH detection when custom path is invalid". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should reject non-executable files on Unix". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should reject non-file paths". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 ❯ detectClaude src/cli/services/agent-spawner.ts:141:35 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should handle which command error". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 ❯ detectClaude src/cli/services/agent-spawner.ts:141:35 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should return unavailable when Claude is not found". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 192 in src/shared/pathUtils.ts

View workflow job for this annotation

GitHub Actions / test

Unhandled error

Error: [vitest] No "existsSync" export is defined on the "fs" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("fs"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ detectNodeVersionManagerBinPaths src/shared/pathUtils.ts:192:9 ❯ buildExpandedPath src/shared/pathUtils.ts:305:30 ❯ getExpandedPath src/cli/services/agent-spawner.ts:51:9 ❯ src/cli/services/agent-spawner.ts:81:39 ❯ findCommandInPath src/cli/services/agent-spawner.ts:80:9 ❯ detectAgent src/cli/services/agent-spawner.ts:131:27 This error originated in "src/__tests__/cli/services/agent-spawner.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should fall back to PATH detection when custom path is invalid". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
// Check nvm/current symlink first (preferred)
const nvmCurrentBin = path.join(nvmDir, 'current', 'bin');
if (fs.existsSync(nvmCurrentBin)) {
Expand Down Expand Up @@ -302,6 +302,7 @@
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 @@
} 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 @@
];
}

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