Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

737 lines
23 KiB
TypeScript

/**
* Tests for LangSmith mode runner.
*
* These tests mock the LangSmith evaluate() function to verify:
* - Target function does all work (generation + evaluation)
* - Evaluator just extracts pre-computed feedback
* - Dataset context extraction is respected
* - Filters trigger dataset example preloading
*/
import { mock } from 'jest-mock-extended';
import type { Client } from 'langsmith/client';
import { evaluate as langsmithEvaluate } from 'langsmith/evaluation';
import type { Dataset, Example } from 'langsmith/schemas';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import type { SimpleWorkflow } from '@/types/workflow';
import type { Evaluator, Feedback, RunConfig } from '../harness/harness-types';
import { createLogger } from '../harness/logger';
const silentLogger = createLogger(false);
jest.mock('langsmith/evaluation', () => ({
evaluate: jest.fn().mockResolvedValue({ experimentName: 'test-experiment' }),
}));
jest.mock('langsmith/traceable', () => ({
traceable: jest.fn(
<T extends (...args: unknown[]) => unknown>(fn: T, _options: unknown): T => fn,
),
}));
function createMockWorkflow(name = 'Test Workflow'): SimpleWorkflow {
return { name, nodes: [], connections: {} };
}
function createMockEvaluator(
name: string,
feedback: Feedback[] = [{ evaluator: name, metric: 'score', score: 1, kind: 'score' }],
): Evaluator {
return {
name,
evaluate: jest.fn().mockResolvedValue(feedback),
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isCallable(value: unknown): value is (...args: unknown[]) => unknown {
return typeof value === 'function';
}
type LangsmithTargetOutput = {
workflow: SimpleWorkflow;
prompt: string;
feedback: Feedback[];
};
function isSimpleWorkflow(value: unknown): value is SimpleWorkflow {
return isRecord(value) && Array.isArray(value.nodes) && isRecord(value.connections);
}
function isFeedback(value: unknown): value is Feedback {
return (
isRecord(value) &&
typeof value.evaluator === 'string' &&
typeof value.metric === 'string' &&
typeof value.score === 'number' &&
(value.kind === 'score' || value.kind === 'metric' || value.kind === 'detail')
);
}
function isLangsmithTargetOutput(value: unknown): value is LangsmithTargetOutput {
return (
isRecord(value) &&
isSimpleWorkflow(value.workflow) &&
typeof value.prompt === 'string' &&
Array.isArray(value.feedback) &&
value.feedback.every(isFeedback)
);
}
async function callLangsmithTarget(target: unknown, inputs: unknown): Promise<unknown> {
if (isCallable(target)) return await target(inputs);
if (isRecord(target) && isCallable(target.invoke)) return await target.invoke(inputs);
throw new Error('Expected LangSmith target to be callable');
}
function createMockLangsmithClient() {
const lsClient = mock<Client>();
lsClient.readDataset.mockResolvedValue(mock<Dataset>({ id: 'test-dataset-id' }));
lsClient.listExamples.mockReturnValue((async function* () {})());
lsClient.awaitPendingTraceBatches.mockResolvedValue(undefined);
return lsClient;
}
describe('Runner - LangSmith Mode', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('runEvaluation() with LangSmith', () => {
it('should call langsmith evaluate() with correct options', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const config: RunConfig = {
mode: 'langsmith',
dataset: 'my-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test-experiment',
repetitions: 2,
concurrency: 4,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [_target, options] = mockEvaluate.mock.calls[0];
expect(options).toEqual(
expect.objectContaining({
data: 'my-dataset',
experimentPrefix: 'test-experiment',
numRepetitions: 2,
maxConcurrency: 4,
client: lsClient,
}),
);
});
it('should create target function that generates workflow and runs evaluators', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const workflow = createMockWorkflow('Generated');
const generateWorkflow = jest.fn().mockResolvedValue(workflow);
const evaluator = createMockEvaluator('test', [
{ evaluator: 'test', metric: 'score', score: 0.9, kind: 'score' },
]);
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow,
evaluators: [evaluator],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
const result = await callLangsmithTarget(target, { prompt: 'Create a workflow' });
expect(isLangsmithTargetOutput(result)).toBe(true);
if (!isLangsmithTargetOutput(result)) throw new Error('Expected LangSmith target output');
// Collectors are passed explicitly from the traceable wrapper to capture token usage and subgraph metrics
expect(generateWorkflow).toHaveBeenCalledWith(
'Create a workflow',
expect.objectContaining({
tokenUsage: expect.any(Function),
subgraphMetrics: expect.any(Function),
}),
);
expect(evaluator.evaluate).toHaveBeenCalledWith(
workflow,
expect.objectContaining({ prompt: 'Create a workflow' }),
);
expect(result).toEqual({
workflow,
prompt: 'Create a workflow',
feedback: [{ evaluator: 'test', metric: 'score', score: 0.9, kind: 'score' }],
});
});
it('should write artifacts when outputDir is provided', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'v2-evals-langsmith-out-'));
try {
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
outputDir: tempDir,
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow('Generated')),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
await callLangsmithTarget(target, { prompt: 'Create a workflow' });
const entries = fs.readdirSync(tempDir, { withFileTypes: true });
const exampleDir = entries.find(
(e) => e.isDirectory() && e.name.startsWith('example-001-'),
)?.name;
expect(exampleDir).toBeDefined();
expect(fs.existsSync(path.join(tempDir, exampleDir!, 'prompt.txt'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, exampleDir!, 'workflow.json'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, exampleDir!, 'feedback.json'))).toBe(true);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it('should aggregate feedback from multiple evaluators in target', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const evaluator1 = createMockEvaluator('e1', [
{ evaluator: 'e1', metric: 'score', score: 0.8, kind: 'score' },
]);
const evaluator2 = createMockEvaluator('e2', [
{ evaluator: 'e2', metric: 'a', score: 0.9, kind: 'metric' },
{ evaluator: 'e2', metric: 'b', score: 1.0, kind: 'metric' },
]);
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [evaluator1, evaluator2],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
const result = await callLangsmithTarget(target, { prompt: 'Test' });
expect(isLangsmithTargetOutput(result)).toBe(true);
if (!isLangsmithTargetOutput(result)) throw new Error('Expected LangSmith target output');
expect(result.feedback).toHaveLength(3);
expect(result.feedback).toContainEqual({
evaluator: 'e1',
metric: 'score',
score: 0.8,
kind: 'score',
});
expect(result.feedback).toContainEqual({
evaluator: 'e2',
metric: 'a',
score: 0.9,
kind: 'metric',
});
expect(result.feedback).toContainEqual({
evaluator: 'e2',
metric: 'b',
score: 1.0,
kind: 'metric',
});
});
it('should handle evaluator errors gracefully in target', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const goodEvaluator = createMockEvaluator('good', [
{ evaluator: 'good', metric: 'score', score: 1, kind: 'score' },
]);
const badEvaluator: Evaluator = {
name: 'bad',
evaluate: jest.fn().mockRejectedValue(new Error('Evaluator crashed')),
};
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [goodEvaluator, badEvaluator],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
const result = await callLangsmithTarget(target, { prompt: 'Test' });
expect(isLangsmithTargetOutput(result)).toBe(true);
if (!isLangsmithTargetOutput(result)) throw new Error('Expected LangSmith target output');
expect(result.feedback).toContainEqual({
evaluator: 'good',
metric: 'score',
score: 1,
kind: 'score',
});
expect(result.feedback).toContainEqual({
evaluator: 'bad',
metric: 'error',
score: 0,
kind: 'score',
comment: 'Evaluator crashed',
});
});
it('should create evaluator that extracts pre-computed feedback', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [_target, options] = mockEvaluate.mock.calls[0];
expect(Array.isArray(options.evaluators)).toBe(true);
if (!Array.isArray(options.evaluators))
throw new Error('Expected LangSmith evaluators array');
expect(options.evaluators).toHaveLength(1);
const evaluatorFn = options.evaluators[0];
expect(isCallable(evaluatorFn)).toBe(true);
if (!isCallable(evaluatorFn)) throw new Error('Expected evaluator function');
const extracted = await evaluatorFn({
outputs: {
feedback: [
{ evaluator: 'test', metric: 'score', score: 0.9, kind: 'score' },
{ evaluator: 'other', metric: 'trigger', score: 0.8, kind: 'metric' },
],
},
});
expect(extracted).toEqual([
{ key: 'test.score', score: 0.9 },
{ key: 'other.trigger', score: 0.8 },
]);
});
it('should keep programmatic prefixes but not llm-judge metric prefixes', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [_target, options] = mockEvaluate.mock.calls[0];
expect(Array.isArray(options.evaluators)).toBe(true);
if (!Array.isArray(options.evaluators))
throw new Error('Expected LangSmith evaluators array');
const evaluatorFn = options.evaluators[0];
expect(isCallable(evaluatorFn)).toBe(true);
if (!isCallable(evaluatorFn)) throw new Error('Expected evaluator function');
const extracted = await evaluatorFn({
outputs: {
feedback: [
{ evaluator: 'llm-judge', metric: 'functionality', score: 0.9, kind: 'metric' },
{ evaluator: 'programmatic', metric: 'trigger', score: 0.8, kind: 'metric' },
{
evaluator: 'llm-judge',
metric: 'maintainability.nodeNamingQuality',
score: 0.7,
kind: 'detail',
},
],
},
});
expect(extracted).toEqual([
{ key: 'functionality', score: 0.9 },
{ key: 'programmatic.trigger', score: 0.8 },
{ key: 'maintainability.nodeNamingQuality', score: 0.7 },
]);
});
it('should handle missing feedback in outputs', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [_target, options] = mockEvaluate.mock.calls[0];
expect(Array.isArray(options.evaluators)).toBe(true);
if (!Array.isArray(options.evaluators))
throw new Error('Expected LangSmith evaluators array');
const evaluatorFn = options.evaluators[0];
expect(isCallable(evaluatorFn)).toBe(true);
if (!isCallable(evaluatorFn)) throw new Error('Expected evaluator function');
const extracted = await evaluatorFn({ outputs: {} });
expect(extracted).toEqual([
{
key: 'evaluationError',
score: 0,
comment: 'No feedback found in target output',
},
]);
});
it('should pass dataset-level context to evaluators', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const evaluateContextual: Evaluator['evaluate'] = async (_workflow, ctx) => [
{ evaluator: 'contextual', metric: 'score', score: ctx.dos ? 1 : 0, kind: 'score' },
];
const evaluator: Evaluator = {
name: 'contextual',
evaluate: jest.fn(evaluateContextual),
};
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [evaluator],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
const result = await callLangsmithTarget(target, {
prompt: 'Test',
evals: { dos: 'Use Slack', donts: 'No HTTP' },
});
expect(isLangsmithTargetOutput(result)).toBe(true);
if (!isLangsmithTargetOutput(result)) throw new Error('Expected LangSmith target output');
expect(evaluator.evaluate).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ dos: 'Use Slack', donts: 'No HTTP' }),
);
expect(result.feedback).toContainEqual({
evaluator: 'contextual',
metric: 'score',
score: 1,
kind: 'score',
});
});
it('should ignore invalid referenceWorkflow in dataset context', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const evaluate = jest.fn<
ReturnType<Evaluator['evaluate']>,
Parameters<Evaluator['evaluate']>
>(async (_workflow, ctx) => [
{
evaluator: 'ref-check',
metric: 'hasRef',
score: ctx.referenceWorkflows && ctx.referenceWorkflows.length > 0 ? 1 : 0,
kind: 'score',
},
]);
const evaluator: Evaluator = {
name: 'ref-check',
evaluate,
};
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [evaluator],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [target] = mockEvaluate.mock.calls[0];
const result = await callLangsmithTarget(target, {
prompt: 'Test',
evals: {
referenceWorkflow: { nodes: [{}], connections: {} },
},
});
expect(isLangsmithTargetOutput(result)).toBe(true);
if (!isLangsmithTargetOutput(result)) throw new Error('Expected LangSmith target output');
const ctx = evaluate.mock.calls[0]?.[1];
expect(ctx?.referenceWorkflows).toBeUndefined();
expect(result.feedback).toContainEqual({
evaluator: 'ref-check',
metric: 'hasRef',
score: 0,
kind: 'score',
});
});
it('should pre-load and filter examples when filters are provided', async () => {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const examples: Example[] = [
mock<Example>({
id: 'e1',
inputs: { prompt: 'One', evals: { dos: 'Use Slack', donts: 'No HTTP' } },
metadata: { notion_id: 'n1', categories: ['data_transformation'] },
}),
mock<Example>({
id: 'e2',
inputs: { prompt: 'Two', evals: { dos: 'Use Gmail', donts: 'No Slack' } },
metadata: { notion_id: 'n2', categories: ['other'] },
}),
];
const lsClient = createMockLangsmithClient();
lsClient.listExamples.mockReturnValue(
(async function* () {
for (const ex of examples) yield ex;
})(),
);
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
filters: { notionId: 'n1', technique: 'data_transformation', doSearch: 'slack' },
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await runEvaluation(config);
expect(mockEvaluate).toHaveBeenCalledTimes(1);
const [_target, options] = mockEvaluate.mock.calls[0];
const data: unknown = options.data;
expect(Array.isArray(data)).toBe(true);
if (!Array.isArray(data)) throw new Error('Expected `evaluate()` to receive example array');
const ids = data
.filter((e): e is { id: string } => isRecord(e) && typeof e.id === 'string')
.map((e) => e.id);
expect(ids).toEqual(['e1']);
});
it('should throw when filters match no examples', async () => {
const lsClient = createMockLangsmithClient();
lsClient.listExamples.mockReturnValue(
(async function* () {
yield mock<Example>({
id: 'e1',
inputs: { prompt: 'One', evals: { dos: 'Use Slack', donts: 'No HTTP' } },
metadata: { notion_id: 'n1', categories: ['data_transformation'] },
});
})(),
);
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [createMockEvaluator('test')],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
filters: { notionId: 'does-not-exist' },
},
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
await expect(runEvaluation(config)).rejects.toThrow('No examples matched filters');
});
it('should include evaluatorAverages in summary', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-test-'));
try {
const mockEvaluate = jest.mocked(langsmithEvaluate);
const lsClient = createMockLangsmithClient();
const evaluator1 = createMockEvaluator('pairwise', [
{ evaluator: 'pairwise', metric: 'score', score: 0.8, kind: 'score' },
]);
const evaluator2 = createMockEvaluator('programmatic', [
{ evaluator: 'programmatic', metric: 'overall', score: 0.9, kind: 'score' },
]);
// Mock evaluate to call the target function with test inputs
mockEvaluate.mockImplementationOnce(async (target, _options) => {
// Call the target to populate capturedResults
await callLangsmithTarget(target, { prompt: 'Test prompt 1' });
await callLangsmithTarget(target, { prompt: 'Test prompt 2' });
return { experimentName: 'test-experiment' } as Awaited<
ReturnType<typeof langsmithEvaluate>
>;
});
const config: RunConfig = {
mode: 'langsmith',
dataset: 'test-dataset',
generateWorkflow: jest.fn().mockResolvedValue(createMockWorkflow()),
evaluators: [evaluator1, evaluator2],
langsmithClient: lsClient,
langsmithOptions: {
experimentName: 'test',
repetitions: 1,
concurrency: 1,
},
outputDir: tempDir, // Enable artifact saving to capture results
logger: silentLogger,
};
const { runEvaluation } = await import('../harness/runner');
const summary = await runEvaluation(config);
// The summary should include evaluatorAverages computed from captured results
expect(summary.evaluatorAverages).toBeDefined();
expect(summary.evaluatorAverages).toEqual({
pairwise: 0.8,
programmatic: 0.9,
});
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
});