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
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { processExecutableFile } from '../../../src/prompts/processors/executable';
|
|
|
|
import type { ApiProvider } from '../../../src/types/index';
|
|
|
|
vi.mock('../../../src/logger', () => ({
|
|
default: {
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../src/cache', () => ({
|
|
getCache: vi.fn(() => ({
|
|
get: vi.fn(),
|
|
set: vi.fn(),
|
|
})),
|
|
isCacheEnabled: vi.fn(() => false),
|
|
}));
|
|
|
|
describe('processExecutableFile', () => {
|
|
const mockProvider = {
|
|
id: vi.fn(() => 'test-provider'),
|
|
label: 'Test Provider',
|
|
callApi: vi.fn(),
|
|
} as ApiProvider;
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
const describeUnix = process.platform === 'win32' ? describe.skip : describe;
|
|
|
|
// Cross-platform tests
|
|
it('should process a script with exec: prefix', async () => {
|
|
const scriptPath =
|
|
process.platform === 'win32' ? 'cmd.exe /c echo "test"' : '/usr/bin/echo "test"';
|
|
const prompts = await processExecutableFile(scriptPath, {});
|
|
|
|
expect(prompts).toHaveLength(1);
|
|
expect(prompts[0].label).toBe(scriptPath);
|
|
expect(prompts[0].raw).toBe(scriptPath);
|
|
expect(typeof prompts[0].function).toBe('function');
|
|
});
|
|
|
|
it('should use custom label when provided', async () => {
|
|
const scriptPath = process.platform === 'win32' ? 'cmd.exe' : '/bin/echo';
|
|
const prompts = await processExecutableFile(scriptPath, { label: 'Custom Label' });
|
|
|
|
expect(prompts[0].label).toBe('Custom Label');
|
|
});
|
|
|
|
it('should handle binary executables', async () => {
|
|
const scriptPath =
|
|
process.platform === 'win32' ? 'C:\\Windows\\System32\\cmd.exe' : '/usr/bin/ls';
|
|
const prompts = await processExecutableFile(scriptPath, {});
|
|
|
|
expect(prompts).toHaveLength(1);
|
|
expect(prompts[0].label).toBe(scriptPath);
|
|
// Binary files should not be read as raw content
|
|
expect(prompts[0].raw).toBe(scriptPath);
|
|
});
|
|
|
|
it('should handle non-existent files gracefully', async () => {
|
|
const scriptPath = '/non/existent/script.sh';
|
|
const prompts = await processExecutableFile(scriptPath, {});
|
|
|
|
expect(prompts).toHaveLength(1);
|
|
expect(prompts[0].label).toBe(scriptPath);
|
|
expect(prompts[0].raw).toBe(scriptPath);
|
|
});
|
|
|
|
// Unix-specific tests
|
|
describeUnix('Unix shell script tests', () => {
|
|
let sharedPrompts: Awaited<ReturnType<typeof processExecutableFile>>;
|
|
let tempDir: string;
|
|
let scriptPath: string;
|
|
|
|
beforeAll(async () => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-executable-test-'));
|
|
scriptPath = path.join(tempDir, 'shared-test-script.sh');
|
|
fs.writeFileSync(
|
|
scriptPath,
|
|
`#!/bin/sh
|
|
context="$1"
|
|
case "$context" in
|
|
*'"mode":"args"'*)
|
|
printf '%s\\n' "Context received: $context"
|
|
;;
|
|
*'"mode":"config"'*)
|
|
printf '%s\\n' "Test output"
|
|
;;
|
|
*'"mode":"stderr"'*)
|
|
printf '%s\\n' "Error message" >&2
|
|
printf '%s\\n' "Normal output"
|
|
;;
|
|
*'"mode":"error"'*)
|
|
printf '%s\\n' "Error only" >&2
|
|
exit 1
|
|
;;
|
|
*'"mode":"relative"'*)
|
|
printf '%s\\n' "Relative path works"
|
|
;;
|
|
*)
|
|
printf '%s\\n' "Hello from shell script"
|
|
;;
|
|
esac
|
|
`,
|
|
);
|
|
fs.chmodSync(scriptPath, 0o755);
|
|
sharedPrompts = await processExecutableFile(scriptPath, {});
|
|
});
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should process a simple shell script', async () => {
|
|
expect(sharedPrompts).toHaveLength(1);
|
|
expect(sharedPrompts[0].label).toBe(scriptPath);
|
|
expect(sharedPrompts[0].raw).toContain('#!/bin/sh');
|
|
expect(typeof sharedPrompts[0].function).toBe('function');
|
|
|
|
const result = await sharedPrompts[0].function!({
|
|
vars: { test: 'value' },
|
|
provider: mockProvider,
|
|
});
|
|
|
|
expect(result).toBe('Hello from shell script');
|
|
});
|
|
|
|
it('should handle scripts with arguments', async () => {
|
|
const result = await sharedPrompts[0].function!({
|
|
vars: { mode: 'args', name: 'test' },
|
|
provider: mockProvider,
|
|
});
|
|
|
|
// The script should receive the context as JSON
|
|
expect(result).toContain('Context received:');
|
|
expect(result).toContain('"vars"');
|
|
expect(result).toContain('"name":"test"');
|
|
});
|
|
|
|
it('should pass config to the function', async () => {
|
|
const config = { temperature: 0.5 };
|
|
const prompts = await processExecutableFile(scriptPath, { config });
|
|
|
|
expect(prompts[0].config).toEqual(config);
|
|
|
|
const result = await prompts[0].function!({
|
|
vars: { mode: 'config' },
|
|
provider: mockProvider,
|
|
});
|
|
|
|
expect(result).toBe('Test output');
|
|
});
|
|
|
|
it('should handle scripts that output to stderr', async () => {
|
|
const result = await sharedPrompts[0].function!({
|
|
vars: { mode: 'stderr' },
|
|
provider: mockProvider,
|
|
});
|
|
|
|
// Should return stdout even if there's stderr
|
|
expect(result).toBe('Normal output');
|
|
});
|
|
|
|
it('should reject when script fails with no stdout', async () => {
|
|
await expect(
|
|
sharedPrompts[0].function!({
|
|
vars: { mode: 'error' },
|
|
provider: mockProvider,
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('should resolve relative paths from prompt.config.basePath', async () => {
|
|
const relativeScriptPath = `.${path.sep}${path.basename(scriptPath)}`;
|
|
const prompts = await processExecutableFile(relativeScriptPath, {
|
|
config: { basePath: tempDir },
|
|
});
|
|
|
|
expect(prompts).toHaveLength(1);
|
|
expect(prompts[0].label).toBe(relativeScriptPath);
|
|
expect(typeof prompts[0].function).toBe('function');
|
|
|
|
const result = await prompts[0].function!({
|
|
vars: { mode: 'relative' },
|
|
provider: mockProvider,
|
|
});
|
|
|
|
expect(result).toBe('Relative path works');
|
|
});
|
|
});
|
|
});
|