0d3cb498a3
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
580 lines
19 KiB
TypeScript
580 lines
19 KiB
TypeScript
import './setup';
|
|
|
|
import { randomUUID } from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { expect, it, vi } from 'vitest';
|
|
import { FILE_METADATA_KEY } from '../../src/constants';
|
|
import { evaluate } from '../../src/evaluator';
|
|
import { runExtensionHook } from '../../src/evaluatorHelpers';
|
|
import Eval from '../../src/models/eval';
|
|
import { type ApiProvider, type EvaluateSummaryV3, type TestSuite } from '../../src/types/index';
|
|
import { mockApiProvider, toPrompt } from './helpers';
|
|
import { describeEvaluator } from './lifecycle';
|
|
|
|
describeEvaluator('evaluator metadata', () => {
|
|
it('evaluate with metadata passed to test transform', async () => {
|
|
const mockApiProviderWithMetadata: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider-metadata'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
metadata: { responseTime: 123, modelVersion: 'v1.0' },
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProviderWithMetadata],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
assert: [
|
|
{
|
|
type: 'equals',
|
|
value: 'Output: Test output, Metadata: {"responseTime":123,"modelVersion":"v1.0"}',
|
|
},
|
|
],
|
|
options: {
|
|
transform:
|
|
'context?.metadata ? `Output: ${output}, Metadata: ${JSON.stringify(context.metadata)}` : output',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = await evalRecord.toEvaluateSummary();
|
|
|
|
expect(mockApiProviderWithMetadata.callApi).toHaveBeenCalledTimes(1);
|
|
expect(summary.stats.successes).toBe(1);
|
|
expect(summary.stats.failures).toBe(0);
|
|
expect(summary.results[0].response?.output).toBe(
|
|
'Output: Test output, Metadata: {"responseTime":123,"modelVersion":"v1.0"}',
|
|
);
|
|
});
|
|
|
|
it('evaluate with metadata passed to test transform - no metadata case', async () => {
|
|
const mockApiProviderNoMetadata: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider-no-metadata'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProviderNoMetadata],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
assert: [
|
|
{
|
|
type: 'equals',
|
|
value: 'No metadata: Test output',
|
|
},
|
|
],
|
|
options: {
|
|
transform: 'context?.metadata ? `Has metadata: ${output}` : `No metadata: ${output}`',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = await evalRecord.toEvaluateSummary();
|
|
|
|
expect(mockApiProviderNoMetadata.callApi).toHaveBeenCalledTimes(1);
|
|
expect(summary.stats.successes).toBe(1);
|
|
expect(summary.stats.failures).toBe(0);
|
|
expect(summary.results[0].response?.output).toBe('No metadata: Test output');
|
|
});
|
|
|
|
it('evaluate with metadata passed to test transform - empty metadata', async () => {
|
|
const mockApiProviderEmptyMetadata: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider-empty-metadata'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
metadata: {},
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProviderEmptyMetadata],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
assert: [
|
|
{
|
|
type: 'equals',
|
|
value: 'Empty metadata: Test output',
|
|
},
|
|
],
|
|
options: {
|
|
transform:
|
|
'(context?.metadata && Object.keys(context.metadata).length === 0) ? `Empty metadata: ${output}` : output',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = await evalRecord.toEvaluateSummary();
|
|
|
|
expect(mockApiProviderEmptyMetadata.callApi).toHaveBeenCalledTimes(1);
|
|
expect(summary.stats.successes).toBe(1);
|
|
expect(summary.stats.failures).toBe(0);
|
|
expect(summary.results[0].response?.output).toBe('Empty metadata: Test output');
|
|
});
|
|
|
|
it('evaluate with metadata preserved alongside other context properties', async () => {
|
|
const mockApiProviderWithMetadata: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider-metadata-context'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
metadata: { modelInfo: 'gpt-4' },
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProviderWithMetadata],
|
|
prompts: [toPrompt('Test {{ var }}')],
|
|
tests: [
|
|
{
|
|
vars: { var: 'value' },
|
|
assert: [
|
|
{
|
|
type: 'equals',
|
|
value: 'All context: Test output',
|
|
},
|
|
],
|
|
options: {
|
|
transform:
|
|
'(Boolean(context?.vars) && Boolean(context?.prompt) && Boolean(context?.metadata)) ? `All context: ${output}` : `Missing context: ${output}`',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = await evalRecord.toEvaluateSummary();
|
|
|
|
expect(mockApiProviderWithMetadata.callApi).toHaveBeenCalledTimes(1);
|
|
expect(summary.stats.successes).toBe(1);
|
|
expect(summary.stats.failures).toBe(0);
|
|
expect(summary.results[0].response?.output).toBe('All context: Test output');
|
|
});
|
|
|
|
it('merges metadata correctly for regular tests', async () => {
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
defaultTest: {
|
|
metadata: { defaultKey: 'defaultValue' },
|
|
},
|
|
tests: [
|
|
{
|
|
metadata: { testKey: 'testValue' },
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const results = await evalRecord.getResults();
|
|
expect(results[0].testCase.metadata).toEqual({
|
|
defaultKey: 'defaultValue',
|
|
testKey: 'testValue',
|
|
});
|
|
});
|
|
|
|
it('merges response metadata with test metadata', async () => {
|
|
const mockProviderWithMetadata: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider-with-metadata'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
metadata: { responseKey: 'responseValue' },
|
|
}),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockProviderWithMetadata],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
metadata: { testKey: 'testValue' },
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const results = await evalRecord.getResults();
|
|
|
|
// Check that both test metadata and response metadata are present in the result
|
|
expect(results[0].metadata).toEqual({
|
|
testKey: 'testValue',
|
|
responseKey: 'responseValue',
|
|
[FILE_METADATA_KEY]: {},
|
|
});
|
|
});
|
|
|
|
it('evaluate with _conversation variable', async () => {
|
|
const mockApiProvider: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('test-provider'),
|
|
callApi: vi.fn().mockImplementation((prompt) =>
|
|
Promise.resolve({
|
|
output: prompt,
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('{{ var1 }} {{ _conversation[0].output }}')],
|
|
tests: [
|
|
{
|
|
vars: { var1: 'First run' },
|
|
},
|
|
{
|
|
vars: { var1: 'Second run' },
|
|
},
|
|
],
|
|
};
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = await evalRecord.toEvaluateSummary();
|
|
|
|
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(2);
|
|
expect(summary.stats.successes).toBe(2);
|
|
expect(summary.stats.failures).toBe(0);
|
|
expect(summary.results[0].response?.output).toBe('First run ');
|
|
expect(summary.results[1].response?.output).toBe('Second run First run ');
|
|
});
|
|
|
|
it('should maintain separate conversation histories based on metadata.conversationId', async () => {
|
|
const mockApiProvider = {
|
|
id: () => 'test-provider',
|
|
callApi: vi.fn().mockImplementation((_prompt) => ({
|
|
output: 'Test output',
|
|
})),
|
|
};
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [
|
|
{
|
|
raw: '{% for completion in _conversation %}User: {{ completion.input }}\nAssistant: {{ completion.output }}\n{% endfor %}User: {{ question }}',
|
|
label: 'Conversation test',
|
|
},
|
|
],
|
|
tests: [
|
|
// First conversation
|
|
{
|
|
vars: { question: 'Question 1A' },
|
|
metadata: { conversationId: 'conversation1' },
|
|
},
|
|
{
|
|
vars: { question: 'Question 1B' },
|
|
metadata: { conversationId: 'conversation1' },
|
|
},
|
|
// Second conversation
|
|
{
|
|
vars: { question: 'Question 2A' },
|
|
metadata: { conversationId: 'conversation2' },
|
|
},
|
|
{
|
|
vars: { question: 'Question 2B' },
|
|
metadata: { conversationId: 'conversation2' },
|
|
},
|
|
],
|
|
};
|
|
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
|
|
// Check that the API was called with the correct prompts
|
|
expect(mockApiProvider.callApi).toHaveBeenCalledTimes(4);
|
|
|
|
// First conversation, first question
|
|
expect(mockApiProvider.callApi).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.stringContaining('User: Question 1A'),
|
|
expect.anything(),
|
|
undefined,
|
|
);
|
|
|
|
// First conversation, second question (should include history)
|
|
expect(mockApiProvider.callApi).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.stringContaining('User: Question 1A\nAssistant: Test output\nUser: Question 1B'),
|
|
expect.anything(),
|
|
undefined,
|
|
);
|
|
|
|
// Second conversation, first question (should NOT include first conversation)
|
|
expect(mockApiProvider.callApi).toHaveBeenNthCalledWith(
|
|
3,
|
|
expect.stringContaining('User: Question 2A'),
|
|
expect.anything(),
|
|
undefined,
|
|
);
|
|
|
|
// Second conversation, second question (should only include second conversation history)
|
|
expect(mockApiProvider.callApi).toHaveBeenNthCalledWith(
|
|
4,
|
|
expect.stringContaining('User: Question 2A\nAssistant: Test output\nUser: Question 2B'),
|
|
expect.anything(),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('should include sessionId in metadata for afterEach hook', async () => {
|
|
const mockApiProvider = {
|
|
id: () => 'test-provider',
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
sessionId: 'test-session-123',
|
|
}),
|
|
};
|
|
|
|
const mockExtension = 'file://test-extension.js';
|
|
let capturedContext: any;
|
|
|
|
const mockedRunExtensionHook = vi.mocked(runExtensionHook);
|
|
mockedRunExtensionHook.mockImplementation(async (_extensions, hookName, context) => {
|
|
if (hookName === 'afterEach') {
|
|
capturedContext = context;
|
|
}
|
|
return context;
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
vars: { var1: 'value1' },
|
|
},
|
|
],
|
|
extensions: [mockExtension],
|
|
};
|
|
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
|
|
expect(capturedContext).toBeDefined();
|
|
expect(capturedContext.result.metadata.sessionId).toBe('test-session-123');
|
|
});
|
|
|
|
it('should include sessionIds array from test metadata for iterative providers', async () => {
|
|
const mockApiProvider = {
|
|
id: () => 'test-provider',
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
}),
|
|
};
|
|
|
|
const mockExtension = 'file://test-extension.js';
|
|
let capturedContext: any;
|
|
|
|
const mockedRunExtensionHook = vi.mocked(runExtensionHook);
|
|
mockedRunExtensionHook.mockImplementation(async (_extensions, hookName, context) => {
|
|
if (hookName === 'afterEach') {
|
|
capturedContext = context;
|
|
}
|
|
return context;
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
vars: { var1: 'value1' },
|
|
metadata: {
|
|
sessionIds: ['iter-session-1', 'iter-session-2', 'iter-session-3'],
|
|
},
|
|
},
|
|
],
|
|
extensions: [mockExtension],
|
|
};
|
|
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
|
|
expect(capturedContext).toBeDefined();
|
|
expect(capturedContext.result.metadata.sessionIds).toEqual([
|
|
'iter-session-1',
|
|
'iter-session-2',
|
|
'iter-session-3',
|
|
]);
|
|
expect(capturedContext.result.metadata.sessionId).toBeUndefined();
|
|
});
|
|
|
|
it('should persist afterEach hook namedScores, metadata, and response.metadata into result and metrics', async () => {
|
|
const mockExtension = 'file://test-extension.js:afterEach';
|
|
|
|
const mockedRunExtensionHook = vi.mocked(runExtensionHook);
|
|
mockedRunExtensionHook.mockImplementation(async (_extensions, hookName, context) => {
|
|
if (hookName === 'afterEach') {
|
|
const ctx = context as { test: any; result: any };
|
|
return {
|
|
...ctx,
|
|
result: {
|
|
...ctx.result,
|
|
namedScores: {
|
|
...ctx.result.namedScores,
|
|
hook_metric: 42,
|
|
},
|
|
metadata: {
|
|
...ctx.result.metadata,
|
|
hook_key: 'hook_value',
|
|
},
|
|
response: {
|
|
...ctx.result.response,
|
|
metadata: {
|
|
...ctx.result.response?.metadata,
|
|
session_url: 'https://example.com/session/123',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return context;
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
vars: {},
|
|
assert: [{ type: 'equals', value: 'Test output' }],
|
|
},
|
|
],
|
|
extensions: [mockExtension],
|
|
};
|
|
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = (await evalRecord.toEvaluateSummary()) as EvaluateSummaryV3;
|
|
|
|
// Verify hook's namedScores flowed into prompt metrics
|
|
expect(summary.prompts[0].metrics?.namedScores).toHaveProperty('hook_metric', 42);
|
|
|
|
// Verify hook's metadata and namedScores are in the persisted result
|
|
const result = summary.results[0];
|
|
expect(result.metadata).toHaveProperty('hook_key', 'hook_value');
|
|
expect(result.namedScores).toHaveProperty('hook_metric', 42);
|
|
expect(result.response?.metadata).toHaveProperty(
|
|
'session_url',
|
|
'https://example.com/session/123',
|
|
);
|
|
});
|
|
|
|
it('should persist row without hook modifications when afterEach hook throws', async () => {
|
|
const mockExtension = 'file://test-extension.js:afterEach';
|
|
|
|
const mockedRunExtensionHook = vi.mocked(runExtensionHook);
|
|
mockedRunExtensionHook.mockImplementation(async (_extensions, hookName, _context) => {
|
|
if (hookName === 'afterEach') {
|
|
throw new Error('Hook exploded');
|
|
}
|
|
return _context;
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [mockApiProvider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [
|
|
{
|
|
vars: {},
|
|
assert: [{ type: 'equals', value: 'Test output' }],
|
|
},
|
|
],
|
|
extensions: [mockExtension],
|
|
};
|
|
|
|
const evalRecord = await Eval.create({}, testSuite.prompts, { id: randomUUID() });
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = (await evalRecord.toEvaluateSummary()) as EvaluateSummaryV3;
|
|
|
|
// Row should still be persisted despite hook failure
|
|
expect(summary.results).toHaveLength(1);
|
|
expect(summary.results[0].success).toBe(true);
|
|
expect(summary.stats.successes).toBe(1);
|
|
});
|
|
|
|
it('drops stale provider headers when afterEach replaces response metadata', async () => {
|
|
const outputPath = path.join(os.tmpdir(), `promptfoo-evaluator-${randomUUID()}.jsonl`);
|
|
const mockExtension = 'file://test-extension.js:afterEach';
|
|
const provider: ApiProvider = {
|
|
id: vi.fn().mockReturnValue('metadata-provider'),
|
|
callApi: vi.fn().mockResolvedValue({
|
|
output: 'Test output',
|
|
metadata: {
|
|
headers: {
|
|
authorization: 'Bearer provider-secret',
|
|
'x-safe-debug': 'provider-debug',
|
|
},
|
|
},
|
|
tokenUsage: { total: 10, prompt: 5, completion: 5, cached: 0, numRequests: 1 },
|
|
}),
|
|
};
|
|
|
|
vi.mocked(runExtensionHook).mockImplementation(async (_extensions, hookName, context) => {
|
|
if (hookName !== 'afterEach') {
|
|
return context;
|
|
}
|
|
const ctx = context as { test: any; result: any };
|
|
return {
|
|
...ctx,
|
|
result: {
|
|
...ctx.result,
|
|
response: {
|
|
...ctx.result.response,
|
|
metadata: {
|
|
hook_key: 'hook_value',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
const testSuite: TestSuite = {
|
|
providers: [provider],
|
|
prompts: [toPrompt('Test prompt')],
|
|
tests: [{}],
|
|
extensions: [mockExtension],
|
|
};
|
|
|
|
try {
|
|
const evalRecord = await Eval.create({ outputPath }, testSuite.prompts, {
|
|
id: randomUUID(),
|
|
});
|
|
await evaluate(testSuite, evalRecord, {});
|
|
const summary = (await evalRecord.toEvaluateSummary()) as EvaluateSummaryV3;
|
|
const [result] = summary.results;
|
|
const [artifactResult] = fs
|
|
.readFileSync(outputPath, 'utf8')
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line));
|
|
|
|
// The hook replaced response.metadata, so the legacy top-level metadata.headers copied
|
|
// from the original transport is stale and must be dropped (not persisted).
|
|
expect(result.metadata?.headers).toBeUndefined();
|
|
expect(result.response?.metadata).toEqual({ hook_key: 'hook_value' });
|
|
expect(artifactResult.metadata.headers).toBeUndefined();
|
|
expect(artifactResult.response.metadata).toEqual({ hook_key: 'hook_value' });
|
|
expect(JSON.stringify({ artifactResult, result })).not.toContain('provider-secret');
|
|
} finally {
|
|
fs.rmSync(outputPath, { force: true });
|
|
}
|
|
});
|
|
});
|