chore: import upstream snapshot with attribution
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
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
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
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user