0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import { PythonProvider } from '../../src/providers/pythonCompletion';
|
|
import * as pythonUtils from '../../src/python/pythonUtils';
|
|
import { mockProcessEnv } from '../util/utils';
|
|
|
|
// Windows CI has severe filesystem delays - allow up to 90s
|
|
const TEST_TIMEOUT = process.platform === 'win32' ? 90000 : 15000;
|
|
|
|
// Windows-specific test to verify pipe delimiter handles drive letters correctly
|
|
// This test ensures paths like C:\ don't break the protocol parsing
|
|
describe('PythonProvider Windows Path Handling', () => {
|
|
let tempDir: string;
|
|
let restoreEnv: () => void;
|
|
let pathProvider: PythonProvider;
|
|
let concurrentProvider: PythonProvider;
|
|
let protocolProvider: PythonProvider;
|
|
let specialCharsProvider: PythonProvider;
|
|
const providers: PythonProvider[] = [];
|
|
|
|
const createProvider = (scriptName: string, scriptContent: string) => {
|
|
const scriptPath = path.join(tempDir, scriptName);
|
|
fs.writeFileSync(scriptPath, scriptContent);
|
|
const provider = new PythonProvider(scriptPath, {
|
|
id: `python:${scriptName}`,
|
|
config: { basePath: tempDir },
|
|
});
|
|
providers.push(provider);
|
|
return provider;
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
// Reset Python state
|
|
pythonUtils.state.cachedPythonPath = null;
|
|
pythonUtils.state.validationPromise = null;
|
|
|
|
restoreEnv = mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: 'false' });
|
|
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-windows-path-test-'));
|
|
|
|
pathProvider = createProvider(
|
|
'path_test.py',
|
|
`
|
|
import os
|
|
|
|
def call_api(prompt, options, context):
|
|
temp_dir = os.environ.get('TEMP', os.environ.get('TMP', '/tmp'))
|
|
return {
|
|
"output": f"Processed: {prompt}",
|
|
"metadata": {
|
|
"temp_path": temp_dir,
|
|
"has_colon": ":" in temp_dir
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
concurrentProvider = createProvider(
|
|
'concurrent_test.py',
|
|
`
|
|
def call_api(prompt, options, context):
|
|
return {
|
|
"output": f"Processed: {prompt}"
|
|
}
|
|
`,
|
|
);
|
|
|
|
protocolProvider = createProvider(
|
|
'protocol_test.py',
|
|
`
|
|
def call_api(prompt, options, context):
|
|
# If we got here, the protocol parsing worked correctly
|
|
return {
|
|
"output": "Protocol parsing successful",
|
|
"platform": "${process.platform}"
|
|
}
|
|
`,
|
|
);
|
|
|
|
specialCharsProvider = createProvider(
|
|
'special_chars.py',
|
|
`
|
|
def call_api(prompt, options, context):
|
|
import os
|
|
temp_dir = os.environ.get('TEMP', '/tmp')
|
|
return {
|
|
"output": "Success",
|
|
"metadata": {
|
|
"temp_dir": temp_dir,
|
|
"has_special_chars": any(c in temp_dir for c in [' ', '-', '_', '.'])
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
|
|
await Promise.all(providers.map((provider) => provider.initialize()));
|
|
}, TEST_TIMEOUT);
|
|
|
|
afterAll(async () => {
|
|
// Cleanup providers
|
|
const shutdownResults = await Promise.allSettled(
|
|
providers.map(async (provider) => ({
|
|
providerId: provider.id(),
|
|
result: await provider.shutdown(),
|
|
})),
|
|
);
|
|
const shutdownFailures = shutdownResults
|
|
.map((result, index) =>
|
|
result.status === 'rejected'
|
|
? `${providers[index]?.id() ?? `provider-${index}`}: ${String(result.reason)}`
|
|
: null,
|
|
)
|
|
.filter((failure): failure is string => failure !== null);
|
|
|
|
providers.length = 0;
|
|
|
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true });
|
|
}
|
|
|
|
pythonUtils.state.cachedPythonPath = null;
|
|
pythonUtils.state.validationPromise = null;
|
|
|
|
restoreEnv();
|
|
|
|
if (shutdownFailures.length > 0) {
|
|
throw new Error(`PythonProvider shutdown failed: ${shutdownFailures.join('; ')}`);
|
|
}
|
|
}, TEST_TIMEOUT);
|
|
|
|
it(
|
|
'should handle paths with colons (like C:\\ on Windows)',
|
|
async () => {
|
|
// This test verifies that the protocol delimiter (pipe |) doesn't conflict
|
|
// with Windows drive letters (C:, D:, etc.) in file paths
|
|
const result = await pathProvider.callApi('Test prompt');
|
|
|
|
// Verify the call succeeded
|
|
expect(result.output).toBe('Processed: Test prompt');
|
|
expect(result.error).toBeUndefined();
|
|
|
|
// On Windows, temp path should contain a colon (C:, D:, etc.)
|
|
if (process.platform === 'win32') {
|
|
expect(result.metadata?.has_colon).toBe(true);
|
|
expect(result.metadata?.temp_path).toMatch(/^[A-Z]:\\/);
|
|
}
|
|
},
|
|
TEST_TIMEOUT,
|
|
);
|
|
|
|
it(
|
|
'should handle multiple concurrent calls with Windows paths',
|
|
async () => {
|
|
// Stress test: ensure protocol works with multiple concurrent requests
|
|
// where temp file paths all contain colons
|
|
// Execute multiple calls concurrently
|
|
const promises = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
promises.push(concurrentProvider.callApi(`Request ${i}`));
|
|
}
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
// All calls should succeed
|
|
expect(results).toHaveLength(5);
|
|
results.forEach((result, index) => {
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.output).toBe(`Processed: Request ${index}`);
|
|
});
|
|
},
|
|
TEST_TIMEOUT,
|
|
);
|
|
|
|
it(
|
|
'should parse protocol commands correctly with Windows paths',
|
|
async () => {
|
|
// This test verifies the internal protocol command parsing
|
|
// Command format: CALL|function_name|request_file|response_file
|
|
// With Windows paths: CALL|call_api|C:\path\req.json|C:\path\resp.json
|
|
const result = await protocolProvider.callApi('Test');
|
|
|
|
expect(result.output).toBe('Protocol parsing successful');
|
|
expect(result.error).toBeUndefined();
|
|
},
|
|
TEST_TIMEOUT,
|
|
);
|
|
|
|
it(
|
|
'should handle paths with special characters',
|
|
async () => {
|
|
// Test that paths with various special characters work
|
|
// (except pipe | which is the delimiter)
|
|
const result = await specialCharsProvider.callApi('Test');
|
|
|
|
expect(result.output).toBe('Success');
|
|
expect(result.error).toBeUndefined();
|
|
// Verify temp directory path was processed correctly
|
|
expect(result.metadata?.temp_dir).toBeTruthy();
|
|
},
|
|
TEST_TIMEOUT,
|
|
);
|
|
});
|