Files
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
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

829 lines
28 KiB
TypeScript

import dedent from 'dedent';
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { ResultFailureReason } from '../../src/types/index';
import {
convertSlashCommentsToHash,
extractFirstJsonObject,
extractJsonObjects,
getAjv,
isValidJson,
orderKeys,
resetAjv,
safeJsonStringify,
summarizeEvaluateResultForLogging,
} from '../../src/util/json';
import { createEvaluateResult } from '../factories/eval';
import { createAtomicTestCase, createPrompt } from '../factories/testSuite';
import { mockProcessEnv } from './utils';
import type { EvaluateResult } from '../../src/types/index';
describe('json utilities', () => {
describe('getAjv', () => {
beforeAll(() => {
mockProcessEnv({ NODE_ENV: 'test' });
});
beforeEach(() => {
mockProcessEnv({ PROMPTFOO_DISABLE_AJV_STRICT_MODE: undefined });
resetAjv();
});
afterEach(() => {
mockProcessEnv({ PROMPTFOO_DISABLE_AJV_STRICT_MODE: undefined });
});
it('should create an Ajv instance with default options', () => {
const ajv = getAjv();
expect(ajv).toBeDefined();
expect(ajv.opts.strictSchema).toBe(true);
});
it('should disable strict mode when PROMPTFOO_DISABLE_AJV_STRICT_MODE is set', () => {
mockProcessEnv({ PROMPTFOO_DISABLE_AJV_STRICT_MODE: 'true' });
const ajv = getAjv();
expect(ajv.opts.strictSchema).toBe(false);
});
it('should add formats to the Ajv instance', () => {
const ajv = getAjv();
expect(ajv.formats).toBeDefined();
expect(Object.keys(ajv.formats)).not.toHaveLength(0);
});
it('should reuse the same instance on subsequent calls', () => {
const firstInstance = getAjv();
const secondInstance = getAjv();
expect(firstInstance).toBe(secondInstance);
});
it('should reset the instance when resetAjv is called', () => {
const firstInstance = getAjv();
resetAjv();
const secondInstance = getAjv();
expect(firstInstance).not.toBe(secondInstance);
});
it('should only allow resetAjv to be called in test environment', () => {
const originalNodeEnv = process.env.NODE_ENV;
try {
mockProcessEnv({ NODE_ENV: 'production' });
expect(() => resetAjv()).toThrow('resetAjv can only be called in test environment');
} finally {
mockProcessEnv({ NODE_ENV: originalNodeEnv });
}
});
});
describe('isValidJson', () => {
it('returns true for valid JSON', () => {
expect(isValidJson('{"key": "value"}')).toBe(true);
expect(isValidJson('[1, 2, 3]')).toBe(true);
expect(isValidJson('"string"')).toBe(true);
expect(isValidJson('123')).toBe(true);
expect(isValidJson('true')).toBe(true);
expect(isValidJson('null')).toBe(true);
});
it('returns false for invalid JSON', () => {
expect(isValidJson('{')).toBe(false);
expect(isValidJson('["unclosed array"')).toBe(false);
expect(isValidJson('{"key": value}')).toBe(false);
expect(isValidJson('undefined')).toBe(false);
});
});
describe('safeJsonStringify', () => {
it('stringifies simple objects', () => {
const obj: { key: string; number: number } = { key: 'value', number: 123 };
expect(safeJsonStringify(obj)).toBe('{"key":"value","number":123}');
});
it('handles circular references', () => {
const obj: { key: string; circular?: any } = { key: 'value' };
obj.circular = obj;
expect(safeJsonStringify(obj)).toBe('{"key":"value"}');
});
it('pretty prints when specified', () => {
const obj: { key: string; nested: { inner: string } } = {
key: 'value',
nested: { inner: 'content' },
};
const expected = dedent`
{
"key": "value",
"nested": {
"inner": "content"
}
}`;
expect(safeJsonStringify(obj, true)).toBe(expected);
});
it('handles null values', () => {
expect(safeJsonStringify(null)).toBe('null');
});
it('handles undefined values', () => {
expect(safeJsonStringify(undefined)).toBeUndefined();
});
it('returns undefined or strips non-serializable values', () => {
expect(safeJsonStringify(() => {})).toBeUndefined(); // Function
expect(safeJsonStringify(Symbol('sym'))).toBeUndefined(); // Symbol
expect(safeJsonStringify({ key: 'value' })).toBe('{"key":"value"}');
});
it('returns undefined for circular references in arrays', () => {
const arr: (number | any[])[] = [1, 2, 3];
arr.push(arr);
expect(safeJsonStringify(arr)).toBe('[1,2,3,null]');
});
it('handles complex nested structures', () => {
const complex: {
string: string;
number: number;
boolean: boolean;
null: null;
array: (number | string | { three: number })[];
nested: { a: number; b: number[] };
} = {
string: 'value',
number: 123,
boolean: true,
null: null,
array: [1, 'two', { three: 3 }],
nested: {
a: 1,
b: [2, 3],
},
};
const result = safeJsonStringify(complex);
expect(result).toBeDefined(); // Ensure it returns a string
expect(JSON.parse(result as string)).toEqual(complex); // Ensure it matches the original structure
});
it('handles arrays with circular references', () => {
const arr: any[] = [1, 2, 3];
arr.push(arr);
expect(safeJsonStringify(arr)).toBe('[1,2,3,null]');
});
it('handles nested circular references', () => {
const obj: any = { a: { b: {} } };
obj.a.b.c = obj.a;
expect(safeJsonStringify(obj)).toBe('{"a":{"b":{}}}');
});
it('preserves shared non-circular references', () => {
const shared = { value: 'shared' };
expect(safeJsonStringify({ a: shared, b: shared, c: [shared] })).toBe(
'{"a":{"value":"shared"},"b":{"value":"shared"},"c":[{"value":"shared"}]}',
);
});
it('preserves non-circular nested structures', () => {
const nested = { a: { b: { c: 1 } }, d: [1, 2, { e: 3 }] };
expect(JSON.parse(safeJsonStringify(nested) as string)).toEqual(nested);
});
});
describe('extractJsonObjects', () => {
it('should extract a single JSON object from a string', () => {
const input = '{"key": "value"}';
const expectedOutput = [{ key: 'value' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should extract multiple JSON objects from a string', () => {
const input = 'yolo {"key1": "value1"} some text {"key2": "value2"} fomo';
const expectedOutput = [{ key1: 'value1' }, { key2: 'value2' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should return an empty array if no JSON objects are found', () => {
const input = 'no json here';
const expectedOutput: any[] = [];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle nested JSON objects', () => {
const input = 'wassup {"outer": {"inner": "value"}, "foo": [1,2,3,4]}';
const expectedOutput = [{ outer: { inner: 'value' }, foo: [1, 2, 3, 4] }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle comments', () => {
const input = `{
"reason": "all good",
"score": 1.0 // perfect score
}`;
const expectedOutput = [{ reason: 'all good', score: 1.0 }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle jsonl', () => {
const input = `{"reason": "hello", "score": 1.0}
{"reason": "world", "score": 0.0}`;
const expectedOutput = [
{ reason: 'hello', score: 1.0 },
{ reason: 'world', score: 0.0 },
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle invalid JSON gracefully', () => {
const input = '{"key": "value" some text {"key2": "value2"}';
const expectedOutput = [{ key2: 'value2' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle incomplete JSON', () => {
const input = `{
"incomplete": "object"`;
expect(extractJsonObjects(input)).toEqual([
{
incomplete: 'object',
},
]);
});
it('should handle string containing incomplete JSON', () => {
const input = dedent`{
"key1": "value1",
"key2": {
"nested": "value2"
},
"key3": "value3"
}
{
"incomplete": "object"`;
expect(extractJsonObjects(input)).toEqual([
{
key1: 'value1',
key2: {
nested: 'value2',
},
key3: 'value3',
},
{
incomplete: 'object',
},
]);
});
it('should handle this case', () => {
const obj = {
vars: [
{
language: 'Klingon',
body: 'Live long and prosper',
},
{
language: 'Elvish',
body: 'Good morning',
},
{
language: 'Esperanto',
body: 'I love learning languages',
},
{
language: 'Morse Code',
body: 'Help',
},
{
language: 'Emoji',
body: 'I am feeling happy 😊',
},
{
language: 'Binary',
body: 'Yes',
},
{
language: 'Javascript',
body: 'Hello, World!',
},
{
language: 'Shakespearean',
body: 'To be or not to be',
},
{
language: 'Leet Speak',
body: 'You are amazing',
},
{
language: 'Old English',
body: 'What is thy name?',
},
{
language: 'Yoda Speak',
body: 'Strong with the force, you are',
},
],
};
const input = JSON.stringify(obj);
expect(extractJsonObjects(input)).toEqual([obj]);
});
it('should handle YAML-style unquoted strings', () => {
const input = '{key: value, another_key: another value}';
const expectedOutput = [{ key: 'value', another_key: 'another value' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
describe('convertSlashCommentsToHash', () => {
it('should convert basic // comments to # comments', () => {
const input = 'some text // this is a comment';
const expected = 'some text # this is a comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should not convert // inside double quoted strings', () => {
const input = '"this // is not a comment" // but this is';
const expected = '"this // is not a comment" # but this is';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should not convert // inside single quoted strings', () => {
const input = "'don't convert // here' // convert here";
const expected = "'don't convert // here' # convert here";
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle escaped quotes in strings', () => {
const input = '"escaped \\" quote // not a comment" // real comment';
const expected = '"escaped \\" quote // not a comment" # real comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle multiple comments in one line', () => {
const input = 'text // first comment // not a second comment';
const expected = 'text # first comment // not a second comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle comments at the start of the line', () => {
const input = '// comment at start\ntext // comment at end';
const expected = '# comment at start\ntext # comment at end';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle strings with multiple lines', () => {
const input = dedent`
line1 // comment 1
"string with // not a comment"
line3 // comment 2`;
const expected = dedent`
line1 # comment 1
"string with // not a comment"
line3 # comment 2`;
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle nested quotes', () => {
const input = '"outer \\"inner // not comment\\" still outer" // real comment';
const expected = '"outer \\"inner // not comment\\" still outer" # real comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle mixed single and double quotes', () => {
const input = '"contains \'nested\' // not comment" // real comment';
const expected = '"contains \'nested\' // not comment" # real comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle empty strings', () => {
expect(convertSlashCommentsToHash('')).toBe('');
});
it('should handle strings without comments', () => {
const input = 'no comments here';
expect(convertSlashCommentsToHash(input)).toBe(input);
});
it('should handle strings with only a comment', () => {
const input = '// just a comment';
const expected = '# just a comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle multiple sequential comment markers', () => {
const input = 'text //// double comment';
const expected = 'text ## double comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle JSON-like structures', () => {
const input =
'{\n "key": "value", // comment here\n "key2": "value2" // another comment\n}';
const expected =
'{\n "key": "value", # comment here\n "key2": "value2" # another comment\n}';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
it('should handle comments after different types of values', () => {
const inputs = [
'null // after null',
'true // after boolean',
'42 // after number',
'"string" // after string',
'[] // after array',
'{} // after object',
];
const expected = inputs.map((input) => input.replace('//', '#'));
inputs.forEach((input, i) => {
expect(convertSlashCommentsToHash(input)).toBe(expected[i]);
});
});
it('should not treat URL schemes as comments', () => {
const inputs = [
'url: http://example.com/path',
'url: https://example.com/path',
'url: http://example.com//double-slash',
];
inputs.forEach((input) => {
expect(convertSlashCommentsToHash(input)).toBe(input);
});
});
it('should still convert comments after URLs', () => {
const input = 'url: http://example.com/path // trailing comment';
const expected = 'url: http://example.com/path # trailing comment';
expect(convertSlashCommentsToHash(input)).toBe(expected);
});
});
it('should skip non-object YAML values', () => {
const input = 'plain string {key: value} 42 {another: obj} true';
const expectedOutput = [{ key: 'value' }, { another: 'obj' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle YAML with mixed quoted and unquoted strings', () => {
const input = '{unquoted: value, "quoted": "value", another: "mixed"}';
const expectedOutput = [{ unquoted: 'value', quoted: 'value', another: 'mixed' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should ignore YAML arrays and scalars', () => {
const input = '[1, 2, 3] {obj: value} plain: string {another: obj}';
const expectedOutput = [{ obj: 'value' }, { another: 'obj' }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle YAML with special characters', () => {
const input = '{key: value with spaces, special: value!@#$%, empty:}';
const expectedOutput = [{ key: 'value with spaces', special: 'value!@#$%', empty: null }];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
describe('LLM output scenarios', () => {
it('should handle JSON with inline comments', () => {
const input = dedent`
{
"reason": "The test passed",
"score": 1.0 # The score is perfect because all criteria were met
}`;
const expectedOutput = [
{
reason: 'The test passed',
score: 1.0,
},
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle JSON with multiline string containing markdown', () => {
const input = dedent`
{
"reason": "The summary provided is mostly accurate but contains some omissions:
- Point 1: Details here
- Point 2: More details
Some additional context...",
"score": 0.75
}`;
const expectedOutput = [
{
reason:
'The summary provided is mostly accurate but contains some omissions:\n- Point 1: Details here - Point 2: More details\nSome additional context...',
score: 0.75,
},
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle JSON with unescaped backticks', () => {
const input = dedent`
{
"reason": "\`\`The summary accurately reflects...",
"score": 1.0
}`;
const expectedOutput = [
{
reason: '``The summary accurately reflects...',
score: 1.0,
},
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle JSON with trailing commas', () => {
const input = dedent`
{
"key1": "value1",
"key2": "value2",
}`;
const expectedOutput = [
{
key1: 'value1',
key2: 'value2',
},
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
it('should handle multiple JSON objects in LLM output with text in between', () => {
const input = dedent`
Here's my analysis:
{
"first_point": "valid observation",
"score": 0.8
}
And another point:
{
"second_point": "another observation",
"score": 0.9
}`;
const expectedOutput = [
{
first_point: 'valid observation',
score: 0.8,
},
{
second_point: 'another observation',
score: 0.9,
},
];
expect(extractJsonObjects(input)).toEqual(expectedOutput);
});
});
});
describe('orderKeys', () => {
it('orders keys according to specified order', () => {
const obj = { c: 3, a: 1, b: 2 };
const order: (keyof typeof obj)[] = ['a', 'b', 'c'];
const result = orderKeys(obj, order);
expect(Object.keys(result)).toEqual(['a', 'b', 'c']);
});
it('places unspecified keys at the end', () => {
const obj = { d: 4, b: 2, a: 1, c: 3 };
const order: (keyof typeof obj)[] = ['a', 'b'];
const result = orderKeys(obj, order);
expect(Object.keys(result)).toEqual(['a', 'b', 'd', 'c']);
});
it('ignores specified keys that do not exist in the object', () => {
const obj = { a: 1, c: 3 };
const order = ['a', 'b', 'c', 'd'] as (keyof typeof obj)[];
const result = orderKeys(obj, order);
expect(Object.keys(result)).toEqual(['a', 'c']);
});
it('returns an empty object when input is empty', () => {
const obj = {};
const order = ['a', 'b', 'c'] as (keyof typeof obj)[];
const result = orderKeys(obj, order);
expect(result).toEqual({});
});
it('returns the original object when order is empty', () => {
const obj = { c: 3, a: 1, b: 2 };
const order: (keyof typeof obj)[] = [];
const result = orderKeys(obj, order);
expect(result).toEqual(obj);
});
it('preserves nested object structures', () => {
const obj = { c: { x: 1 }, a: [1, 2], b: 2 };
const order: (keyof typeof obj)[] = ['a', 'b', 'c'];
const result = orderKeys(obj, order);
expect(result).toEqual({ a: [1, 2], b: 2, c: { x: 1 } });
});
it('handles objects with symbol keys', () => {
const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');
const obj = { [sym1]: 1, b: 2, [sym2]: 3, a: 4 };
const order: (keyof typeof obj)[] = ['a', 'b'];
const result = orderKeys(obj, order);
expect(Object.getOwnPropertySymbols(result)).toEqual([sym1, sym2]);
expect(Object.keys(result)).toEqual(['a', 'b']);
});
it('maintains the correct types for keys and values', () => {
const obj = { a: 'string', b: 42, c: true, d: null, e: undefined };
const order: (keyof typeof obj)[] = ['b', 'a', 'c', 'd', 'e'];
const result = orderKeys(obj, order);
expect(result).toEqual({ b: 42, a: 'string', c: true, d: null, e: undefined });
});
});
describe('extractFirstJsonObject', () => {
it('should extract the first JSON object from a string', () => {
const input = '{"key1": "value1"} {"key2": "value2"}';
const expected = { key1: 'value1' };
expect(extractFirstJsonObject(input)).toEqual(expected);
});
it('should throw an error when no JSON objects are found', () => {
const input = 'no json here';
expect(() => extractFirstJsonObject(input)).toThrow(
'Expected a JSON object, but got "no json here"',
);
});
});
describe('summarizeEvaluateResultForLogging', () => {
// Create test EvaluateResult with reasonable data size
const createTestEvaluateResult = (): EvaluateResult =>
createEvaluateResult({
id: 'test-eval-result',
success: false,
score: 0.5,
error: 'Test error',
failureReason: ResultFailureReason.ERROR,
latencyMs: 100,
promptId: 'test-prompt-id',
provider: { id: 'test-provider', label: 'Test Provider' },
response: {
output: 'This is a test output that is reasonably sized for testing purposes.',
error: undefined,
cached: false,
cost: 0.01,
tokenUsage: { total: 1000, prompt: 500, completion: 500 },
metadata: {
model: 'gpt-4',
timestamp: '2024-01-01T00:00:00Z',
additionalData: 'Some additional metadata for testing',
},
},
testCase: createAtomicTestCase({
description: 'Test case with regular data',
vars: { input: 'test input', context: 'test context' },
}),
prompt: createPrompt('Test prompt', {
display: 'Test Prompt Display',
label: 'Test Prompt Label',
}),
vars: {
userInput: 'test user input',
systemPrompt: 'test system prompt',
},
});
it('should throw TypeError for null or undefined input', () => {
expect(() => summarizeEvaluateResultForLogging(null as any)).toThrow(TypeError);
expect(() => summarizeEvaluateResultForLogging(undefined as any)).toThrow(TypeError);
});
it('should create a safe summary of evaluation results', () => {
const testResult = createTestEvaluateResult();
const summary = summarizeEvaluateResultForLogging(testResult);
expect(summary.id).toBe('test-eval-result');
expect(summary.testIdx).toBe(0);
expect(summary.promptIdx).toBe(0);
expect(summary.success).toBe(false);
expect(summary.score).toBe(0.5);
expect(summary.error).toBe('Test error');
expect(summary.failureReason).toBe(ResultFailureReason.ERROR);
expect(summary.provider?.id).toBe('test-provider');
expect(summary.provider?.label).toBe('Test Provider');
expect(summary.response?.output).toBeDefined();
expect(summary.response?.cached).toBe(false);
expect(summary.response?.cost).toBe(0.01);
expect(summary.response?.tokenUsage).toEqual({
total: 1000,
prompt: 500,
completion: 500,
});
expect(summary.response?.metadata?.keys).toContain('model');
expect(summary.response?.metadata?.keys).toContain('timestamp');
expect(summary.response?.metadata?.keys).toContain('additionalData');
expect(summary.response?.metadata?.keyCount).toBe(3);
expect(summary.testCase?.description).toBe('Test case with regular data');
expect(summary.testCase?.vars).toEqual(['input', 'context']);
});
it('should handle custom options', () => {
const testResult = createTestEvaluateResult();
const summary = summarizeEvaluateResultForLogging(testResult, 20, false);
expect(summary.response?.output?.length).toBeLessThanOrEqual(35); // 20 + '...[truncated]'
expect(summary.response?.metadata).toBeUndefined();
});
it('should handle EvaluateResult with minimal data', () => {
const minimalResult: EvaluateResult = createEvaluateResult({
id: 'minimal-result',
testIdx: 1,
promptIdx: 2,
score: 1.0,
latencyMs: 50,
promptId: 'minimal-prompt-id',
provider: { id: 'minimal-provider' },
prompt: createPrompt('test prompt', { label: 'Test Prompt' }),
});
const summary = summarizeEvaluateResultForLogging(minimalResult);
expect(summary.id).toBe('minimal-result');
expect(summary.success).toBe(true);
expect(summary.score).toBe(1.0);
expect(summary.provider?.id).toBe('minimal-provider');
});
it('should handle long output strings by truncating them', () => {
const longOutput = 'A'.repeat(1000);
const resultWithLongOutput: EvaluateResult = {
...createTestEvaluateResult(),
response: {
output: longOutput,
cached: false,
},
};
const summary = summarizeEvaluateResultForLogging(resultWithLongOutput, 100);
expect(summary.response?.output?.length).toBeLessThanOrEqual(115); // 100 + '...[truncated]'
expect(summary.response?.output).toContain('...[truncated]');
});
it('should be safely stringifiable', () => {
const testResult = createTestEvaluateResult();
const summary = summarizeEvaluateResultForLogging(testResult);
expect(() => {
const result = safeJsonStringify(summary);
expect(result).toBeDefined();
expect(typeof result).toBe('string');
}).not.toThrow();
});
it('should handle non-string output values', () => {
const resultWithObjectOutput: EvaluateResult = {
...createTestEvaluateResult(),
response: {
output: { complexObject: 'value', nested: { data: 'test' } },
cached: false,
},
};
const summary = summarizeEvaluateResultForLogging(resultWithObjectOutput);
expect(summary.response?.output).toBe('[object Object]');
});
describe('Integration test - evaluator error logging scenario', () => {
it('should handle the exact scenario from evaluator.ts without throwing', () => {
const testEvalResult = createTestEvaluateResult();
const mockError = new Error('Database connection failed');
expect(() => {
const resultSummary = summarizeEvaluateResultForLogging(testEvalResult);
const logMessage = `Error saving result: ${mockError} ${safeJsonStringify(resultSummary)}`;
expect(logMessage).toBeDefined();
expect(logMessage).toContain('Error saving result: Error: Database connection failed');
expect(logMessage).toContain('"id":"test-eval-result"');
expect(logMessage).toContain('"success":false');
expect(logMessage.length).toBeLessThan(5000);
}).not.toThrow();
});
it('should perform efficiently', () => {
const testResult = createTestEvaluateResult();
const start = Date.now();
const summary = summarizeEvaluateResultForLogging(testResult);
const stringified = safeJsonStringify(summary);
const duration = Date.now() - start;
expect(duration).toBeLessThan(100);
expect(stringified).toBeDefined();
});
});
});
});