Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

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