mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-03 18:27:09 +02:00
chore(core): Migrate @n8n/agents from Jest to Vitest (no-changelog) (#31529)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
01cc906ebd
commit
73c02cb1c2
|
|
@ -1,7 +0,0 @@
|
|||
/** @type {import('jest').Config} */
|
||||
const base = require('../../../jest.config');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
testPathIgnorePatterns: [...(base.testPathIgnorePatterns || []), '/integration/'],
|
||||
};
|
||||
|
|
@ -43,9 +43,9 @@
|
|||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"test:integration": "vitest run --config vitest.integration.config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -91,6 +91,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@types/json-schema": "^7.0.15"
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-mock-extended": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,71 +3,73 @@ type MockExportResult = { code: number; error?: Error };
|
|||
type MockExportCallback = (result: MockExportResult) => void;
|
||||
type MockExporter = {
|
||||
type: 'exporter';
|
||||
export: jest.Mock<void, [unknown[], MockExportCallback]>;
|
||||
shutdown: jest.Mock<Promise<void>, []>;
|
||||
export: Mock<(...args: [unknown[], MockExportCallback]) => void>;
|
||||
shutdown: Mock<(...args: []) => Promise<void>>;
|
||||
};
|
||||
const mockExporterInstances: MockExporter[] = [];
|
||||
const mockBatchProcessorInputs: unknown[] = [];
|
||||
const mockBatchProcessorInstances: Array<{
|
||||
forceFlush: jest.Mock<Promise<void>, []>;
|
||||
onStart: jest.Mock<void, [unknown, unknown]>;
|
||||
onEnd: jest.Mock<void, [unknown]>;
|
||||
shutdown: jest.Mock<Promise<void>, []>;
|
||||
forceFlush: Mock<(...args: []) => Promise<void>>;
|
||||
onStart: Mock<(...args: [unknown, unknown]) => void>;
|
||||
onEnd: Mock<(...args: [unknown]) => void>;
|
||||
shutdown: Mock<(...args: []) => Promise<void>>;
|
||||
}> = [];
|
||||
const mockProviderConfigs: unknown[] = [];
|
||||
const mockAwaitPendingTraceBatches = jest.fn(async () => await Promise.resolve());
|
||||
const mockTracer = { startSpan: jest.fn() };
|
||||
const mockAwaitPendingTraceBatches = vi.fn(async () => await Promise.resolve());
|
||||
const mockTracer = { startSpan: vi.fn() };
|
||||
const mockProvider = {
|
||||
getTracer: jest.fn(() => mockTracer),
|
||||
register: jest.fn(),
|
||||
forceFlush: jest.fn(),
|
||||
shutdown: jest.fn(),
|
||||
getTracer: vi.fn(() => mockTracer),
|
||||
register: vi.fn(),
|
||||
forceFlush: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
};
|
||||
|
||||
jest.mock('langsmith/experimental/otel/exporter', () => ({
|
||||
LangSmithOTLPTraceExporter: jest.fn((config: unknown) => {
|
||||
vi.mock('langsmith/experimental/otel/exporter', () => ({
|
||||
LangSmithOTLPTraceExporter: vi.fn(function (config: unknown) {
|
||||
mockExporterConfigs.push(config);
|
||||
const exporter: MockExporter = {
|
||||
type: 'exporter',
|
||||
export: jest.fn((_: unknown[], resultHandler: MockExportCallback) => {
|
||||
export: vi.fn((_: unknown[], resultHandler: MockExportCallback) => {
|
||||
resultHandler({ code: 0 });
|
||||
}),
|
||||
shutdown: jest.fn(async () => await Promise.resolve()),
|
||||
shutdown: vi.fn(async () => await Promise.resolve()),
|
||||
};
|
||||
mockExporterInstances.push(exporter);
|
||||
return exporter;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@opentelemetry/sdk-trace-base', () => ({
|
||||
BatchSpanProcessor: jest.fn((exporter: unknown) => {
|
||||
vi.mock('@opentelemetry/sdk-trace-base', () => ({
|
||||
BatchSpanProcessor: vi.fn(function (exporter: unknown) {
|
||||
mockBatchProcessorInputs.push(exporter);
|
||||
const processor = {
|
||||
forceFlush: jest.fn(async () => await Promise.resolve()),
|
||||
onStart: jest.fn(),
|
||||
onEnd: jest.fn(),
|
||||
shutdown: jest.fn(async () => await Promise.resolve()),
|
||||
forceFlush: vi.fn(async () => await Promise.resolve()),
|
||||
onStart: vi.fn(),
|
||||
onEnd: vi.fn(),
|
||||
shutdown: vi.fn(async () => await Promise.resolve()),
|
||||
};
|
||||
mockBatchProcessorInstances.push(processor);
|
||||
return processor;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('langsmith', () => ({
|
||||
vi.mock('langsmith', () => ({
|
||||
RunTree: {
|
||||
getSharedClient: jest.fn(() => ({
|
||||
getSharedClient: vi.fn(() => ({
|
||||
awaitPendingTraceBatches: mockAwaitPendingTraceBatches,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@opentelemetry/sdk-trace-node', () => ({
|
||||
NodeTracerProvider: jest.fn((config: unknown) => {
|
||||
vi.mock('@opentelemetry/sdk-trace-node', () => ({
|
||||
NodeTracerProvider: vi.fn(function (config: unknown) {
|
||||
mockProviderConfigs.push(config);
|
||||
return mockProvider;
|
||||
}),
|
||||
}));
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { LangSmithTelemetry } from '../integrations/langsmith';
|
||||
|
||||
describe('LangSmithTelemetry', () => {
|
||||
|
|
@ -131,8 +133,8 @@ describe('LangSmithTelemetry', () => {
|
|||
});
|
||||
|
||||
it('resolves function headers for every export request', async () => {
|
||||
const getHeaders = jest
|
||||
.fn<Promise<Record<string, string>>, []>()
|
||||
const getHeaders = vi
|
||||
.fn<(...args: []) => Promise<Record<string, string>>>()
|
||||
.mockResolvedValueOnce({ Authorization: 'Bearer proxy-token-1' })
|
||||
.mockResolvedValueOnce({ Authorization: 'Bearer proxy-token-2' });
|
||||
|
||||
|
|
@ -186,8 +188,8 @@ describe('LangSmithTelemetry', () => {
|
|||
|
||||
it('reports export failure when function headers reject', async () => {
|
||||
const refreshError = new Error('could not refresh headers');
|
||||
const getHeaders = jest
|
||||
.fn<Promise<Record<string, string>>, []>()
|
||||
const getHeaders = vi
|
||||
.fn<(...args: []) => Promise<Record<string, string>>>()
|
||||
.mockRejectedValueOnce(refreshError);
|
||||
|
||||
await new LangSmithTelemetry({
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class TestFilesystem extends BaseFilesystem {
|
|||
readonly provider = 'test';
|
||||
status: ProviderStatus = 'pending';
|
||||
|
||||
initFn = jest.fn().mockResolvedValue(undefined);
|
||||
destroyFn = jest.fn().mockResolvedValue(undefined);
|
||||
initFn = vi.fn().mockResolvedValue(undefined);
|
||||
destroyFn = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
constructor(id: string, options?: BaseFilesystemOptions) {
|
||||
super(options);
|
||||
|
|
@ -176,7 +176,7 @@ describe('BaseFilesystem', () => {
|
|||
|
||||
describe('lifecycle hooks', () => {
|
||||
it('calls onInit hook after successful init', async () => {
|
||||
const onInit = jest.fn();
|
||||
const onInit = vi.fn();
|
||||
const fs = new TestFilesystem('1', { onInit });
|
||||
|
||||
await fs._init();
|
||||
|
|
@ -185,7 +185,7 @@ describe('BaseFilesystem', () => {
|
|||
});
|
||||
|
||||
it('does not fail when onInit hook throws', async () => {
|
||||
const onInit = jest.fn().mockRejectedValue(new Error('hook err'));
|
||||
const onInit = vi.fn().mockRejectedValue(new Error('hook err'));
|
||||
const fs = new TestFilesystem('1', { onInit });
|
||||
|
||||
await fs._init();
|
||||
|
|
@ -194,7 +194,7 @@ describe('BaseFilesystem', () => {
|
|||
});
|
||||
|
||||
it('calls onDestroy hook during destroy', async () => {
|
||||
const onDestroy = jest.fn();
|
||||
const onDestroy = vi.fn();
|
||||
const fs = new TestFilesystem('1', { onDestroy });
|
||||
await fs._init();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Mock } from 'vitest';
|
||||
|
||||
import { BaseSandbox } from '../../workspace/sandbox/base-sandbox';
|
||||
import type {
|
||||
CommandResult,
|
||||
|
|
@ -40,17 +42,17 @@ class StubProcessHandle extends ProcessHandle {
|
|||
}
|
||||
|
||||
function makeStubProcessManager(): SandboxProcessManager & {
|
||||
spawnMock: jest.Mock;
|
||||
spawnMock: Mock;
|
||||
} {
|
||||
const handle = new StubProcessHandle(1);
|
||||
const spawnMock = jest.fn().mockResolvedValue(handle);
|
||||
const spawnMock = vi.fn().mockResolvedValue(handle);
|
||||
return {
|
||||
spawn: spawnMock,
|
||||
list: jest.fn().mockResolvedValue([]),
|
||||
get: jest.fn().mockResolvedValue(undefined),
|
||||
kill: jest.fn().mockResolvedValue(false),
|
||||
list: vi.fn().mockResolvedValue([]),
|
||||
get: vi.fn().mockResolvedValue(undefined),
|
||||
kill: vi.fn().mockResolvedValue(false),
|
||||
spawnMock,
|
||||
} as unknown as SandboxProcessManager & { spawnMock: jest.Mock };
|
||||
} as unknown as SandboxProcessManager & { spawnMock: Mock };
|
||||
}
|
||||
|
||||
class TestSandbox extends BaseSandbox {
|
||||
|
|
@ -58,9 +60,9 @@ class TestSandbox extends BaseSandbox {
|
|||
readonly name: string;
|
||||
readonly provider = 'test';
|
||||
|
||||
startFn = jest.fn().mockResolvedValue(undefined);
|
||||
stopFn = jest.fn().mockResolvedValue(undefined);
|
||||
destroyFn = jest.fn().mockResolvedValue(undefined);
|
||||
startFn = vi.fn().mockResolvedValue(undefined);
|
||||
stopFn = vi.fn().mockResolvedValue(undefined);
|
||||
destroyFn = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
constructor(id: string, options?: BaseSandboxOptions) {
|
||||
super(options);
|
||||
|
|
@ -215,7 +217,7 @@ describe('BaseSandbox', () => {
|
|||
|
||||
describe('lifecycle hooks', () => {
|
||||
it('calls onStart hook after successful start', async () => {
|
||||
const onStart = jest.fn();
|
||||
const onStart = vi.fn();
|
||||
const sb = new TestSandbox('1', { onStart });
|
||||
|
||||
await sb._start();
|
||||
|
|
@ -224,7 +226,7 @@ describe('BaseSandbox', () => {
|
|||
});
|
||||
|
||||
it('does not fail when onStart hook throws', async () => {
|
||||
const onStart = jest.fn().mockRejectedValue(new Error('hook error'));
|
||||
const onStart = vi.fn().mockRejectedValue(new Error('hook error'));
|
||||
const sb = new TestSandbox('1', { onStart });
|
||||
|
||||
await sb._start();
|
||||
|
|
@ -233,7 +235,7 @@ describe('BaseSandbox', () => {
|
|||
});
|
||||
|
||||
it('calls onStop hook before stopping', async () => {
|
||||
const onStop = jest.fn();
|
||||
const onStop = vi.fn();
|
||||
const sb = new TestSandbox('1', { onStop });
|
||||
await sb._start();
|
||||
|
||||
|
|
@ -243,7 +245,7 @@ describe('BaseSandbox', () => {
|
|||
});
|
||||
|
||||
it('calls onDestroy hook before destroying', async () => {
|
||||
const onDestroy = jest.fn();
|
||||
const onDestroy = vi.fn();
|
||||
const sb = new TestSandbox('1', { onDestroy });
|
||||
await sb._start();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { callLifecycle } from '../../workspace/lifecycle';
|
|||
describe('callLifecycle', () => {
|
||||
it('calls _init when both _init and init exist', async () => {
|
||||
const target = {
|
||||
_init: jest.fn().mockResolvedValue(undefined),
|
||||
init: jest.fn().mockResolvedValue(undefined),
|
||||
_init: vi.fn().mockResolvedValue(undefined),
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await callLifecycle(target, 'init');
|
||||
|
|
@ -15,7 +15,7 @@ describe('callLifecycle', () => {
|
|||
|
||||
it('falls back to init when _init is undefined', async () => {
|
||||
const target = {
|
||||
init: jest.fn().mockResolvedValue(undefined),
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await callLifecycle(target, 'init');
|
||||
|
|
@ -25,8 +25,8 @@ describe('callLifecycle', () => {
|
|||
|
||||
it('calls _start when both _start and start exist', async () => {
|
||||
const target = {
|
||||
_start: jest.fn().mockResolvedValue(undefined),
|
||||
start: jest.fn().mockResolvedValue(undefined),
|
||||
_start: vi.fn().mockResolvedValue(undefined),
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await callLifecycle(target, 'start');
|
||||
|
|
@ -37,8 +37,8 @@ describe('callLifecycle', () => {
|
|||
|
||||
it('calls _stop over stop', async () => {
|
||||
const target = {
|
||||
_stop: jest.fn().mockResolvedValue(undefined),
|
||||
stop: jest.fn().mockResolvedValue(undefined),
|
||||
_stop: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await callLifecycle(target, 'stop');
|
||||
|
|
@ -49,8 +49,8 @@ describe('callLifecycle', () => {
|
|||
|
||||
it('calls _destroy over destroy', async () => {
|
||||
const target = {
|
||||
_destroy: jest.fn().mockResolvedValue(undefined),
|
||||
destroy: jest.fn().mockResolvedValue(undefined),
|
||||
_destroy: vi.fn().mockResolvedValue(undefined),
|
||||
destroy: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
await callLifecycle(target, 'destroy');
|
||||
|
|
@ -68,7 +68,7 @@ describe('callLifecycle', () => {
|
|||
it('propagates errors from lifecycle methods', async () => {
|
||||
const error = new Error('lifecycle failure');
|
||||
const target = {
|
||||
_start: jest.fn().mockRejectedValue(error),
|
||||
_start: vi.fn().mockRejectedValue(error),
|
||||
};
|
||||
|
||||
await expect(callLifecycle(target, 'start')).rejects.toThrow('lifecycle failure');
|
||||
|
|
@ -78,7 +78,7 @@ describe('callLifecycle', () => {
|
|||
const target = {
|
||||
value: 42,
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
_init: jest.fn(async function (this: { value: number }) {
|
||||
_init: vi.fn(async function (this: { value: number }) {
|
||||
expect(this.value).toBe(42);
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,20 +8,20 @@ function makeFakeFilesystem(overrides: Partial<WorkspaceFilesystem> = {}): Works
|
|||
name: 'TestFS',
|
||||
provider: 'test',
|
||||
status: 'ready',
|
||||
readFile: jest.fn().mockResolvedValue('file content'),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined),
|
||||
appendFile: jest.fn().mockResolvedValue(undefined),
|
||||
deleteFile: jest.fn().mockResolvedValue(undefined),
|
||||
copyFile: jest.fn().mockResolvedValue(undefined),
|
||||
moveFile: jest.fn().mockResolvedValue(undefined),
|
||||
mkdir: jest.fn().mockResolvedValue(undefined),
|
||||
rmdir: jest.fn().mockResolvedValue(undefined),
|
||||
readdir: jest.fn().mockResolvedValue([
|
||||
readFile: vi.fn().mockResolvedValue('file content'),
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
appendFile: vi.fn().mockResolvedValue(undefined),
|
||||
deleteFile: vi.fn().mockResolvedValue(undefined),
|
||||
copyFile: vi.fn().mockResolvedValue(undefined),
|
||||
moveFile: vi.fn().mockResolvedValue(undefined),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
rmdir: vi.fn().mockResolvedValue(undefined),
|
||||
readdir: vi.fn().mockResolvedValue([
|
||||
{ name: 'file1.txt', type: 'file' as const },
|
||||
{ name: 'subdir', type: 'directory' as const },
|
||||
]),
|
||||
exists: jest.fn().mockResolvedValue(true),
|
||||
stat: jest.fn().mockResolvedValue({
|
||||
exists: vi.fn().mockResolvedValue(true),
|
||||
stat: vi.fn().mockResolvedValue({
|
||||
name: 'test.txt',
|
||||
path: '/test.txt',
|
||||
type: 'file' as const,
|
||||
|
|
@ -46,7 +46,7 @@ function makeFakeSandbox(overrides: Partial<WorkspaceSandbox> = {}): WorkspaceSa
|
|||
name: 'TestSandbox',
|
||||
provider: 'test',
|
||||
status: 'running',
|
||||
executeCommand: jest.fn().mockResolvedValue(mockResult),
|
||||
executeCommand: vi.fn().mockResolvedValue(mockResult),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@ describe('createWorkspaceTools', () => {
|
|||
|
||||
it('str_replace_file handler reads then writes changed content', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
readFile: jest.fn().mockResolvedValue('first\nsecond'),
|
||||
readFile: vi.fn().mockResolvedValue('first\nsecond'),
|
||||
});
|
||||
const tools = createWorkspaceTools({ filesystem: fs });
|
||||
const strReplaceTool = tools.find((t) => t.name === 'workspace_str_replace_file')!;
|
||||
|
|
@ -153,7 +153,7 @@ describe('createWorkspaceTools', () => {
|
|||
|
||||
it('str_replace_file handler returns errors without writing when replacement is not unique', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
readFile: jest.fn().mockResolvedValue('same\nsame'),
|
||||
readFile: vi.fn().mockResolvedValue('same\nsame'),
|
||||
});
|
||||
const tools = createWorkspaceTools({ filesystem: fs });
|
||||
const strReplaceTool = tools.find((t) => t.name === 'workspace_str_replace_file')!;
|
||||
|
|
@ -176,7 +176,7 @@ describe('createWorkspaceTools', () => {
|
|||
|
||||
it('batch_str_replace_file handler applies all replacements atomically', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
readFile: jest.fn().mockResolvedValue('const a = 1;\nconst b = 2;'),
|
||||
readFile: vi.fn().mockResolvedValue('const a = 1;\nconst b = 2;'),
|
||||
});
|
||||
const tools = createWorkspaceTools({ filesystem: fs });
|
||||
const batchStrReplaceTool = tools.find((t) => t.name === 'workspace_batch_str_replace_file')!;
|
||||
|
|
@ -203,7 +203,7 @@ describe('createWorkspaceTools', () => {
|
|||
|
||||
it('batch_str_replace_file handler does not write when any replacement fails', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
readFile: jest.fn().mockResolvedValue('const a = 1;\nconst b = 2;'),
|
||||
readFile: vi.fn().mockResolvedValue('const a = 1;\nconst b = 2;'),
|
||||
});
|
||||
const tools = createWorkspaceTools({ filesystem: fs });
|
||||
const batchStrReplaceTool = tools.find((t) => t.name === 'workspace_batch_str_replace_file')!;
|
||||
|
|
@ -251,7 +251,7 @@ describe('createWorkspaceTools', () => {
|
|||
});
|
||||
|
||||
it('execute_command handler includes sandbox default command environment', async () => {
|
||||
const executeCommand = jest.fn().mockResolvedValue({
|
||||
const executeCommand = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
exitCode: 0,
|
||||
stdout: 'ok',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Mock } from 'vitest';
|
||||
|
||||
import type { WorkspaceFilesystem, WorkspaceSandbox } from '../../workspace/types';
|
||||
import { Workspace } from '../../workspace/workspace';
|
||||
|
||||
|
|
@ -7,17 +9,17 @@ function makeFakeFilesystem(overrides: Partial<WorkspaceFilesystem> = {}): Works
|
|||
name: 'TestFS',
|
||||
provider: 'test',
|
||||
status: 'pending',
|
||||
readFile: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
appendFile: jest.fn(),
|
||||
deleteFile: jest.fn(),
|
||||
copyFile: jest.fn(),
|
||||
moveFile: jest.fn(),
|
||||
mkdir: jest.fn(),
|
||||
rmdir: jest.fn(),
|
||||
readdir: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
appendFile: vi.fn(),
|
||||
deleteFile: vi.fn(),
|
||||
copyFile: vi.fn(),
|
||||
moveFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
rmdir: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
stat: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -87,13 +89,13 @@ describe('Workspace', () => {
|
|||
it('calls filesystem._init then sandbox._start', async () => {
|
||||
const order: string[] = [];
|
||||
const fs = makeFakeFilesystem({
|
||||
_init: jest.fn(async () => {
|
||||
_init: vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('fs-init');
|
||||
}),
|
||||
});
|
||||
const sb = makeFakeSandbox({
|
||||
_start: jest.fn(async () => {
|
||||
_start: vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('sb-start');
|
||||
}),
|
||||
|
|
@ -114,7 +116,7 @@ describe('Workspace', () => {
|
|||
|
||||
it('initializes only filesystem when no sandbox', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
_init: jest.fn().mockResolvedValue(undefined),
|
||||
_init: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const ws = new Workspace({ filesystem: fs });
|
||||
|
||||
|
|
@ -126,7 +128,7 @@ describe('Workspace', () => {
|
|||
|
||||
it('starts only sandbox when no filesystem', async () => {
|
||||
const sb = makeFakeSandbox({
|
||||
_start: jest.fn().mockResolvedValue(undefined),
|
||||
_start: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const ws = new Workspace({ sandbox: sb });
|
||||
|
||||
|
|
@ -138,11 +140,11 @@ describe('Workspace', () => {
|
|||
|
||||
it('destroys filesystem and sets error status when sandbox start fails', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
_init: jest.fn().mockResolvedValue(undefined),
|
||||
_destroy: jest.fn().mockResolvedValue(undefined),
|
||||
_init: vi.fn().mockResolvedValue(undefined),
|
||||
_destroy: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const sb = makeFakeSandbox({
|
||||
_start: jest.fn().mockRejectedValue(new Error('sandbox start failed')),
|
||||
_start: vi.fn().mockRejectedValue(new Error('sandbox start failed')),
|
||||
});
|
||||
const ws = new Workspace({ filesystem: fs, sandbox: sb });
|
||||
|
||||
|
|
@ -155,12 +157,12 @@ describe('Workspace', () => {
|
|||
|
||||
it('is idempotent when already ready', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
_init: jest.fn().mockResolvedValue(undefined),
|
||||
_init: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const ws = new Workspace({ filesystem: fs });
|
||||
|
||||
await ws.init();
|
||||
(fs._init as jest.Mock).mockClear();
|
||||
(fs._init as Mock).mockClear();
|
||||
|
||||
await ws.init();
|
||||
|
||||
|
|
@ -170,7 +172,7 @@ describe('Workspace', () => {
|
|||
it('deduplicates concurrent init calls', async () => {
|
||||
let resolveInit: () => void;
|
||||
const fs = makeFakeFilesystem({
|
||||
_init: jest.fn(
|
||||
_init: vi.fn(
|
||||
async () =>
|
||||
await new Promise<void>((r) => {
|
||||
resolveInit = r;
|
||||
|
|
@ -194,13 +196,13 @@ describe('Workspace', () => {
|
|||
it('calls sandbox._destroy then filesystem._destroy', async () => {
|
||||
const order: string[] = [];
|
||||
const fs = makeFakeFilesystem({
|
||||
_destroy: jest.fn(async () => {
|
||||
_destroy: vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('fs-destroy');
|
||||
}),
|
||||
});
|
||||
const sb = makeFakeSandbox({
|
||||
_destroy: jest.fn(async () => {
|
||||
_destroy: vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
order.push('sb-destroy');
|
||||
}),
|
||||
|
|
@ -221,10 +223,10 @@ describe('Workspace', () => {
|
|||
|
||||
it('transitions to error when sandbox destroy throws', async () => {
|
||||
const fs = makeFakeFilesystem({
|
||||
_destroy: jest.fn().mockResolvedValue(undefined),
|
||||
_destroy: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const sb = makeFakeSandbox({
|
||||
_destroy: jest.fn().mockRejectedValue(new Error('sandbox boom')),
|
||||
_destroy: vi.fn().mockRejectedValue(new Error('sandbox boom')),
|
||||
});
|
||||
const ws = new Workspace({ filesystem: fs, sandbox: sb });
|
||||
|
||||
|
|
@ -285,7 +287,7 @@ describe('Workspace', () => {
|
|||
|
||||
it('returns execute_command tool when sandbox has executeCommand', () => {
|
||||
const sb = makeFakeSandbox({
|
||||
executeCommand: jest.fn(),
|
||||
executeCommand: vi.fn(),
|
||||
});
|
||||
const ws = new Workspace({ sandbox: sb });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import * as aiModule from 'ai';
|
||||
import type { Mock, MockedFunction } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isLlmMessage } from '../../sdk/message';
|
||||
|
|
@ -12,20 +14,21 @@ import type { BuiltTelemetry } from '../../types/telemetry';
|
|||
import { AgentRuntime } from '../agent-runtime';
|
||||
import { AgentEventBus } from '../event-bus';
|
||||
import { InMemoryMemory } from '../memory-store';
|
||||
import { toAiSdkTools } from '../tool-adapter';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Mock provider packages so createModel() doesn't fail when no API key is set
|
||||
jest.mock('@ai-sdk/openai', () => ({
|
||||
vi.mock('@ai-sdk/openai', () => ({
|
||||
createOpenAI: () =>
|
||||
Object.assign(() => ({ provider: 'openai', modelId: 'mock', specificationVersion: 'v3' }), {
|
||||
embeddingModel: () => ({ provider: 'openai', modelId: 'mock', specificationVersion: 'v2' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/anthropic', () => ({
|
||||
vi.mock('@ai-sdk/anthropic', () => ({
|
||||
createAnthropic: () => () => ({
|
||||
provider: 'anthropic',
|
||||
modelId: 'mock',
|
||||
|
|
@ -37,17 +40,17 @@ jest.mock('@ai-sdk/anthropic', () => ({
|
|||
type AiImport = typeof import('ai');
|
||||
|
||||
// Mock generateText and streamText from the 'ai' package
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
embed: jest.fn(),
|
||||
embedMany: jest.fn(),
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
tool: jest.fn((config: unknown) => config),
|
||||
embed: vi.fn(),
|
||||
embedMany: vi.fn(),
|
||||
generateText: vi.fn(),
|
||||
streamText: vi.fn(),
|
||||
tool: vi.fn((config: unknown) => config),
|
||||
Output: {
|
||||
object: jest.fn(({ schema }: { schema: unknown }) => ({ _type: 'object', schema })),
|
||||
object: vi.fn(({ schema }: { schema: unknown }) => ({ _type: 'object', schema })),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -56,12 +59,11 @@ jest.mock('ai', () => {
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { embed, embedMany, generateText, streamText } = require('ai') as {
|
||||
embed: jest.Mock;
|
||||
embedMany: jest.Mock;
|
||||
generateText: jest.Mock;
|
||||
streamText: jest.Mock;
|
||||
const { embed, embedMany, generateText, streamText } = aiModule as unknown as {
|
||||
embed: Mock;
|
||||
embedMany: Mock;
|
||||
generateText: Mock;
|
||||
streamText: Mock;
|
||||
};
|
||||
|
||||
/** Minimal successful generateText response. */
|
||||
|
|
@ -111,9 +113,9 @@ function makeErrorStream(error: Error) {
|
|||
|
||||
function makeExecutionCounter() {
|
||||
return {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +307,7 @@ describe('AgentRuntime — execution counters', () => {
|
|||
|
||||
describe('AgentRuntime.generate() — graceful error contract', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('resolves (never rejects) when the LLM call throws', async () => {
|
||||
|
|
@ -432,7 +434,7 @@ describe('AgentRuntime.generate() — graceful error contract', () => {
|
|||
|
||||
describe('AgentRuntime.stream() — graceful error contract', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('resolves (never rejects) when the LLM stream throws', async () => {
|
||||
|
|
@ -574,7 +576,7 @@ describe('AgentRuntime.stream() — graceful error contract', () => {
|
|||
|
||||
describe('AgentRuntime.resume() — graceful error contract', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('rejects with an error when the runId is not found', async () => {
|
||||
|
|
@ -596,7 +598,7 @@ describe('AgentRuntime.resume() — graceful error contract', () => {
|
|||
|
||||
describe('AgentRuntime — state transitions on error', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('starts idle, then reflects running→failed after a generate error', async () => {
|
||||
|
|
@ -1440,7 +1442,7 @@ describe('AgentRuntime — concurrent tool execution', () => {
|
|||
|
||||
describe('AgentRuntime.generate() — structured output', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns structuredOutput when schema is configured', async () => {
|
||||
|
|
@ -1522,7 +1524,7 @@ describe('AgentRuntime.generate() — structured output', () => {
|
|||
|
||||
describe('AgentRuntime.stream() — structured output', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('includes structuredOutput in the finish chunk when schema is configured', async () => {
|
||||
|
|
@ -1636,7 +1638,7 @@ describe('AgentRuntime.stream() — structured output', () => {
|
|||
|
||||
describe('AgentRuntime.resume() — structured output', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns structuredOutput after resume in generate mode', async () => {
|
||||
|
|
@ -1704,16 +1706,12 @@ describe('AgentRuntime.resume() — structured output', () => {
|
|||
/* eslint-disable @typescript-eslint/require-await */
|
||||
describe('providerOptions — tool adapter', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('forwards providerOptions to the AI SDK tool when set', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const ai = require('ai') as { tool: jest.Mock };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const adapter = require('../tool-adapter') as {
|
||||
toAiSdkTools: (tools: BuiltTool[]) => Record<string, unknown>;
|
||||
};
|
||||
const ai = aiModule as unknown as { tool: Mock };
|
||||
const adapter = { toAiSdkTools };
|
||||
|
||||
const builtTool: BuiltTool = {
|
||||
name: 'set_code',
|
||||
|
|
@ -1736,12 +1734,8 @@ describe('providerOptions — tool adapter', () => {
|
|||
});
|
||||
|
||||
it('forwards arbitrary provider options (not just Anthropic)', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const ai = require('ai') as { tool: jest.Mock };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const adapter = require('../tool-adapter') as {
|
||||
toAiSdkTools: (tools: BuiltTool[]) => Record<string, unknown>;
|
||||
};
|
||||
const ai = aiModule as unknown as { tool: Mock };
|
||||
const adapter = { toAiSdkTools };
|
||||
|
||||
const builtTool: BuiltTool = {
|
||||
name: 'draw',
|
||||
|
|
@ -1761,12 +1755,8 @@ describe('providerOptions — tool adapter', () => {
|
|||
});
|
||||
|
||||
it('does not pass providerOptions when not set', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const ai = require('ai') as { tool: jest.Mock };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const adapter = require('../tool-adapter') as {
|
||||
toAiSdkTools: (tools: BuiltTool[]) => Record<string, unknown>;
|
||||
};
|
||||
const ai = aiModule as unknown as { tool: Mock };
|
||||
const adapter = { toAiSdkTools };
|
||||
|
||||
const builtTool: BuiltTool = {
|
||||
name: 'search',
|
||||
|
|
@ -1831,7 +1821,7 @@ describe('Tool builder — providerOptions', () => {
|
|||
|
||||
describe('AgentRuntime — runtime input schema validation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('surfaces a ZodError as a tool error outcome when LLM provides invalid input', async () => {
|
||||
|
|
@ -1878,7 +1868,7 @@ describe('AgentRuntime — runtime input schema validation', () => {
|
|||
|
||||
describe('AgentRuntime — runtime JSON Schema input validation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes valid input through without error', async () => {
|
||||
|
|
@ -1994,7 +1984,7 @@ describe('AgentRuntime — runtime JSON Schema input validation', () => {
|
|||
});
|
||||
|
||||
it('does not invoke the handler when JSON Schema validation fails', async () => {
|
||||
const handlerFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const handlerFn = vi.fn().mockResolvedValue({ ok: true });
|
||||
const tool: BuiltTool = {
|
||||
name: 'json_tool',
|
||||
description: 'json tool',
|
||||
|
|
@ -2032,11 +2022,11 @@ describe('AgentRuntime — runtime JSON Schema input validation', () => {
|
|||
|
||||
describe('AgentRuntime — Tool builder with JSON Schema input', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes valid input to the handler when built via Tool builder', async () => {
|
||||
const handlerFn = jest.fn().mockResolvedValue({ found: true });
|
||||
const handlerFn = vi.fn().mockResolvedValue({ found: true });
|
||||
|
||||
const tool = new Tool('lookup')
|
||||
.description('Look up a record by id')
|
||||
|
|
@ -2076,7 +2066,7 @@ describe('AgentRuntime — Tool builder with JSON Schema input', () => {
|
|||
});
|
||||
|
||||
it('produces a tool error when the LLM sends input that fails JSON Schema validation', async () => {
|
||||
const handlerFn = jest.fn().mockResolvedValue({ found: true });
|
||||
const handlerFn = vi.fn().mockResolvedValue({ found: true });
|
||||
|
||||
const tool = new Tool('lookup')
|
||||
.description('Look up a record by id')
|
||||
|
|
@ -2119,7 +2109,7 @@ describe('AgentRuntime — Tool builder with JSON Schema input', () => {
|
|||
});
|
||||
|
||||
it('validates enum and pattern constraints defined in JSON Schema', async () => {
|
||||
const handlerFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const handlerFn = vi.fn().mockResolvedValue({ ok: true });
|
||||
|
||||
const tool = new Tool('set_status')
|
||||
.description('Set the status of a record')
|
||||
|
|
@ -2165,7 +2155,7 @@ describe('AgentRuntime — Tool builder with JSON Schema input', () => {
|
|||
|
||||
describe('AgentRuntime — runtime resume data schema validation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('surfaces a ZodError as a top-level error when consumer provides invalid resume data', async () => {
|
||||
|
|
@ -2203,7 +2193,7 @@ describe('AgentRuntime — runtime resume data schema validation', () => {
|
|||
|
||||
describe('AgentRuntime — tool approval (HITL wrapper)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('suspends when a tool has .requireApproval() set', async () => {
|
||||
|
|
@ -2316,7 +2306,7 @@ describe('AgentRuntime — tool approval (HITL wrapper)', () => {
|
|||
|
||||
describe('external abort signal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should cancel run when external signal fires', async () => {
|
||||
|
|
@ -2359,7 +2349,7 @@ describe('external abort signal', () => {
|
|||
|
||||
describe('provider options merging', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should deep-merge thinking config with call-level providerOptions', async () => {
|
||||
|
|
@ -2379,7 +2369,6 @@ describe('provider options merging', () => {
|
|||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect((callArgs.providerOptions as Record<string, Record<string, unknown>>).anthropic).toEqual(
|
||||
{
|
||||
|
|
@ -2396,11 +2385,10 @@ describe('provider options merging', () => {
|
|||
|
||||
describe('tool systemInstruction merging', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function getSystemMessageText(): string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const messages = callArgs.messages as Array<Record<string, unknown>>;
|
||||
const systemMsg = messages[0];
|
||||
|
|
@ -2504,7 +2492,7 @@ describe('tool systemInstruction merging', () => {
|
|||
|
||||
describe('instruction providerOptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should attach providerOptions to system message', async () => {
|
||||
|
|
@ -2523,7 +2511,6 @@ describe('instruction providerOptions', () => {
|
|||
persistence: { resourceId: 'user1', threadId: 'thread1' },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const messages = callArgs.messages as Array<Record<string, unknown>>;
|
||||
const systemMsg = messages[0];
|
||||
|
|
@ -2540,7 +2527,7 @@ describe('instruction providerOptions', () => {
|
|||
|
||||
describe('AgentRuntime — observation log jobs', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('schedules observation after a persisted stream turn', async () => {
|
||||
|
|
@ -2628,8 +2615,8 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
const memory = new InMemoryMemory() as InMemoryMemory &
|
||||
Required<Pick<BuiltMemory, 'saveEmbeddings' | 'queryEmbeddings'>>;
|
||||
const fakeEmbedder = { specificationVersion: 'v2' } as never;
|
||||
const observationLockSpy = jest.spyOn(memory, 'acquireObservationLogTaskLock');
|
||||
const episodicLockSpy = jest.spyOn(memory.episodic.taskLock!, 'acquire');
|
||||
const observationLockSpy = vi.spyOn(memory, 'acquireObservationLogTaskLock');
|
||||
const episodicLockSpy = vi.spyOn(memory.episodic.taskLock!, 'acquire');
|
||||
|
||||
const runtime = new AgentRuntime({
|
||||
name: 'observing-agent',
|
||||
|
|
@ -2701,7 +2688,7 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
createdAt: new Date('2026-05-20T12:00:00Z'),
|
||||
},
|
||||
]);
|
||||
const extract = jest.fn(async () => {
|
||||
const extract = vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
|
||||
return {
|
||||
|
|
@ -2713,7 +2700,7 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
],
|
||||
};
|
||||
});
|
||||
jest.spyOn(memory.episodic.taskLock!, 'acquire').mockResolvedValue(null);
|
||||
vi.spyOn(memory.episodic.taskLock!, 'acquire').mockResolvedValue(null);
|
||||
|
||||
const runtime = new AgentRuntime({
|
||||
name: 'observing-agent',
|
||||
|
|
@ -2951,7 +2938,7 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
persistence: { threadId: 'shared-thread', resourceId: 'resource-2' },
|
||||
});
|
||||
|
||||
const generateTextMock = generateText as jest.MockedFunction<
|
||||
const generateTextMock = generateText as MockedFunction<
|
||||
(input: {
|
||||
messages: Array<{
|
||||
role: string;
|
||||
|
|
@ -3021,7 +3008,7 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
persistence: { threadId: 'thread-1', resourceId: 'resource-1' },
|
||||
});
|
||||
|
||||
const generateTextMock = generateText as jest.MockedFunction<
|
||||
const generateTextMock = generateText as MockedFunction<
|
||||
(input: { messages: unknown[] }) => unknown
|
||||
>;
|
||||
const [{ messages }] = generateTextMock.mock.calls[0];
|
||||
|
|
@ -3143,7 +3130,7 @@ describe('AgentRuntime — observation log jobs', () => {
|
|||
|
||||
describe('AgentRuntime — telemetry propagation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const baseTelemetry: BuiltTelemetry = {
|
||||
|
|
@ -3153,7 +3140,7 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
recordInputs: true,
|
||||
recordOutputs: false,
|
||||
integrations: [],
|
||||
tracer: { startSpan: jest.fn() },
|
||||
tracer: { startSpan: vi.fn() },
|
||||
};
|
||||
|
||||
it('passes telemetry config into generateText as experimental_telemetry', async () => {
|
||||
|
|
@ -3169,7 +3156,6 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
|
||||
await runtime.generate('hello');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const expTelemetry = callArgs.experimental_telemetry as Record<string, unknown>;
|
||||
expect(expTelemetry).toBeDefined();
|
||||
|
|
@ -3199,7 +3185,6 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
runtime.setTelemetry(updatedTelemetry);
|
||||
await runtime.generate('hello');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const expTelemetry = callArgs.experimental_telemetry as Record<string, unknown>;
|
||||
expect(expTelemetry.functionId).toBe('updated-agent');
|
||||
|
|
@ -3209,12 +3194,12 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
it('wraps generate calls in a telemetry root span when the tracer supports active spans', async () => {
|
||||
generateText.mockResolvedValue(makeGenerateSuccess());
|
||||
const span = {
|
||||
end: jest.fn(),
|
||||
recordException: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
end: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
};
|
||||
const tracer = {
|
||||
startActiveSpan: jest.fn(async (_name: string, _options: unknown, fn: unknown) => {
|
||||
startActiveSpan: vi.fn(async (_name: string, _options: unknown, fn: unknown) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error('Expected span callback');
|
||||
}
|
||||
|
|
@ -3254,7 +3239,7 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
it('can suppress the generic runtime root span while keeping native telemetry enabled', async () => {
|
||||
generateText.mockResolvedValue(makeGenerateSuccess());
|
||||
const tracer = {
|
||||
startActiveSpan: jest.fn(),
|
||||
startActiveSpan: vi.fn(),
|
||||
};
|
||||
const telemetry: BuiltTelemetry = {
|
||||
...baseTelemetry,
|
||||
|
|
@ -3273,7 +3258,7 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
await runtime.generate('hello');
|
||||
|
||||
expect(tracer.startActiveSpan).not.toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(callArgs.experimental_telemetry).toEqual(
|
||||
expect.objectContaining({
|
||||
|
|
@ -3287,12 +3272,12 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
it('adds a LangSmith tool catalog to telemetry root spans', async () => {
|
||||
generateText.mockResolvedValue(makeGenerateSuccess());
|
||||
const span = {
|
||||
end: jest.fn(),
|
||||
recordException: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
end: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
};
|
||||
const tracer = {
|
||||
startActiveSpan: jest.fn(async (_name: string, _options: unknown, fn: unknown) => {
|
||||
startActiveSpan: vi.fn(async (_name: string, _options: unknown, fn: unknown) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error('Expected span callback');
|
||||
}
|
||||
|
|
@ -3358,7 +3343,6 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
const { stream } = await runtime.stream('hello');
|
||||
await collectChunks(stream);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = streamText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const expTelemetry = callArgs.experimental_telemetry as Record<string, unknown>;
|
||||
expect(expTelemetry).toBeDefined();
|
||||
|
|
@ -3383,7 +3367,6 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
telemetry: baseTelemetry,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
const expTelemetry = callArgs.experimental_telemetry as Record<string, unknown>;
|
||||
expect(expTelemetry).toBeDefined();
|
||||
|
|
@ -3431,22 +3414,22 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
const spans: Array<{
|
||||
name: string;
|
||||
span: {
|
||||
end: jest.Mock;
|
||||
recordException: jest.Mock;
|
||||
setAttributes: jest.Mock;
|
||||
setStatus: jest.Mock;
|
||||
end: Mock;
|
||||
recordException: Mock;
|
||||
setAttributes: Mock;
|
||||
setStatus: Mock;
|
||||
};
|
||||
}> = [];
|
||||
const tracer = {
|
||||
startActiveSpan: jest.fn(async (name: string, _options: unknown, fn: unknown) => {
|
||||
startActiveSpan: vi.fn(async (name: string, _options: unknown, fn: unknown) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error('Expected span callback');
|
||||
}
|
||||
const span = {
|
||||
end: jest.fn(),
|
||||
recordException: jest.fn(),
|
||||
setAttributes: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
end: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
setAttributes: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
};
|
||||
spans.push({ name, span });
|
||||
const spanFn = fn as (spanValue: typeof span) => Promise<unknown>;
|
||||
|
|
@ -3552,7 +3535,6 @@ describe('AgentRuntime — telemetry propagation', () => {
|
|||
|
||||
await runtime.generate('hello');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const callArgs = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
expect(callArgs.experimental_telemetry).toBeUndefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ type GenerateObjectCall = {
|
|||
|
||||
type GenerateObjectResult = { object: unknown; usage?: { totalTokens?: number } };
|
||||
|
||||
const mockGenerateObject = jest.fn<Promise<GenerateObjectResult>, [GenerateObjectCall]>();
|
||||
const { mockGenerateObject } = vi.hoisted(() => ({
|
||||
mockGenerateObject: vi.fn<(...args: [GenerateObjectCall]) => Promise<GenerateObjectResult>>(),
|
||||
}));
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<typeof AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<typeof AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
generateObject: async (call: GenerateObjectCall): Promise<GenerateObjectResult> =>
|
||||
|
|
@ -30,7 +32,7 @@ jest.mock('ai', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const fakeModel = { doGenerate: jest.fn() } as unknown as ModelConfig;
|
||||
const fakeModel = { doGenerate: vi.fn() } as unknown as ModelConfig;
|
||||
|
||||
describe('episodic memory defaults', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -175,9 +177,9 @@ describe('episodic memory defaults', () => {
|
|||
|
||||
it('counts extraction and reflection generation tokens when usage is available', async () => {
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
|
||||
mockGenerateObject.mockImplementationOnce(async ({ schema }) => {
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import {
|
|||
} from '../episodic-memory';
|
||||
import { InMemoryMemory } from '../memory-store';
|
||||
|
||||
jest.mock('ai', () => ({
|
||||
embed: jest.fn(),
|
||||
embedMany: jest.fn(),
|
||||
vi.mock('ai', () => ({
|
||||
embed: vi.fn(),
|
||||
embedMany: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedEmbed = jest.mocked(embed);
|
||||
const mockedEmbedMany = jest.mocked(embedMany);
|
||||
const mockedEmbed = vi.mocked(embed);
|
||||
const mockedEmbedMany = vi.mocked(embedMany);
|
||||
const fakeEmbedder = { specificationVersion: 'v2' } as never;
|
||||
|
||||
function entry(overrides: Partial<EpisodicMemoryEntry> = {}): EpisodicMemoryEntry {
|
||||
|
|
@ -219,9 +219,9 @@ describe('createRecallMemoryTool', () => {
|
|||
it('counts recall query embedding tokens when usage is available', async () => {
|
||||
mockedEmbed.mockResolvedValue({ embedding: [1, 0], usage: { tokens: 7 } } as never);
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
const memory = new InMemoryMemory();
|
||||
const tool = createRecallMemoryTool({
|
||||
|
|
@ -306,7 +306,7 @@ describe('InMemoryMemory episodic source cleanup', () => {
|
|||
|
||||
describe('runEpisodicMemoryIndexer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockedEmbedMany.mockResolvedValue({ embeddings: [[1, 0]], usage: { tokens: 1 } } as never);
|
||||
mockedEmbed.mockResolvedValue({ embedding: [1, 0], usage: { tokens: 1 } } as never);
|
||||
});
|
||||
|
|
@ -376,7 +376,7 @@ describe('runEpisodicMemoryIndexer', () => {
|
|||
},
|
||||
]);
|
||||
const sourceError = new Error('entry/source write failed');
|
||||
jest.spyOn(memory.episodic, 'saveEntryWithSources').mockRejectedValueOnce(sourceError);
|
||||
vi.spyOn(memory.episodic, 'saveEntryWithSources').mockRejectedValueOnce(sourceError);
|
||||
const extract: EpisodicMemoryExtractFn = async () =>
|
||||
await Promise.resolve({
|
||||
entries: [
|
||||
|
|
@ -411,9 +411,9 @@ describe('runEpisodicMemoryIndexer', () => {
|
|||
it('counts episodic entry embedding tokens when usage is available', async () => {
|
||||
mockedEmbedMany.mockResolvedValue({ embeddings: [[1, 0]], usage: { tokens: 23 } } as never);
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
const memory = new InMemoryMemory();
|
||||
const [observation] = await memory.appendObservationLogEntries([
|
||||
|
|
@ -738,9 +738,9 @@ describe('runEpisodicMemoryIndexer', () => {
|
|||
usage: { tokens: 2 },
|
||||
} as never);
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
|
||||
await runEpisodicMemoryIndexer({
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { McpConnection } from '../mcp-connection';
|
||||
|
||||
const sseCtor = jest.fn();
|
||||
const streamableHttpCtor = jest.fn();
|
||||
const stdioCtor = jest.fn();
|
||||
const sseCtor = vi.fn();
|
||||
const streamableHttpCtor = vi.fn();
|
||||
const stdioCtor = vi.fn();
|
||||
|
||||
const clientConnect = jest.fn().mockResolvedValue(undefined);
|
||||
const clientListTools = jest.fn().mockResolvedValue({
|
||||
const clientConnect = vi.fn().mockResolvedValue(undefined);
|
||||
const clientListTools = vi.fn().mockResolvedValue({
|
||||
tools: [
|
||||
{ name: 'echo', description: '', inputSchema: { type: 'object' } },
|
||||
{ name: 'add', description: '', inputSchema: { type: 'object' } },
|
||||
{ name: 'subtract', description: '', inputSchema: { type: 'object' } },
|
||||
],
|
||||
});
|
||||
const clientClose = jest.fn().mockResolvedValue(undefined);
|
||||
const clientClose = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
class FakeClient {
|
||||
connect = clientConnect;
|
||||
|
|
@ -20,32 +20,34 @@ class FakeClient {
|
|||
close = clientClose;
|
||||
}
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: jest.fn(() => new FakeClient()),
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: vi.fn(function () {
|
||||
return new FakeClient();
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
|
||||
SSEClientTransport: jest.fn((url: URL, options: unknown) => {
|
||||
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
|
||||
SSEClientTransport: vi.fn(function (url: URL, options: unknown) {
|
||||
sseCtor(url, options);
|
||||
return { type: 'sse', url, options };
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: jest.fn((options: unknown) => {
|
||||
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: vi.fn(function (options: unknown) {
|
||||
stdioCtor(options);
|
||||
return { type: 'stdio', options };
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
|
||||
StreamableHTTPClientTransport: jest.fn((url: URL, options: unknown) => {
|
||||
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
|
||||
StreamableHTTPClientTransport: vi.fn(function (url: URL, options: unknown) {
|
||||
streamableHttpCtor(url, options);
|
||||
return { type: 'streamableHttp', url, options };
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
||||
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
||||
CallToolResultSchema: {},
|
||||
}));
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ describe('McpConnection — custom fetch forwarding', () => {
|
|||
});
|
||||
|
||||
it('forwards `fetch` to StreamableHTTPClientTransport when provided', async () => {
|
||||
const customFetch = jest.fn();
|
||||
const customFetch = vi.fn();
|
||||
const conn = new McpConnection({
|
||||
name: 's1',
|
||||
url: 'https://example.test/mcp',
|
||||
|
|
@ -79,7 +81,7 @@ describe('McpConnection — custom fetch forwarding', () => {
|
|||
});
|
||||
|
||||
it('forwards `fetch` to SSEClientTransport and to its eventSourceInit when provided', async () => {
|
||||
const customFetch = jest.fn();
|
||||
const customFetch = vi.fn();
|
||||
const conn = new McpConnection({
|
||||
name: 's2',
|
||||
url: 'https://example.test/mcp',
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ type ProviderOpts = {
|
|||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
// All providers are mocked via jest.mock so require() inside the registry entries
|
||||
// All providers are mocked via vi.mock so require() inside the registry entries
|
||||
// returns these stubs instead of the real packages.
|
||||
jest.mock('@ai-sdk/anthropic', () => ({
|
||||
vi.mock('@ai-sdk/anthropic', () => ({
|
||||
createAnthropic: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'anthropic',
|
||||
modelId: model,
|
||||
|
|
@ -23,7 +23,7 @@ jest.mock('@ai-sdk/anthropic', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/openai', () => ({
|
||||
vi.mock('@ai-sdk/openai', () => ({
|
||||
createOpenAI: (opts?: ProviderOpts) =>
|
||||
Object.assign(
|
||||
(model: string) => ({
|
||||
|
|
@ -47,7 +47,7 @@ jest.mock('@ai-sdk/openai', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/google', () => ({
|
||||
vi.mock('@ai-sdk/google', () => ({
|
||||
createGoogleGenerativeAI: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'google',
|
||||
modelId: model,
|
||||
|
|
@ -57,7 +57,7 @@ jest.mock('@ai-sdk/google', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/xai', () => ({
|
||||
vi.mock('@ai-sdk/xai', () => ({
|
||||
createXai: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'xai',
|
||||
modelId: model,
|
||||
|
|
@ -67,7 +67,7 @@ jest.mock('@ai-sdk/xai', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/groq', () => ({
|
||||
vi.mock('@ai-sdk/groq', () => ({
|
||||
createGroq: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'groq',
|
||||
modelId: model,
|
||||
|
|
@ -77,7 +77,7 @@ jest.mock('@ai-sdk/groq', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/deepseek', () => ({
|
||||
vi.mock('@ai-sdk/deepseek', () => ({
|
||||
createDeepSeek: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'deepseek',
|
||||
modelId: model,
|
||||
|
|
@ -87,7 +87,7 @@ jest.mock('@ai-sdk/deepseek', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/cohere', () => ({
|
||||
vi.mock('@ai-sdk/cohere', () => ({
|
||||
createCohere: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'cohere',
|
||||
modelId: model,
|
||||
|
|
@ -97,7 +97,7 @@ jest.mock('@ai-sdk/cohere', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/mistral', () => ({
|
||||
vi.mock('@ai-sdk/mistral', () => ({
|
||||
createMistral: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'mistral',
|
||||
modelId: model,
|
||||
|
|
@ -107,7 +107,7 @@ jest.mock('@ai-sdk/mistral', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/gateway', () => ({
|
||||
vi.mock('@ai-sdk/gateway', () => ({
|
||||
createGateway: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'vercel',
|
||||
modelId: model,
|
||||
|
|
@ -118,7 +118,7 @@ jest.mock('@ai-sdk/gateway', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/azure', () => ({
|
||||
vi.mock('@ai-sdk/azure', () => ({
|
||||
createAzure:
|
||||
(opts?: { apiKey?: string; resourceName?: string; apiVersion?: string; baseURL?: string }) =>
|
||||
(model: string) => ({
|
||||
|
|
@ -131,7 +131,7 @@ jest.mock('@ai-sdk/azure', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@openrouter/ai-sdk-provider', () => ({
|
||||
vi.mock('@openrouter/ai-sdk-provider', () => ({
|
||||
createOpenRouter: (opts?: ProviderOpts) => (model: string) => ({
|
||||
provider: 'openrouter',
|
||||
modelId: model,
|
||||
|
|
@ -142,7 +142,7 @@ jest.mock('@openrouter/ai-sdk-provider', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@ai-sdk/amazon-bedrock', () => ({
|
||||
vi.mock('@ai-sdk/amazon-bedrock', () => ({
|
||||
createAmazonBedrock:
|
||||
(opts?: {
|
||||
region?: string;
|
||||
|
|
@ -160,8 +160,8 @@ jest.mock('@ai-sdk/amazon-bedrock', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const mockProxyAgent = jest.fn();
|
||||
jest.mock('undici', () => ({
|
||||
const { mockProxyAgent } = vi.hoisted(() => ({ mockProxyAgent: vi.fn() }));
|
||||
vi.mock('undici', () => ({
|
||||
ProxyAgent: mockProxyAgent,
|
||||
}));
|
||||
|
||||
|
|
@ -197,8 +197,8 @@ describe('createModel', () => {
|
|||
|
||||
it('should pass through a prebuilt LanguageModel', () => {
|
||||
const prebuilt = {
|
||||
doGenerate: jest.fn(),
|
||||
doStream: jest.fn(),
|
||||
doGenerate: vi.fn(),
|
||||
doStream: vi.fn(),
|
||||
specificationVersion: 'v2' as const,
|
||||
modelId: 'custom-model',
|
||||
provider: 'custom',
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ import {
|
|||
type GenerateTextCall = Record<string, unknown>;
|
||||
type GenerateTextResult = { text: string; usage?: { totalTokens?: number } };
|
||||
|
||||
const mockGenerateText = jest.fn<Promise<GenerateTextResult>, [GenerateTextCall]>();
|
||||
const { mockGenerateText } = vi.hoisted(() => ({
|
||||
mockGenerateText: vi.fn<(...args: [GenerateTextCall]) => Promise<GenerateTextResult>>(),
|
||||
}));
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<typeof AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<typeof AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
generateText: async (call: GenerateTextCall): Promise<GenerateTextResult> =>
|
||||
|
|
@ -84,9 +86,9 @@ describe('observation-log observer defaults', () => {
|
|||
usage: { totalTokens: 17 },
|
||||
});
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await createObservationLogObserveFn('openai/gpt-4o-mini')({
|
||||
|
|
@ -249,7 +251,7 @@ describe('runObservationLogObserver', () => {
|
|||
messages: [message('m1', 'user', 'short turn', new Date(2026, 4, 12, 14, 30))],
|
||||
});
|
||||
|
||||
const observe = jest.fn().mockResolvedValue('* CRITICAL (14:30) User said something durable.');
|
||||
const observe = vi.fn().mockResolvedValue('* CRITICAL (14:30) User said something durable.');
|
||||
|
||||
const result = await runObservationLogObserver({
|
||||
memory: store,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ import {
|
|||
type GenerateTextCall = Record<string, unknown>;
|
||||
type GenerateTextResult = { text: string; usage?: { totalTokens?: number } };
|
||||
|
||||
const mockGenerateText = jest.fn<Promise<GenerateTextResult>, [GenerateTextCall]>();
|
||||
const { mockGenerateText } = vi.hoisted(() => ({
|
||||
mockGenerateText: vi.fn<(...args: [GenerateTextCall]) => Promise<GenerateTextResult>>(),
|
||||
}));
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<typeof AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<typeof AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
generateText: async (call: GenerateTextCall): Promise<GenerateTextResult> =>
|
||||
|
|
@ -80,9 +82,9 @@ describe('observation-log reflector defaults', () => {
|
|||
usage: { totalTokens: 19 },
|
||||
});
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await createObservationLogReflectFn('openai/gpt-4o-mini')({
|
||||
|
|
@ -268,7 +270,7 @@ describe('runObservationLogReflector', () => {
|
|||
tokenCount: 2,
|
||||
},
|
||||
]);
|
||||
const reflect = jest.fn().mockResolvedValue('{"drop":[],"merge":[]}');
|
||||
const reflect = vi.fn().mockResolvedValue('{"drop":[],"merge":[]}');
|
||||
|
||||
const result = await runObservationLogReflector({
|
||||
memory: store,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function lockStore(
|
|||
): BuiltObservationLogTaskLockStore {
|
||||
return {
|
||||
acquireObservationLogTaskLock: acquire,
|
||||
releaseObservationLogTaskLock: jest.fn().mockResolvedValue(undefined),
|
||||
releaseObservationLogTaskLock: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -71,9 +71,9 @@ describe('ScopedMemoryTaskRunner', () => {
|
|||
});
|
||||
|
||||
it('skips a task when the store lock is already held', async () => {
|
||||
const acquire = jest.fn().mockResolvedValue(null);
|
||||
const acquire = vi.fn().mockResolvedValue(null);
|
||||
const runner = new ScopedMemoryTaskRunner({ lockStore: lockStore(acquire) });
|
||||
const task = jest.fn().mockResolvedValue(undefined);
|
||||
const task = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const handle = runner.schedule({ taskKind: 'observer', observationScopeId: 'thread-1' }, task);
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ describe('ScopedMemoryTaskRunner', () => {
|
|||
|
||||
it('captures onEvent failures without failing the task lifecycle', async () => {
|
||||
const eventError = new Error('event sink failed');
|
||||
const task = jest.fn(async () => await Promise.resolve('done'));
|
||||
const task = vi.fn(async () => await Promise.resolve('done'));
|
||||
const runner = new ScopedMemoryTaskRunner({
|
||||
onEvent: () => {
|
||||
throw eventError;
|
||||
|
|
@ -167,9 +167,10 @@ describe('ScopedMemoryTaskRunner', () => {
|
|||
});
|
||||
|
||||
it('acquires and releases a store lock around the task', async () => {
|
||||
const acquire = jest.fn<
|
||||
ReturnType<BuiltObservationLogTaskLockStore['acquireObservationLogTaskLock']>,
|
||||
Parameters<BuiltObservationLogTaskLockStore['acquireObservationLogTaskLock']>
|
||||
const acquire = vi.fn<
|
||||
(
|
||||
...args: Parameters<BuiltObservationLogTaskLockStore['acquireObservationLogTaskLock']>
|
||||
) => ReturnType<BuiltObservationLogTaskLockStore['acquireObservationLogTaskLock']>
|
||||
>(
|
||||
async (observationScopeId, taskKind, opts) =>
|
||||
await Promise.resolve(lockHandle(observationScopeId, taskKind, opts.holderId)),
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ type GenerateTextCall = {
|
|||
|
||||
type GenerateTextResult = { text: string; usage?: { totalTokens?: number } };
|
||||
|
||||
const mockGenerateText = jest.fn<Promise<GenerateTextResult>, [GenerateTextCall]>();
|
||||
const { mockGenerateText } = vi.hoisted(() => ({
|
||||
mockGenerateText: vi.fn<(...args: [GenerateTextCall]) => Promise<GenerateTextResult>>(),
|
||||
}));
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<typeof AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<typeof AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
generateText: async (call: GenerateTextCall): Promise<GenerateTextResult> =>
|
||||
|
|
@ -156,9 +158,9 @@ describe('generateTitleFromMessage', () => {
|
|||
it('counts title generation tokens when usage is available', async () => {
|
||||
mockGenerateText.mockResolvedValue({ text: 'Berlin rain alert', usage: { totalTokens: 9 } });
|
||||
const counter = {
|
||||
incrementMessageCount: jest.fn(),
|
||||
incrementToolCallCount: jest.fn(),
|
||||
incrementTokenCount: jest.fn(),
|
||||
incrementMessageCount: vi.fn(),
|
||||
incrementToolCallCount: vi.fn(),
|
||||
incrementTokenCount: vi.fn(),
|
||||
};
|
||||
|
||||
await generateTitleFromMessage(fakeModel, 'Build a daily Berlin rain alert workflow', {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import { toAiSdkTools } from '../tool-adapter';
|
|||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
type AiImport = typeof import('ai');
|
||||
|
||||
const jsonSchemaMock = jest.fn((schema: JSONSchema7) => ({ __jsonSchema: schema }));
|
||||
const { jsonSchemaMock } = vi.hoisted(() => ({
|
||||
jsonSchemaMock: vi.fn((schema: JSONSchema7) => ({ __jsonSchema: schema })),
|
||||
}));
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
tool: jest.fn((config: unknown) => config),
|
||||
tool: vi.fn((config: unknown) => config),
|
||||
jsonSchema: (schema: JSONSchema7) => jsonSchemaMock(schema),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import * as aiModule from 'ai';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import type { BuiltTelemetry } from '../../types';
|
||||
import { Agent } from '../agent';
|
||||
|
||||
// Mock provider packages so createModel() doesn't fail when no API key is set.
|
||||
jest.mock('@ai-sdk/openai', () => ({
|
||||
vi.mock('@ai-sdk/openai', () => ({
|
||||
createOpenAI: () => () => ({ provider: 'openai', modelId: 'mock', specificationVersion: 'v3' }),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
type AiImport = typeof import('ai');
|
||||
|
||||
jest.mock('ai', () => {
|
||||
const actual = jest.requireActual<AiImport>('ai');
|
||||
vi.mock('ai', async () => {
|
||||
const actual = await vi.importActual<AiImport>('ai');
|
||||
return {
|
||||
...actual,
|
||||
generateText: jest.fn(),
|
||||
generateText: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { generateText } = require('ai') as {
|
||||
generateText: jest.Mock;
|
||||
const { generateText } = aiModule as unknown as {
|
||||
generateText: Mock;
|
||||
};
|
||||
|
||||
function makeGenerateSuccess(text = 'OK') {
|
||||
|
|
@ -46,13 +48,13 @@ function makeTelemetry(functionId: string): BuiltTelemetry {
|
|||
recordInputs: true,
|
||||
recordOutputs: true,
|
||||
integrations: [],
|
||||
tracer: { startSpan: jest.fn() },
|
||||
tracer: { startSpan: vi.fn() },
|
||||
};
|
||||
}
|
||||
|
||||
describe('Agent telemetry', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('updates telemetry on an already-built runtime', async () => {
|
||||
|
|
@ -66,9 +68,8 @@ describe('Agent telemetry', () => {
|
|||
agent.telemetry(makeTelemetry('updated-agent'));
|
||||
await agent.generate('second');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const firstCall = generateText.mock.calls[0][0] as Record<string, unknown>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
|
||||
const secondCall = generateText.mock.calls[1][0] as Record<string, unknown>;
|
||||
const firstTelemetry = firstCall.experimental_telemetry as Record<string, unknown>;
|
||||
const secondTelemetry = secondCall.experimental_telemetry as Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ type EmbeddingProviderOpts = {
|
|||
baseURL?: string;
|
||||
};
|
||||
|
||||
jest.mock('@ai-sdk/openai', () => ({
|
||||
vi.mock('@ai-sdk/openai', () => ({
|
||||
createOpenAI: (opts?: EmbeddingProviderOpts) =>
|
||||
Object.assign(
|
||||
(model: string) => ({
|
||||
|
|
@ -43,12 +43,12 @@ jest.mock('@ai-sdk/openai', () => ({
|
|||
|
||||
describe('Memory builder — episodic memory', () => {
|
||||
const minimalBackend = {
|
||||
getThread: jest.fn().mockResolvedValue(null),
|
||||
saveThread: jest.fn().mockResolvedValue({}),
|
||||
deleteThread: jest.fn().mockResolvedValue(undefined),
|
||||
getMessages: jest.fn().mockResolvedValue([]),
|
||||
saveMessages: jest.fn().mockResolvedValue(undefined),
|
||||
deleteMessages: jest.fn().mockResolvedValue(undefined),
|
||||
getThread: vi.fn().mockResolvedValue(null),
|
||||
saveThread: vi.fn().mockResolvedValue({}),
|
||||
deleteThread: vi.fn().mockResolvedValue(undefined),
|
||||
getMessages: vi.fn().mockResolvedValue([]),
|
||||
saveMessages: vi.fn().mockResolvedValue(undefined),
|
||||
deleteMessages: vi.fn().mockResolvedValue(undefined),
|
||||
describe: () => ({
|
||||
name: 'minimal',
|
||||
constructorName: 'MinimalMemory',
|
||||
|
|
|
|||
|
|
@ -160,12 +160,12 @@ describe('Memory builder — observation log memory', () => {
|
|||
|
||||
it('rejects backends that do not implement the observation-log store', () => {
|
||||
const minimalBackend = {
|
||||
getThread: jest.fn().mockResolvedValue(null),
|
||||
saveThread: jest.fn().mockResolvedValue({}),
|
||||
deleteThread: jest.fn().mockResolvedValue(undefined),
|
||||
getMessages: jest.fn().mockResolvedValue([]),
|
||||
saveMessages: jest.fn().mockResolvedValue(undefined),
|
||||
deleteMessages: jest.fn().mockResolvedValue(undefined),
|
||||
getThread: vi.fn().mockResolvedValue(null),
|
||||
saveThread: vi.fn().mockResolvedValue({}),
|
||||
deleteThread: vi.fn().mockResolvedValue(undefined),
|
||||
getMessages: vi.fn().mockResolvedValue([]),
|
||||
saveMessages: vi.fn().mockResolvedValue(undefined),
|
||||
deleteMessages: vi.fn().mockResolvedValue(undefined),
|
||||
describe: () => ({
|
||||
name: 'minimal',
|
||||
constructorName: 'MinimalMemory',
|
||||
|
|
|
|||
|
|
@ -35,23 +35,20 @@ describe('Telemetry builder', () => {
|
|||
});
|
||||
|
||||
it('accepts a pre-built tracer', async () => {
|
||||
const fakeTracer = { startSpan: jest.fn() };
|
||||
const fakeTracer = { startSpan: vi.fn() };
|
||||
const built = await new Telemetry().tracer(fakeTracer).build();
|
||||
expect(built.tracer).toBe(fakeTracer);
|
||||
});
|
||||
|
||||
it('throws when both .tracer() and .otlpEndpoint() are set', async () => {
|
||||
await expect(
|
||||
new Telemetry()
|
||||
.tracer({ startSpan: jest.fn() })
|
||||
.otlpEndpoint('http://localhost:4318')
|
||||
.build(),
|
||||
new Telemetry().tracer({ startSpan: vi.fn() }).otlpEndpoint('http://localhost:4318').build(),
|
||||
).rejects.toThrow('Cannot set both .tracer() and .otlpEndpoint()');
|
||||
});
|
||||
|
||||
it('collects multiple integrations', async () => {
|
||||
const int1: TelemetryIntegration = { onStart: jest.fn() };
|
||||
const int2: TelemetryIntegration = { onFinish: jest.fn() };
|
||||
const int1: TelemetryIntegration = { onStart: vi.fn() };
|
||||
const int2: TelemetryIntegration = { onFinish: vi.fn() };
|
||||
const built = await new Telemetry().integration(int1).integration(int2).build();
|
||||
expect(built.integrations).toHaveLength(2);
|
||||
});
|
||||
|
|
@ -89,7 +86,7 @@ describe('Telemetry — redaction wrapping', () => {
|
|||
});
|
||||
|
||||
it('does not wrap integrations when .redact() is not set', async () => {
|
||||
const integration: TelemetryIntegration = { onStart: jest.fn() };
|
||||
const integration: TelemetryIntegration = { onStart: vi.fn() };
|
||||
const built = await new Telemetry().integration(integration).build();
|
||||
// The integration should be a copy (not the same reference due to spread) but functionally identical
|
||||
expect(built.integrations[0].onStart).toBe(integration.onStart);
|
||||
|
|
@ -154,12 +151,12 @@ describe('Telemetry — redaction wrapping', () => {
|
|||
|
||||
describe('Telemetry.shutdown()', () => {
|
||||
it('calls provider.shutdown() when provider exists', async () => {
|
||||
const shutdownMock = jest.fn().mockResolvedValue(undefined);
|
||||
const shutdownMock = vi.fn().mockResolvedValue(undefined);
|
||||
const built = await new Telemetry().build();
|
||||
// Manually inject a mock provider
|
||||
const withProvider = {
|
||||
...built,
|
||||
provider: { forceFlush: jest.fn(), shutdown: shutdownMock },
|
||||
provider: { forceFlush: vi.fn(), shutdown: shutdownMock },
|
||||
};
|
||||
await Telemetry.shutdown(withProvider);
|
||||
expect(shutdownMock).toHaveBeenCalled();
|
||||
|
|
@ -174,11 +171,11 @@ describe('Telemetry.shutdown()', () => {
|
|||
|
||||
describe('Telemetry.forceFlush()', () => {
|
||||
it('calls provider.forceFlush() when provider exists', async () => {
|
||||
const forceFlushMock = jest.fn().mockResolvedValue(undefined);
|
||||
const forceFlushMock = vi.fn().mockResolvedValue(undefined);
|
||||
const built = await new Telemetry().build();
|
||||
const withProvider = {
|
||||
...built,
|
||||
provider: { forceFlush: forceFlushMock, shutdown: jest.fn() },
|
||||
provider: { forceFlush: forceFlushMock, shutdown: vi.fn() },
|
||||
};
|
||||
|
||||
await Telemetry.forceFlush(withProvider);
|
||||
|
|
@ -191,8 +188,8 @@ describe('Telemetry.forceFlush()', () => {
|
|||
const withProvider = {
|
||||
...built,
|
||||
provider: {
|
||||
forceFlush: jest.fn().mockRejectedValue(new Error('flush failed')),
|
||||
shutdown: jest.fn(),
|
||||
forceFlush: vi.fn().mockRejectedValue(new Error('flush failed')),
|
||||
shutdown: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Mock } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { BuiltTelemetry, BuiltTool, InterruptibleToolContext, ToolContext } from '../../types';
|
||||
|
|
@ -19,8 +20,8 @@ function makeBuiltTool(overrides: Partial<BuiltTool> = {}): BuiltTool {
|
|||
};
|
||||
}
|
||||
|
||||
function makeCtx(resumeData?: unknown): { ctx: InterruptibleToolContext; suspendMock: jest.Mock } {
|
||||
const suspendMock = jest.fn().mockImplementation(async (payload: unknown) => {
|
||||
function makeCtx(resumeData?: unknown): { ctx: InterruptibleToolContext; suspendMock: Mock } {
|
||||
const suspendMock = vi.fn().mockImplementation(async (payload: unknown) => {
|
||||
return await Promise.resolve({ __suspended: true, payload });
|
||||
});
|
||||
const ctx: InterruptibleToolContext = {
|
||||
|
|
@ -279,7 +280,7 @@ describe('wrapToolForApproval — telemetry propagation', () => {
|
|||
recordInputs: true,
|
||||
recordOutputs: true,
|
||||
integrations: [],
|
||||
tracer: { startSpan: jest.fn() },
|
||||
tracer: { startSpan: vi.fn() },
|
||||
};
|
||||
|
||||
it('forwards parentTelemetry to the original handler when approval is not needed', async () => {
|
||||
|
|
|
|||
|
|
@ -156,11 +156,9 @@ description: Has no instructions.
|
|||
});
|
||||
|
||||
it('uses locale-independent ordering for registry hashes', () => {
|
||||
const localeCompareSpy = jest
|
||||
.spyOn(String.prototype, 'localeCompare')
|
||||
.mockImplementation(() => {
|
||||
throw new Error('localeCompare must not be used for registry ordering');
|
||||
});
|
||||
const localeCompareSpy = vi.spyOn(String.prototype, 'localeCompare').mockImplementation(() => {
|
||||
throw new Error('localeCompare must not be used for registry ordering');
|
||||
});
|
||||
|
||||
try {
|
||||
expect(() =>
|
||||
|
|
@ -416,7 +414,7 @@ Use the workflow SDK.`,
|
|||
instructions: 'Full private skill body: Extract decisions.',
|
||||
},
|
||||
]);
|
||||
const prepare = jest.fn(async () => {
|
||||
const prepare = vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
source.registry = {
|
||||
...source.registry,
|
||||
|
|
@ -460,7 +458,7 @@ Use the workflow SDK.`,
|
|||
instructions: 'Extract decisions.',
|
||||
},
|
||||
]);
|
||||
const prepare = jest.fn(async () => {
|
||||
const prepare = vi.fn(async () => {
|
||||
await Promise.resolve();
|
||||
source.registry = {
|
||||
...source.registry,
|
||||
|
|
@ -542,7 +540,7 @@ Use the workflow SDK.`,
|
|||
},
|
||||
},
|
||||
]);
|
||||
const loadFile = jest.fn(
|
||||
const loadFile = vi.fn(
|
||||
async (_skillId: string, filePath: string) =>
|
||||
await Promise.resolve({
|
||||
skillId: 'summarize_notes',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"types": ["node", "vitest/globals"],
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
|
|
|
|||
64
packages/@n8n/agents/vite.config.ts
Normal file
64
packages/@n8n/agents/vite.config.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { createVitestConfig } from '@n8n/vitest-config/node';
|
||||
import type { Plugin } from 'vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
import { configDefaults } from 'vitest/config';
|
||||
|
||||
/**
|
||||
* Source modules lazy-load their heavy deps via `require()` (intentional runtime
|
||||
* lazy-loading — see `lazy-ai.ts` and `model-factory.ts`). Under Vitest, source is
|
||||
* ESM and gets a Node `createRequire`, which `vi.mock()` does not intercept, so the
|
||||
* AI SDK / provider mocks in the unit tests never apply.
|
||||
*
|
||||
* This plugin rewrites those `require('spec')` calls to eager static imports at
|
||||
* transform time (test runtime only — the files on disk are untouched), which Vite
|
||||
* resolves and `vi.mock` can intercept. Dynamic `require(expr)` calls are routed
|
||||
* through the same eager-import map. The integration config does not use this plugin
|
||||
* because those tests deliberately exercise the real provider packages.
|
||||
*/
|
||||
const REWRITE_REQUIRE_TARGETS = [
|
||||
'/src/runtime/lazy-ai.ts',
|
||||
'/src/runtime/model-factory.ts',
|
||||
'/src/utils/parse.ts',
|
||||
];
|
||||
|
||||
function rewriteSourceRequire(): Plugin {
|
||||
return {
|
||||
name: 'rewrite-source-require',
|
||||
enforce: 'pre',
|
||||
transform(code, id) {
|
||||
const normalized = id.replace(/\\/g, '/');
|
||||
if (!REWRITE_REQUIRE_TARGETS.some((target) => normalized.endsWith(target))) return null;
|
||||
|
||||
const literalRequire = /require\((['"])([^'"]+)\1\)/g;
|
||||
const specs: string[] = [];
|
||||
for (const match of code.matchAll(literalRequire)) {
|
||||
if (!specs.includes(match[2])) specs.push(match[2]);
|
||||
}
|
||||
if (!specs.length) return null;
|
||||
|
||||
const imports = specs.map((spec, i) => `import * as __req_${i} from '${spec}';`).join('\n');
|
||||
const mapEntries = specs.map((spec, i) => `${JSON.stringify(spec)}: __req_${i}`).join(', ');
|
||||
const rewritten = code
|
||||
.replace(literalRequire, (_full, _quote, spec) => `__requireMap[${JSON.stringify(spec)}]`)
|
||||
.replace(/require\(([^'")][^)]*)\)/g, (_full, expr) => `__requireMap[${expr}]`);
|
||||
|
||||
return {
|
||||
code: `${imports}\nconst __requireMap = { ${mapEntries} };\n${rewritten}`,
|
||||
map: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default mergeConfig(
|
||||
createVitestConfig({
|
||||
// The n8n root jest.config sets `restoreMocks: true`, and test files silently rely on
|
||||
// it — omit this and mocks bleed between tests.
|
||||
restoreMocks: true,
|
||||
// Integration tests run via vitest.integration.config.mjs (real providers, long timeouts).
|
||||
exclude: [...configDefaults.exclude, '**/__tests__/integration/**'],
|
||||
}),
|
||||
{
|
||||
plugins: [rewriteSourceRequire()],
|
||||
},
|
||||
);
|
||||
|
|
@ -771,9 +771,21 @@ importers:
|
|||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
'@n8n/vitest-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vitest-config
|
||||
'@types/json-schema':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
vitest-mock-extended:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0(typescript@6.0.2)(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
|
||||
|
||||
packages/@n8n/ai-node-sdk:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user