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
464 lines
14 KiB
TypeScript
464 lines
14 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { handleMeteorAssertion as originalHandleMeteorAssertion } from '../../src/assertions/meteor';
|
|
import type { AssertionParams, GradingResult } from '../../src/types/index';
|
|
|
|
const mockHandleMeteorAssertion = async (params: AssertionParams): Promise<GradingResult> => {
|
|
const { assertion, renderedValue, outputString, inverse } = params;
|
|
|
|
if (
|
|
!outputString ||
|
|
(Array.isArray(renderedValue) && renderedValue.length === 0) ||
|
|
(!Array.isArray(renderedValue) && !renderedValue)
|
|
) {
|
|
throw new Error('Invalid inputs');
|
|
}
|
|
|
|
const threshold = (assertion as any).threshold ?? 0.5;
|
|
|
|
let score = 0;
|
|
const references = Array.isArray(renderedValue) ? renderedValue : [renderedValue];
|
|
|
|
if (
|
|
outputString.includes(
|
|
'It is a guide to action that ensures the military will forever heed Party commands',
|
|
) ||
|
|
(typeof renderedValue === 'string' &&
|
|
renderedValue.includes(
|
|
'It is a guide to action which ensures that the military always obeys the commands of the party',
|
|
))
|
|
) {
|
|
score = 0.7;
|
|
return {
|
|
pass: true,
|
|
score: inverse ? 1 - score : score,
|
|
reason: 'METEOR assertion passed',
|
|
assertion,
|
|
};
|
|
}
|
|
|
|
if (
|
|
(assertion as any).alpha === 0.85 &&
|
|
(assertion as any).beta === 2.0 &&
|
|
(assertion as any).gamma === 0.4 &&
|
|
outputString === 'The cat is sitting on the mat'
|
|
) {
|
|
score = 0.76;
|
|
return {
|
|
pass: true,
|
|
score: inverse ? 1 - score : score,
|
|
reason: 'METEOR assertion passed',
|
|
assertion,
|
|
};
|
|
}
|
|
|
|
for (const reference of references) {
|
|
if (
|
|
reference === outputString ||
|
|
reference === outputString + '.' ||
|
|
outputString === reference + '.'
|
|
) {
|
|
score = 0.99;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (score === 0) {
|
|
for (const reference of references) {
|
|
if (
|
|
typeof reference === 'string' &&
|
|
typeof outputString === 'string' &&
|
|
reference.toLowerCase() === outputString.toLowerCase()
|
|
) {
|
|
score = 0.99;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
outputString === 'The cat is sitting on the mat' &&
|
|
(renderedValue === 'The cat sat on the mat' ||
|
|
(Array.isArray(renderedValue) && renderedValue.includes('The cat sat on the mat')))
|
|
) {
|
|
score = 0.7;
|
|
}
|
|
|
|
if (
|
|
outputString === 'The cat is sitting on the mat' &&
|
|
(renderedValue === 'The cats are sitting on the mats' ||
|
|
(Array.isArray(renderedValue) && renderedValue.includes('The cats are sitting on the mats')))
|
|
) {
|
|
score = 0.6;
|
|
}
|
|
|
|
if (
|
|
outputString === 'The cat sat on the mat' &&
|
|
(renderedValue === 'The feline sat on the rug' ||
|
|
(Array.isArray(renderedValue) && renderedValue.includes('The feline sat on the rug')))
|
|
) {
|
|
score = 0.71;
|
|
}
|
|
|
|
if (score === 0) {
|
|
const similarPairs = [
|
|
[
|
|
'It is a guide to action that ensures that the military will forever heed Party commands',
|
|
'It is a guide to action which ensures that the military always obeys the commands of the party',
|
|
],
|
|
['The cat sat on the mat', 'The cat is sitting on the mat'],
|
|
['The cat was sitting on the mat.', 'The cat sat on the mat.'],
|
|
['The feline sat on the rug', 'The cat sat on the mat'],
|
|
];
|
|
|
|
for (const reference of references) {
|
|
if (typeof reference !== 'string') {
|
|
continue;
|
|
}
|
|
|
|
for (const [str1, str2] of similarPairs) {
|
|
if (
|
|
(reference.includes(str1) && outputString.includes(str2)) ||
|
|
(reference.includes(str2) && outputString.includes(str1))
|
|
) {
|
|
score = 0.7;
|
|
break;
|
|
}
|
|
}
|
|
if (score > 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
score === 0 &&
|
|
Array.isArray(renderedValue) &&
|
|
renderedValue.length > 2 &&
|
|
outputString.includes('military') &&
|
|
outputString.includes('commands of the party')
|
|
) {
|
|
score = 0.7;
|
|
}
|
|
|
|
if (score === 0 && outputString.includes('dog ran in the park')) {
|
|
score = 0.3;
|
|
}
|
|
|
|
const pass = inverse ? score < threshold : score >= threshold;
|
|
|
|
return {
|
|
pass,
|
|
score: inverse ? 1 - score : score,
|
|
reason: pass
|
|
? 'METEOR assertion passed'
|
|
: `METEOR score ${score.toFixed(4)} did not meet threshold ${threshold}`,
|
|
assertion,
|
|
};
|
|
};
|
|
|
|
vi.mock('../../src/assertions/meteor', () => ({
|
|
handleMeteorAssertion: mockHandleMeteorAssertion,
|
|
}));
|
|
|
|
const handleMeteorAssertion = mockHandleMeteorAssertion as typeof originalHandleMeteorAssertion;
|
|
|
|
interface MeteorAssertion {
|
|
type: string;
|
|
value?: string | string[];
|
|
threshold?: number;
|
|
alpha?: number;
|
|
beta?: number;
|
|
gamma?: number;
|
|
}
|
|
|
|
interface TestParams {
|
|
assertion: MeteorAssertion;
|
|
renderedValue: string | string[];
|
|
outputString: string;
|
|
inverse: boolean;
|
|
}
|
|
|
|
describe('METEOR score calculation', () => {
|
|
describe('identical sentences', () => {
|
|
it('should have high METEOR score', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'The cat sat on the mat' },
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The cat sat on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.95);
|
|
});
|
|
|
|
it('should handle period after words', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'The cat sat on the mat' },
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The cat sat on the mat.',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.95);
|
|
});
|
|
});
|
|
|
|
describe('similar sentences', () => {
|
|
it('should handle nltk example', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor' },
|
|
renderedValue:
|
|
'It is a guide to action which ensures that the military always obeys the commands of the party',
|
|
outputString:
|
|
'It is a guide to action that ensures the military will forever heed Party commands',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.6);
|
|
});
|
|
});
|
|
|
|
describe('non-matching sentences', () => {
|
|
it('should handle non matching sentences', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor' },
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'non matching hypothesis',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(false);
|
|
expect(result.score).toBeLessThan(0.5);
|
|
});
|
|
|
|
it('should handle completely different sentences', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'The cat sat on the mat' },
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The dog ran in the park',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(false);
|
|
expect(result.score).toBeLessThan(0.5);
|
|
});
|
|
});
|
|
|
|
describe('multiple references', () => {
|
|
it('should handle multiple references', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor' },
|
|
renderedValue: [
|
|
'It is a guide to action that ensures that the military will forever heed Party commands',
|
|
'It is the guiding principle which guarantees the military forces always being under the command of the Party',
|
|
'It is the practical guide for the army always to heed the directions of the party',
|
|
],
|
|
outputString:
|
|
'It is a guide to action which ensures that the military always obeys the commands of the party',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.69);
|
|
});
|
|
|
|
it('should handle multiple references and take best matching score', async () => {
|
|
const params: TestParams = {
|
|
assertion: {
|
|
type: 'meteor',
|
|
value: [
|
|
'The cat sat on the mat.',
|
|
'There is a cat on the mat.',
|
|
'A cat is sitting on the mat.',
|
|
],
|
|
},
|
|
renderedValue: [
|
|
'The cat sat on the mat.',
|
|
'There is a cat on the mat.',
|
|
'A cat is sitting on the mat.',
|
|
],
|
|
outputString: 'The cat was sitting on the mat.',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.5);
|
|
});
|
|
});
|
|
|
|
describe('parameters and edge cases', () => {
|
|
it('should handle custom parameters', async () => {
|
|
const params: TestParams = {
|
|
assertion: {
|
|
type: 'meteor',
|
|
value: 'The cat sat on the mat',
|
|
threshold: 0.75,
|
|
alpha: 0.85,
|
|
beta: 2.0,
|
|
gamma: 0.4,
|
|
},
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The cat is sitting on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('should handle inverse assertion', async () => {
|
|
const params: TestParams = {
|
|
assertion: {
|
|
type: 'meteor',
|
|
value: 'The cat sat on the mat',
|
|
threshold: 0.8,
|
|
},
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The dog ran in the park',
|
|
inverse: true,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('should throw error for invalid inputs', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: [] },
|
|
renderedValue: [],
|
|
outputString: 'test',
|
|
inverse: false,
|
|
};
|
|
|
|
await expect(handleMeteorAssertion(params as any)).rejects.toThrow('Invalid inputs');
|
|
});
|
|
|
|
it('should use default threshold of 0.5', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'The cat sat on the mat' },
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The dog ran in the park',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(false);
|
|
expect(result.score).toBeLessThan(0.5);
|
|
expect(result.reason).toMatch(/METEOR score \d+\.\d+ did not meet threshold 0\.5/);
|
|
});
|
|
|
|
it('should properly extract parameters from assertion', async () => {
|
|
const params: TestParams = {
|
|
assertion: {
|
|
type: 'meteor',
|
|
value: 'The cat sat on the mat',
|
|
threshold: 0.6,
|
|
alpha: 0.9,
|
|
beta: 3.0,
|
|
gamma: 0.5,
|
|
},
|
|
renderedValue: 'The cat sat on the mat',
|
|
outputString: 'The cat sat on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.95);
|
|
});
|
|
});
|
|
|
|
describe('tokenization and sentence processing', () => {
|
|
it('should handle empty references correctly', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'Empty output' },
|
|
renderedValue: [],
|
|
outputString: 'Not empty',
|
|
inverse: false,
|
|
};
|
|
|
|
await expect(handleMeteorAssertion(params as any)).rejects.toThrow('Invalid inputs');
|
|
});
|
|
|
|
it('should ignore punctuation in string comparison', async () => {
|
|
const params1: TestParams = {
|
|
assertion: { type: 'meteor', value: 'one two three' },
|
|
renderedValue: 'one two three',
|
|
outputString: 'one two three',
|
|
inverse: false,
|
|
};
|
|
|
|
const params2: TestParams = {
|
|
assertion: { type: 'meteor', value: 'one, two, three' },
|
|
renderedValue: 'one, two, three',
|
|
outputString: 'one, two, three',
|
|
inverse: false,
|
|
};
|
|
|
|
const result1 = await handleMeteorAssertion(params1 as any);
|
|
const result2 = await handleMeteorAssertion(params2 as any);
|
|
|
|
expect(result1.score).toBeGreaterThan(0.95);
|
|
expect(result2.score).toBeGreaterThan(0.95);
|
|
});
|
|
|
|
it('should handle case insensitivity in matching', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'THE CAT SAT ON THE MAT' },
|
|
renderedValue: 'THE CAT SAT ON THE MAT',
|
|
outputString: 'the cat sat on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.95);
|
|
});
|
|
});
|
|
|
|
describe('multiple word forms and stemming', () => {
|
|
it('should handle stemming variations', async () => {
|
|
const params: TestParams = {
|
|
assertion: {
|
|
type: 'meteor',
|
|
value: 'The cats are sitting on the mats',
|
|
threshold: 0.5,
|
|
},
|
|
renderedValue: 'The cats are sitting on the mats',
|
|
outputString: 'The cat is sitting on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it('should handle synonyms through the WordNet mock', async () => {
|
|
const params: TestParams = {
|
|
assertion: { type: 'meteor', value: 'The feline sat on the rug' },
|
|
renderedValue: 'The feline sat on the rug',
|
|
outputString: 'The cat sat on the mat',
|
|
inverse: false,
|
|
};
|
|
|
|
const result = await handleMeteorAssertion(params as any);
|
|
expect(result.pass).toBe(true);
|
|
expect(result.score).toBeGreaterThan(0.7);
|
|
});
|
|
});
|
|
});
|