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
190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { generateIdFromPrompt } from '../../src/models/prompt';
|
|
import { sha256 } from '../../src/util/createHash';
|
|
|
|
describe('generateIdFromPrompt', () => {
|
|
describe('priority order', () => {
|
|
it('uses label when provided (highest priority)', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'my-id',
|
|
label: 'My Label',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('My Label'));
|
|
});
|
|
|
|
it('uses id when label is empty string', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'my-id',
|
|
label: '',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('my-id'));
|
|
});
|
|
|
|
it('uses id when label is undefined', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'my-id',
|
|
label: undefined,
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('my-id'));
|
|
});
|
|
|
|
it('uses id when label is not provided', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'my-id',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('my-id'));
|
|
});
|
|
|
|
it('falls back to raw when both label and id are empty', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: '',
|
|
label: '',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('My raw prompt'));
|
|
});
|
|
|
|
it('falls back to raw when both label and id are undefined', () => {
|
|
const result = generateIdFromPrompt({
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256('My raw prompt'));
|
|
});
|
|
});
|
|
|
|
describe('raw content handling', () => {
|
|
it('hashes string raw content directly', () => {
|
|
const result = generateIdFromPrompt({
|
|
raw: 'Simple string prompt',
|
|
});
|
|
expect(result).toBe(sha256('Simple string prompt'));
|
|
});
|
|
|
|
it('stringifies object raw content before hashing', () => {
|
|
const rawObject = { role: 'user', content: 'Hello' };
|
|
const result = generateIdFromPrompt({
|
|
raw: rawObject,
|
|
});
|
|
expect(result).toBe(sha256(JSON.stringify(rawObject)));
|
|
});
|
|
|
|
it('stringifies array raw content before hashing', () => {
|
|
const rawArray = [
|
|
{ role: 'system', content: 'You are helpful' },
|
|
{ role: 'user', content: 'Hello' },
|
|
];
|
|
const result = generateIdFromPrompt({
|
|
raw: rawArray,
|
|
});
|
|
expect(result).toBe(sha256(JSON.stringify(rawArray)));
|
|
});
|
|
|
|
it('handles empty string raw content', () => {
|
|
const result = generateIdFromPrompt({
|
|
raw: '',
|
|
});
|
|
expect(result).toBe(sha256(''));
|
|
});
|
|
|
|
it('handles empty object raw content', () => {
|
|
const result = generateIdFromPrompt({
|
|
raw: {},
|
|
});
|
|
expect(result).toBe(sha256('{}'));
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('treats whitespace-only label as truthy', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'my-id',
|
|
label: ' ',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256(' '));
|
|
});
|
|
|
|
it('treats whitespace-only id as truthy', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: ' ',
|
|
label: '',
|
|
raw: 'My raw prompt',
|
|
});
|
|
expect(result).toBe(sha256(' '));
|
|
});
|
|
|
|
it('produces consistent hashes for same input', () => {
|
|
const prompt = { label: 'Test', raw: 'Content' };
|
|
const result1 = generateIdFromPrompt(prompt);
|
|
const result2 = generateIdFromPrompt(prompt);
|
|
expect(result1).toBe(result2);
|
|
});
|
|
|
|
it('produces different hashes for different labels', () => {
|
|
const result1 = generateIdFromPrompt({ label: 'Label A', raw: 'Same content' });
|
|
const result2 = generateIdFromPrompt({ label: 'Label B', raw: 'Same content' });
|
|
expect(result1).not.toBe(result2);
|
|
});
|
|
|
|
it('produces different hashes for different raw content', () => {
|
|
const result1 = generateIdFromPrompt({ raw: 'Content A' });
|
|
const result2 = generateIdFromPrompt({ raw: 'Content B' });
|
|
expect(result1).not.toBe(result2);
|
|
});
|
|
});
|
|
|
|
describe('real-world scenarios', () => {
|
|
it('handles prompt with all properties', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'prompt-123',
|
|
label: 'Customer Support Prompt',
|
|
raw: 'You are a helpful customer support agent. Answer: {{question}}',
|
|
});
|
|
expect(result).toBe(sha256('Customer Support Prompt'));
|
|
});
|
|
|
|
it('handles prompt with only id and raw (common case)', () => {
|
|
const result = generateIdFromPrompt({
|
|
id: 'prompt-123',
|
|
raw: 'Answer the question: {{question}}',
|
|
});
|
|
expect(result).toBe(sha256('prompt-123'));
|
|
});
|
|
|
|
it('handles prompt with template variables in raw', () => {
|
|
const result = generateIdFromPrompt({
|
|
raw: 'Translate {{text}} to {{language}}',
|
|
});
|
|
expect(result).toBe(sha256('Translate {{text}} to {{language}}'));
|
|
});
|
|
|
|
it('handles prompt with multiline raw content', () => {
|
|
const multilineRaw = `You are a helpful assistant.
|
|
|
|
Please answer the following question:
|
|
{{question}}
|
|
|
|
Be concise and accurate.`;
|
|
const result = generateIdFromPrompt({ raw: multilineRaw });
|
|
expect(result).toBe(sha256(multilineRaw));
|
|
});
|
|
|
|
it('handles JSON-structured prompt', () => {
|
|
const jsonPrompt = {
|
|
raw: {
|
|
messages: [
|
|
{ role: 'system', content: 'You are helpful' },
|
|
{ role: 'user', content: '{{input}}' },
|
|
],
|
|
},
|
|
};
|
|
const result = generateIdFromPrompt(jsonPrompt);
|
|
expect(result).toBe(sha256(JSON.stringify(jsonPrompt.raw)));
|
|
});
|
|
});
|
|
});
|