Files
promptfoo--promptfoo/test/providers/pythonCompletion.unicode.test.ts
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

159 lines
5.3 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';
import type { CallApiContextParams } from '../../src/types/index';
// Windows CI has severe filesystem delays (antivirus, etc.) - allow up to 90s
// (60s for file retry + 30s for Python startup and test overhead)
const TEST_TIMEOUT = process.platform === 'win32' ? 90000 : 15000;
// Skip on Windows CI due to aggressive file security policies blocking temp file IPC
// Works fine on local Windows and all other platforms
const describeOrSkip = process.platform === 'win32' && process.env.CI ? describe.skip : describe;
describeOrSkip('PythonProvider Unicode handling', () => {
let tempDir: string;
let provider: PythonProvider;
let restoreEnv: () => void;
beforeAll(() => {
restoreEnv = mockProcessEnv({ PROMPTFOO_CACHE_ENABLED: 'false' });
pythonUtils.state.cachedPythonPath = null;
pythonUtils.state.validationPromise = null;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-unicode-test-'));
const scriptPath = path.join(tempDir, 'unicode_test.py');
fs.writeFileSync(
scriptPath,
`
def call_api(prompt, options, context):
vars = context.get('vars', {}) if context else {}
if vars.get('mode') == 'nested':
return {
"output": "Complex Unicode test",
"nested": {
"products": [
{"name": "Product®", "price": "€100"},
{"name": "Brand™", "price": "€200"},
{"name": "Item© 2025", "price": "€300"}
],
"metadata": {
"temperature": "25°C",
"description": "Advanced Product® with Brand™ technology ©2025"
}
}
}
if 'product' in vars:
product_name = vars.get('product', 'Unknown')
return {
"output": f"{prompt} - Product: {product_name}",
"metadata": {
"product_name": product_name,
"product_bytes": len(product_name.encode('utf-8'))
}
}
return {
"output": f"Received: {prompt}",
"metadata": {
"prompt_length": len(prompt),
"prompt_bytes": len(prompt.encode('utf-8'))
}
}
`,
);
provider = new PythonProvider(scriptPath, {
id: 'python:unicode_test.py',
config: { basePath: tempDir },
});
});
afterAll(async () => {
restoreEnv();
await provider?.shutdown().catch(() => {});
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true });
}
});
it(
'should correctly handle Unicode characters in prompt',
async () => {
const result = await provider.callApi('Product® Plus');
// Verify the result structure and Unicode preservation
expect(result.output).toBe('Received: Product® Plus');
expect(result.metadata?.prompt_length).toBe(13);
expect(result.metadata?.prompt_bytes).toBe(14);
expect(result.error).toBeUndefined();
// Ensure no null bytes in JSON serialization
const jsonStr = JSON.stringify(result);
expect(jsonStr).not.toContain('\u0000');
expect(jsonStr).not.toContain('\\u0000');
expect(jsonStr).toContain('Product® Plus');
},
TEST_TIMEOUT,
);
it(
'should handle Unicode in context vars',
async () => {
const context: CallApiContextParams = {
prompt: { raw: 'Test prompt', label: 'test' },
vars: {
product: 'Product® Plus™',
company: '© 2025 Company',
price: '€100',
},
};
const result = await provider.callApi('Test prompt', context);
// Verify Unicode handling in response
expect(result.output).toBe('Test prompt - Product: Product® Plus™');
expect(result.metadata?.product_name).toBe('Product® Plus™');
// Note: ® is 2 bytes, ™ is 3 bytes in UTF-8, so total is 17 bytes
expect(result.metadata?.product_bytes).toBe(17);
},
TEST_TIMEOUT,
);
it(
'should handle complex nested Unicode data',
async () => {
const result = await provider.callApi('Test', {
prompt: { raw: 'Test', label: 'test' },
vars: { mode: 'nested' },
});
// Verify complex nested Unicode structures are preserved
const resultAny = result as any;
expect(resultAny.nested.products[0].name).toBe('Product®');
expect(resultAny.nested.products[1].name).toBe('Brand™');
expect(resultAny.nested.products[2].name).toBe('Item© 2025');
expect(resultAny.nested.metadata.temperature).toBe('25°C');
expect(resultAny.nested.metadata.description).toBe(
'Advanced Product® with Brand™ technology ©2025',
);
// Ensure nested Unicode data serializes properly
const jsonStr = JSON.stringify(result);
expect(jsonStr).toContain('Product®');
expect(jsonStr).toContain('Brand™');
expect(jsonStr).toContain('€100');
expect(jsonStr).toContain('25°C');
expect(jsonStr).not.toContain('\u0000');
},
TEST_TIMEOUT,
);
});