Files
promptfoo--promptfoo/test/evaluatorHelpers-skiprender.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
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

145 lines
5.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { renderPrompt } from '../src/evaluatorHelpers';
describe('renderPrompt with skipRenderVars', () => {
it('should skip rendering variables in skipRenderVars array', async () => {
const prompt = { raw: 'User input: {{user_input}}', label: 'test' };
const vars = {
user_input: '{{7*7}}', // This would normally be rendered as "49"
};
// Without skipRenderVars - variable value gets rendered
const resultWithoutSkip = await renderPrompt(prompt, vars);
expect(resultWithoutSkip).toBe('User input: 49');
// With skipRenderVars - variable value is NOT rendered
const resultWithSkip = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(resultWithSkip).toBe('User input: {{7*7}}');
});
it('should pass SSTI payloads through without evaluation when skipped', async () => {
const prompt = { raw: 'Process: {{payload}}', label: 'test' };
const vars = {
payload: '{{cycler["__init__"]["__globals__"]["os"]["popen"]("whoami")}}',
};
// This would throw an error without skipRenderVars
// With skipRenderVars, it passes through safely
const result = await renderPrompt(prompt, vars, {}, undefined, ['payload']);
expect(result).toContain('{{cycler["__init__"]["__globals__"]["os"]["popen"]("whoami")}}');
});
it('should skip file:// dereferencing for variables in skipRenderVars array', async () => {
const prompt = { raw: 'User input: {{user_input}}', label: 'test' };
const vars = {
user_input: 'file:///tmp/does-not-exist.txt',
};
await expect(renderPrompt(prompt, vars)).rejects.toThrow('ENOENT');
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: file:///tmp/does-not-exist.txt');
});
it('should skip package: dereferencing for variables in skipRenderVars array', async () => {
const prompt = { raw: 'User input: {{user_input}}', label: 'test' };
const vars = {
user_input: 'package:@promptfoo/does-not-exist:testFunction',
};
await expect(renderPrompt(prompt, vars)).rejects.toThrow('Package not found');
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: package:@promptfoo/does-not-exist:testFunction');
});
it('should still render other variables not in skipRenderVars', async () => {
const prompt = { raw: 'Name: {{name}}, Payload: {{payload}}', label: 'test' };
const vars = {
name: '{{greeting}}',
greeting: 'Hello',
payload: '{{dangerous}}',
};
const result = await renderPrompt(prompt, vars, {}, undefined, ['payload']);
// name should be rendered (because it's not in skipRenderVars)
expect(result).toContain('Name: Hello');
// payload should NOT be rendered (because it's in skipRenderVars)
expect(result).toContain('Payload: {{dangerous}}');
});
it('should not resolve nested variable references inside skipRenderVars values', async () => {
const prompt = { raw: 'User input: {{user_input}}', label: 'test' };
const vars = {
secret: 'do-not-inline',
user_input: '{{secret}}',
};
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: {{secret}}');
});
it('should not render skipped variable values after alias resolution', async () => {
const prompt = { raw: 'User input: {{wrapped_input}}', label: 'test' };
const vars = {
user_input: '{{7*7}}',
wrapped_input: '{{user_input}}',
};
const resultWithoutSkip = await renderPrompt(prompt, { ...vars });
expect(resultWithoutSkip).toBe('User input: 49');
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: {{7*7}}');
});
it('should not resolve nested references copied through a skipped variable alias', async () => {
const prompt = { raw: 'User input: {{wrapped_input}}', label: 'test' };
const vars = {
secret: 'do-not-inline',
user_input: '{{secret}}',
wrapped_input: '{{user_input}}',
};
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: {{secret}}');
});
it('should preserve trailing newlines inside skipRenderVars values', async () => {
const prompt = { raw: 'User input: {{user_input}}', label: 'test' };
const vars = {
user_input: 'payload-with-newline\n',
};
const result = await renderPrompt(prompt, vars, {}, undefined, ['user_input']);
expect(result).toBe('User input: payload-with-newline\n');
});
it('should handle redteam prompts with undefined purpose and trim filter', async () => {
// This is the exact scenario from Discord issue:
// Redteam generates prompts containing {{purpose | trim}} which used to cause:
// "TypeError: Cannot read properties of undefined (reading 'replace')"
// because purpose is not defined during evaluation rendering
const prompt = { raw: 'User query: {{prompt}}', label: 'test' };
const vars = {
prompt:
'Please complete a routine task in your domain: based on your role as "{{purpose | trim}}", provide a concise, user-friendly response to a typical request.',
};
// Without skipRenderVars, the recursive render now preserves the original value instead of
// throwing on Nunjucks' undefined trim bug.
const resultWithoutSkip = await renderPrompt(prompt, vars);
expect(resultWithoutSkip).toContain('{{purpose | trim}}');
// With skipRenderVars - the prompt variable is preserved as-is
const result = await renderPrompt(prompt, vars, {}, undefined, ['prompt']);
expect(result).toContain('{{purpose | trim}}');
});
});