n8n/packages/@n8n/instance-ai/evaluations/__tests__/runner-prebuilt.test.ts
José Braulio González Valido 30d9a168bc
feat(ai-builder): Add --prebuilt-workflows flag for eval CLI (no-changelog) (#29830)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:47:29 +00:00

124 lines
3.8 KiB
TypeScript

import type { N8nClient, WorkflowResponse } from '../clients/n8n-client';
import type { EvalLogger } from '../harness/logger';
import { runWorkflowTestCase } from '../harness/runner';
import type { WorkflowTestCase } from '../types';
/**
* Focused tests for the `prebuiltWorkflowId` branch of runWorkflowTestCase.
*
* The branch itself is a one-line conditional in runner.ts:
*
* const build = config.prebuiltWorkflowId
* ? await fetchPrebuiltBuild(...)
* : await buildWorkflow(...);
*
* The prebuilt path's invariants (BuildResult shape, error wrapping) are
* covered by `fetchPrebuiltBuild` unit tests in prebuilt-workflows.test.ts.
* These tests lock in the integration:
* • the orchestrator path (sendMessage / SSE) must not run
* • the prebuilt workflow must not be deleted on cleanup
*/
const silentLogger: EvalLogger = {
info: () => {},
verbose: () => {},
success: () => {},
warn: () => {},
error: () => {},
isVerbose: false,
};
function makeClient(overrides: Partial<Record<keyof N8nClient, jest.Mock>> = {}): {
client: N8nClient;
mocks: Record<string, jest.Mock>;
} {
const mocks: Record<string, jest.Mock> = {
getWorkflow: jest.fn(),
sendMessage: jest.fn(),
deleteWorkflow: jest.fn().mockResolvedValue(undefined),
deleteDataTable: jest.fn().mockResolvedValue(undefined),
listDataTables: jest.fn().mockResolvedValue([]),
...overrides,
};
return { client: mocks as unknown as N8nClient, mocks };
}
function makeTestCase(): WorkflowTestCase {
// Empty scenarios => runWorkflowTestCase short-circuits past the
// scenario-execution loop, so we don't need to mock executeScenario.
return {
prompt: 'build me something',
complexity: 'simple',
tags: ['test'],
scenarios: [],
};
}
describe('runWorkflowTestCase with prebuiltWorkflowId', () => {
it('fetches the prebuilt workflow and skips the orchestrator build path', async () => {
const fakeWorkflow = {
id: 'Wprebuilt',
name: 'Prebuilt fixture',
nodes: [],
connections: {},
} as unknown as WorkflowResponse;
const { client, mocks } = makeClient({
getWorkflow: jest.fn().mockResolvedValue(fakeWorkflow),
});
const result = await runWorkflowTestCase({
client,
testCase: makeTestCase(),
timeoutMs: 60_000,
seededCredentialTypes: [],
preRunWorkflowIds: new Set(),
claimedWorkflowIds: new Set(),
logger: silentLogger,
keepWorkflows: false,
prebuiltWorkflowId: 'Wprebuilt',
});
expect(result.workflowBuildSuccess).toBe(true);
expect(result.workflowId).toBe('Wprebuilt');
expect(result.workflowJson).toEqual(fakeWorkflow);
// Orchestrator entry point not invoked
expect(mocks.getWorkflow).toHaveBeenCalledWith('Wprebuilt');
expect(mocks.sendMessage).not.toHaveBeenCalled();
// cleanupBuild iterates createdWorkflowIds; for prebuilts that's
// empty, so the prebuilt workflow must not be deleted even when
// keepWorkflows is false.
expect(mocks.deleteWorkflow).not.toHaveBeenCalled();
});
it('reports build failure with the workflow ID when fetch fails', async () => {
const { client, mocks } = makeClient({
getWorkflow: jest.fn().mockRejectedValue(new Error('HTTP 404')),
});
const result = await runWorkflowTestCase({
client,
testCase: makeTestCase(),
timeoutMs: 60_000,
seededCredentialTypes: [],
preRunWorkflowIds: new Set(),
claimedWorkflowIds: new Set(),
logger: silentLogger,
keepWorkflows: false,
prebuiltWorkflowId: 'Wstale',
});
expect(result.workflowBuildSuccess).toBe(false);
expect(result.buildError).toContain('Wstale');
expect(result.buildError).toContain('HTTP 404');
expect(result.workflowId).toBeUndefined();
// Even on failure, the prebuilt must not be deleted (it's owned by the
// caller, not the eval run).
expect(mocks.deleteWorkflow).not.toHaveBeenCalled();
expect(mocks.sendMessage).not.toHaveBeenCalled();
});
});