diff --git a/plugins/promptfoo/src/agent/loop-install.test.ts b/plugins/promptfoo/src/agent/loop-install.test.ts new file mode 100644 index 0000000..3e82b24 --- /dev/null +++ b/plugins/promptfoo/src/agent/loop-install.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const execFileSyncMock = vi.fn(); + +vi.mock('node:child_process', () => ({ + execFileSync: execFileSyncMock, + execSync: vi.fn(), +})); + +describe('installProviderDependencies', () => { + beforeEach(() => { + execFileSyncMock.mockReset(); + }); + + it('invokes npm without building a shell string', async () => { + const { installProviderDependencies } = await import('./loop.js'); + + installProviderDependencies('/tmp/provider with spaces'); + + expect(execFileSyncMock).toHaveBeenCalledWith( + 'npm', + ['install', '--silent'], + expect.objectContaining({ + cwd: '/tmp/provider with spaces', + timeout: 60000, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + ); + }); +}); diff --git a/plugins/promptfoo/src/agent/loop.ts b/plugins/promptfoo/src/agent/loop.ts index 63a9c1e..3760e16 100644 --- a/plugins/promptfoo/src/agent/loop.ts +++ b/plugins/promptfoo/src/agent/loop.ts @@ -14,7 +14,7 @@ import type { LLMProvider, Message, ToolCall, ChatResponse } from './providers.j import type { DiscoveryResult } from '../types.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; import { pathToFileURL } from 'node:url'; export interface AgentOptions { @@ -193,6 +193,15 @@ Steps: }; } +export function installProviderDependencies(outputDir: string): void { + execFileSync('npm', ['install', '--silent'], { + cwd: outputDir, + timeout: 60000, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }); +} + /** * Execute a single tool call */ @@ -287,10 +296,7 @@ async function executeTool( const packageJsonPath = path.join(outputDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { - execSync(`cd "${outputDir}" && npm install --silent 2>&1`, { - timeout: 60000, - encoding: 'utf-8', - }); + installProviderDependencies(outputDir); } catch { // Ignore install errors, will surface in import }