0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
976 lines
31 KiB
TypeScript
976 lines
31 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { GolangProvider } from '../../src/providers/golangCompletion';
|
|
|
|
// Hoisted mock functions
|
|
const mockExecFile = vi.hoisted(() => vi.fn());
|
|
const mockGetCache = vi.hoisted(() => vi.fn());
|
|
const mockIsCacheEnabled = vi.hoisted(() => vi.fn());
|
|
const fsMocks = vi.hoisted(() => ({
|
|
readFileSync: vi.fn(),
|
|
mkdtempSync: vi.fn(),
|
|
existsSync: vi.fn(),
|
|
rmSync: vi.fn(),
|
|
readdirSync: vi.fn(),
|
|
copyFileSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('child_process', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('child_process')>();
|
|
return {
|
|
...actual,
|
|
default: {
|
|
...actual,
|
|
execFile: mockExecFile,
|
|
},
|
|
execFile: mockExecFile,
|
|
};
|
|
});
|
|
|
|
vi.mock('../../src/cache', () => ({
|
|
getCache: mockGetCache,
|
|
isCacheEnabled: mockIsCacheEnabled,
|
|
}));
|
|
|
|
vi.mock('../../src/logger', () => ({
|
|
default: {
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('fs', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('fs')>();
|
|
return {
|
|
...actual,
|
|
default: {
|
|
...actual,
|
|
...fsMocks,
|
|
},
|
|
...fsMocks,
|
|
};
|
|
});
|
|
vi.mock('fs/promises', () => {
|
|
// Wrap each sync mock so the corresponding fs/promises export returns a real
|
|
// Promise (or rejects), matching the actual API contract.
|
|
const access = vi.fn(async (filePath: any) => {
|
|
if (fsMocks.existsSync(filePath)) {
|
|
return undefined;
|
|
}
|
|
throw Object.assign(new Error(`ENOENT: no such file or directory, access '${filePath}'`), {
|
|
code: 'ENOENT',
|
|
});
|
|
});
|
|
const readFile = vi.fn(async (...args: any[]) => fsMocks.readFileSync(...args));
|
|
const readdir = vi.fn(async (...args: any[]) => fsMocks.readdirSync(...args));
|
|
const mkdtemp = vi.fn(async (...args: any[]) => fsMocks.mkdtempSync(...args));
|
|
const mkdir = vi.fn(async (...args: any[]) => fsMocks.mkdirSync(...args));
|
|
const copyFile = vi.fn(async (...args: any[]) => fsMocks.copyFileSync(...args));
|
|
const rm = vi.fn(async (...args: any[]) => fsMocks.rmSync(...args));
|
|
const promiseApi = { readFile, readdir, mkdtemp, mkdir, copyFile, rm, access };
|
|
return { default: promiseApi, ...promiseApi };
|
|
});
|
|
vi.mock('path');
|
|
|
|
vi.mock('../../src/util', async () => {
|
|
const actual = await vi.importActual('../../src/util');
|
|
return {
|
|
...actual,
|
|
parsePathOrGlob: vi.fn(() => ({
|
|
extension: 'go',
|
|
functionName: undefined,
|
|
isPathPattern: false,
|
|
filePath: '/absolute/path/to/script.go',
|
|
})),
|
|
};
|
|
});
|
|
|
|
describe('GolangProvider', () => {
|
|
const mockReadFileSync = vi.mocked(fs.readFileSync);
|
|
const mockResolve = vi.mocked(path.resolve);
|
|
const mockMkdtempSync = vi.mocked(fs.mkdtempSync);
|
|
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
const mockRmSync = vi.mocked(fs.rmSync);
|
|
const mockReaddirSync = vi.mocked(fs.readdirSync);
|
|
const mockCopyFileSync = vi.mocked(fs.copyFileSync);
|
|
const mockMkdirSync = vi.mocked(fs.mkdirSync);
|
|
const mockDirname = vi.mocked(path.dirname);
|
|
const mockJoin = vi.mocked(path.join);
|
|
const mockRelative = vi.mocked(path.relative);
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
mockExecFile.mockReset();
|
|
mockGetCache.mockReset();
|
|
mockIsCacheEnabled.mockReset();
|
|
|
|
// Reset all mocks to default behavior
|
|
mockGetCache.mockResolvedValue({
|
|
get: vi.fn(),
|
|
set: vi.fn(),
|
|
} as never);
|
|
mockIsCacheEnabled.mockReturnValue(false);
|
|
|
|
// Mock readFileSync to return content for all files
|
|
mockReadFileSync.mockImplementation(function (filePath: fs.PathOrFileDescriptor) {
|
|
const pathStr = filePath.toString();
|
|
if (pathStr.includes('wrapper.go')) {
|
|
return 'package main\n// Mock wrapper.go content';
|
|
}
|
|
if (pathStr.includes('script.go')) {
|
|
return 'package main\n// Mock script.go content';
|
|
}
|
|
return 'mock file content';
|
|
});
|
|
|
|
const mockParsePathOrGlob = vi.mocked((await import('../../src/util')).parsePathOrGlob);
|
|
mockParsePathOrGlob.mockImplementation(function (basePath: string, runPath: string) {
|
|
if (!basePath && runPath === 'script.go') {
|
|
return {
|
|
filePath: 'script.go',
|
|
functionName: undefined,
|
|
isPathPattern: false,
|
|
extension: 'go',
|
|
};
|
|
}
|
|
return {
|
|
filePath: runPath.replace(/^(file:\/\/|golang:)/, '').split(':')[0],
|
|
functionName: runPath.includes(':') ? runPath.split(':')[1] : undefined,
|
|
isPathPattern: false,
|
|
extension: 'go',
|
|
};
|
|
});
|
|
|
|
// Mock path operations
|
|
mockResolve.mockImplementation(function (p: string) {
|
|
if (!p || typeof p !== 'string') {
|
|
return '/absolute/path/undefined';
|
|
}
|
|
if (p === 'script.go') {
|
|
return 'script.go';
|
|
}
|
|
if (p.includes('script.go')) {
|
|
return '/absolute/path/to/script.go';
|
|
}
|
|
if (p.includes('wrapper.go')) {
|
|
return '/absolute/path/to/wrapper.go';
|
|
}
|
|
return p.startsWith('/') ? p : `/absolute/path/to/${p}`;
|
|
});
|
|
|
|
mockRelative.mockImplementation(function (from: string, to: string) {
|
|
if (!from && to === 'script.go') {
|
|
return 'script.go';
|
|
}
|
|
if (to.includes('script.go')) {
|
|
return 'script.go';
|
|
}
|
|
return to;
|
|
});
|
|
|
|
mockDirname.mockImplementation(function (p: string) {
|
|
const paths: Record<string, string> = {
|
|
'/absolute/path/to/script.go': '/absolute/path/to',
|
|
'/absolute/path/to': '/absolute/path',
|
|
'/absolute/path': '/absolute',
|
|
'/absolute': '/',
|
|
};
|
|
return paths[p] || p.split('/').slice(0, -1).join('/') || '/';
|
|
});
|
|
|
|
mockJoin.mockImplementation(function (...paths: string[]) {
|
|
const validPaths = paths.filter((p) => p !== undefined && p !== null);
|
|
if (validPaths.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
// Handle specific test cases
|
|
if (validPaths.includes('go.mod')) {
|
|
const basePath = validPaths.find((p) => p !== 'go.mod') || '/absolute/path/to';
|
|
return `${basePath}/go.mod`;
|
|
}
|
|
if (validPaths.includes('wrapper.go')) {
|
|
return '/tmp/golang-provider-xyz/wrapper.go';
|
|
}
|
|
|
|
return validPaths.join('/').replace(/\/+/g, '/');
|
|
});
|
|
|
|
// Mock file system operations
|
|
mockMkdtempSync.mockReturnValue('/tmp/golang-provider-xyz');
|
|
|
|
mockExistsSync.mockImplementation(function (p: fs.PathLike) {
|
|
const pathStr = p.toString();
|
|
// Always return true for go.mod files and wrapper.go
|
|
if (pathStr.includes('go.mod') || pathStr.includes('wrapper.go')) {
|
|
return true;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Mock directory reading to return predictable results
|
|
mockReaddirSync.mockImplementation(function (p: fs.PathOrFileDescriptor) {
|
|
const pathStr = p.toString();
|
|
if (pathStr.includes('nested')) {
|
|
return [{ name: 'nested-file.go', isDirectory: () => false }] as any;
|
|
}
|
|
return [{ name: 'test.go', isDirectory: () => false }] as any;
|
|
});
|
|
|
|
// Mock directory and file operations
|
|
mockMkdirSync.mockImplementation(function () {
|
|
return undefined;
|
|
});
|
|
mockCopyFileSync.mockImplementation(function () {
|
|
return undefined;
|
|
});
|
|
mockRmSync.mockImplementation(function () {
|
|
return undefined;
|
|
});
|
|
|
|
// Mock exec with proper async behavior
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
args: any,
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
// Handle both execFile(file, args, callback) and execFile(file, args, options, callback) signatures
|
|
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
|
|
// Use setImmediate to ensure proper async behavior
|
|
setImmediate(() => {
|
|
try {
|
|
if (file === 'go' && args[0] === 'build') {
|
|
callback(null, { stdout: '', stderr: '' }, '');
|
|
} else if (file.includes('golang_wrapper')) {
|
|
callback(null, { stdout: '{"output":"test output"}', stderr: '' }, '');
|
|
} else {
|
|
callback(new Error('test error'), { stdout: '', stderr: '' }, '');
|
|
}
|
|
} catch (error) {
|
|
callback(error, { stdout: '', stderr: '' }, '');
|
|
}
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should initialize with correct properties', () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
id: 'testId',
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
expect(provider.id()).toBe('testId');
|
|
});
|
|
|
|
it('should initialize with golang: syntax', () => {
|
|
const provider = new GolangProvider('golang:script.go', {
|
|
id: 'testId',
|
|
config: { basePath: '/base' },
|
|
});
|
|
expect(provider.id()).toBe('testId');
|
|
});
|
|
|
|
it('should initialize with file:// prefix', () => {
|
|
const provider = new GolangProvider('file://script.go', {
|
|
id: 'testId',
|
|
config: { basePath: '/base' },
|
|
});
|
|
expect(provider.id()).toBe('testId');
|
|
});
|
|
|
|
it('should initialize with file:// prefix and function name', () => {
|
|
const provider = new GolangProvider('file://script.go:function_name', {
|
|
id: 'testId',
|
|
config: { basePath: '/base' },
|
|
});
|
|
expect(provider.id()).toBe('testId');
|
|
});
|
|
|
|
it('should handle undefined basePath and use default id', () => {
|
|
const provider = new GolangProvider('script.go');
|
|
expect(provider.id()).toBe('golang:script.go:default');
|
|
expect(provider.config).toEqual({});
|
|
});
|
|
|
|
it('should use class id() method when no id is provided in options', () => {
|
|
const provider = new GolangProvider('script.go:custom_function');
|
|
expect(provider.id()).toBe('golang:script.go:custom_function');
|
|
});
|
|
|
|
it('should allow id() override and later retrieval', () => {
|
|
const provider = new GolangProvider('script.go');
|
|
expect(provider.id()).toBe('golang:script.go:default');
|
|
|
|
const originalId = provider.id;
|
|
provider.id = () => 'overridden-id';
|
|
|
|
expect(provider.id()).toBe('overridden-id');
|
|
|
|
provider.id = originalId;
|
|
expect(provider.id()).toBe('golang:script.go:default');
|
|
});
|
|
});
|
|
|
|
describe('caching', () => {
|
|
it('should use cached result when available', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockIsCacheEnabled.mockReturnValue(true);
|
|
const mockCache = {
|
|
get: vi.fn().mockResolvedValue(JSON.stringify({ output: 'cached result' })),
|
|
set: vi.fn(),
|
|
};
|
|
mockGetCache.mockResolvedValue(mockCache as never);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining('golang:'));
|
|
expect(mockExecFile).not.toHaveBeenCalled();
|
|
expect(result.cached).toBe(true);
|
|
expect(result).toEqual({ output: 'cached result', cached: true });
|
|
});
|
|
|
|
it('should handle cache errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockIsCacheEnabled.mockReturnValue(true);
|
|
const mockCache = {
|
|
get: vi.fn().mockRejectedValue(new Error('Cache error')),
|
|
set: vi.fn(),
|
|
};
|
|
mockGetCache.mockResolvedValue(mockCache as never);
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('Cache error');
|
|
});
|
|
|
|
it('should handle cache set errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockIsCacheEnabled.mockReturnValue(true);
|
|
const mockCache = {
|
|
get: vi.fn().mockResolvedValue(null),
|
|
set: vi.fn().mockRejectedValue(new Error('Cache set error')),
|
|
};
|
|
mockGetCache.mockResolvedValue(mockCache as never);
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('Cache set error');
|
|
});
|
|
|
|
it('should not cache results that contain errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockIsCacheEnabled.mockReturnValue(true);
|
|
const mockCache = {
|
|
get: vi.fn().mockResolvedValue(null),
|
|
set: vi.fn(),
|
|
};
|
|
mockGetCache.mockResolvedValue(mockCache as never);
|
|
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
if (file.includes('golang_wrapper')) {
|
|
callback(null, { stdout: '{"error":"test error in result"}', stderr: '' }, '');
|
|
} else {
|
|
callback(null, { stdout: '', stderr: '' }, '');
|
|
}
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result).toEqual({ error: 'test error in result' });
|
|
expect(mockCache.set).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle circular references in vars when cache is disabled', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockIsCacheEnabled.mockReturnValue(false);
|
|
|
|
const circularObj: any = { name: 'circular' };
|
|
circularObj.self = circularObj;
|
|
|
|
const spy = vi.spyOn(provider as any, 'executeGolangScript').mockImplementation(function () {
|
|
return Promise.resolve({ output: 'mocked result' });
|
|
});
|
|
|
|
const result = await provider.callApi('test prompt', {
|
|
prompt: {
|
|
raw: 'test prompt',
|
|
label: 'test',
|
|
},
|
|
vars: { circular: circularObj },
|
|
});
|
|
|
|
expect(result).toEqual({ output: 'mocked result' });
|
|
expect(spy).toHaveBeenCalledWith(
|
|
'test prompt',
|
|
expect.objectContaining({
|
|
vars: expect.objectContaining({ circular: expect.anything() }),
|
|
}),
|
|
'call_api',
|
|
);
|
|
|
|
spy.mockRestore();
|
|
});
|
|
|
|
it('should execute script directly when cache is not enabled', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockIsCacheEnabled.mockReturnValue(false);
|
|
mockGetCache.mockResolvedValue({
|
|
get: vi.fn().mockResolvedValue(null),
|
|
set: vi.fn(),
|
|
} as never);
|
|
|
|
mockExecFile.mockImplementation(((
|
|
_file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() =>
|
|
callback(null, { stdout: '{"output":"direct execution result"}', stderr: '' }, ''),
|
|
);
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
|
|
expect(result).toEqual({ output: 'direct execution result' });
|
|
const mockCache = await mockGetCache.mock.results[0].value;
|
|
expect(mockCache.set).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('cleanup', () => {
|
|
it('should clean up on error', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockExecFile.mockImplementation(((
|
|
_file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
callback(new Error('test error'), { stdout: '', stderr: '' }, '');
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow(
|
|
'Error running Golang script: test error',
|
|
);
|
|
|
|
expect(mockRmSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('golang-provider'),
|
|
expect.objectContaining({ recursive: true, force: true }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('API methods', () => {
|
|
it('should call callEmbeddingApi successfully', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
callback(
|
|
null,
|
|
{
|
|
stdout: file.includes('golang_wrapper') ? '{"embedding":[0.1,0.2,0.3]}' : '',
|
|
stderr: '',
|
|
},
|
|
'',
|
|
);
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callEmbeddingApi('test prompt');
|
|
expect(result).toEqual({ embedding: [0.1, 0.2, 0.3] });
|
|
});
|
|
|
|
it('should call callClassificationApi successfully', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
callback(
|
|
null,
|
|
{
|
|
stdout: file.includes('golang_wrapper') ? '{"classification":"test_class"}' : '',
|
|
stderr: '',
|
|
},
|
|
'',
|
|
);
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callClassificationApi('test prompt');
|
|
expect(result).toEqual({ classification: 'test_class' });
|
|
});
|
|
|
|
it('should handle stderr output without failing', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
callback(
|
|
null,
|
|
{
|
|
stdout: file.includes('golang_wrapper') ? '{"output":"test"}' : '',
|
|
stderr: 'warning: some go warning',
|
|
},
|
|
'',
|
|
);
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result).toEqual({ output: 'test' });
|
|
});
|
|
});
|
|
|
|
describe('findModuleRoot', () => {
|
|
it('should throw error when go.mod is not found', async () => {
|
|
mockExistsSync.mockImplementation(function () {
|
|
return false;
|
|
});
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow(
|
|
'Could not find go.mod file in any parent directory',
|
|
);
|
|
});
|
|
|
|
it('should find go.mod in parent directory', async () => {
|
|
const checkedPaths: string[] = [];
|
|
|
|
mockExistsSync.mockImplementation(function (p: fs.PathLike) {
|
|
const pathStr = p.toString();
|
|
checkedPaths.push(pathStr);
|
|
// Return true for go.mod and wrapper.go files
|
|
return pathStr.endsWith('/absolute/path/to/go.mod') || pathStr.includes('wrapper.go');
|
|
});
|
|
|
|
mockDirname.mockImplementation(function (p: string) {
|
|
return p.split('/').slice(0, -1).join('/');
|
|
});
|
|
|
|
mockResolve.mockImplementation(function (p: string) {
|
|
return p.startsWith('/') ? p : `/absolute/path/to/${p}`;
|
|
});
|
|
|
|
mockJoin.mockImplementation(function (...paths: string[]) {
|
|
return paths.join('/').replace(/\/+/g, '/');
|
|
});
|
|
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to/subdir' },
|
|
});
|
|
|
|
mockExecFile.mockImplementation(((
|
|
_file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => callback(null, { stdout: '{"output":"test"}', stderr: '' }, ''));
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
const result = await provider.callApi('test prompt');
|
|
expect(result).toEqual({ output: 'test' });
|
|
|
|
expect(checkedPaths).toContain('/absolute/path/to/go.mod');
|
|
expect(mockExistsSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('/absolute/path/to/go.mod'),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('script execution', () => {
|
|
it('should handle JSON parsing errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => {
|
|
callback(
|
|
null,
|
|
{
|
|
stdout: file.includes('golang_wrapper') ? 'invalid json' : '',
|
|
stderr: '',
|
|
},
|
|
'',
|
|
);
|
|
});
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('Error running Golang script');
|
|
});
|
|
|
|
it('should use custom function name when specified', async () => {
|
|
const mockParsePathOrGlob = vi.mocked((await import('../../src/util')).parsePathOrGlob);
|
|
mockParsePathOrGlob.mockReturnValueOnce({
|
|
filePath: '/absolute/path/to/script.go',
|
|
functionName: 'custom_function',
|
|
isPathPattern: false,
|
|
extension: 'go',
|
|
});
|
|
|
|
const provider = new GolangProvider('script.go:custom_function', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
let executedFunctionName = '';
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
if (file.includes('golang_wrapper')) {
|
|
executedFunctionName = args[1]; // args[1] is the function name
|
|
}
|
|
setImmediate(() => callback(null, { stdout: '{"output":"test"}', stderr: '' }, ''));
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await provider.callApi('test prompt');
|
|
expect(executedFunctionName).toBe('custom_function');
|
|
});
|
|
|
|
it('should use custom go executable when specified', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: {
|
|
basePath: '/absolute/path/to',
|
|
goExecutable: '/custom/go',
|
|
},
|
|
});
|
|
|
|
let buildFile = '';
|
|
mockExecFile.mockImplementation(((
|
|
file: string,
|
|
args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
if (args?.[0] === 'build') {
|
|
buildFile = file;
|
|
}
|
|
setImmediate(() => callback(null, { stdout: '{"output":"test"}', stderr: '' }, ''));
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await provider.callApi('test prompt');
|
|
expect(buildFile).toBe('/custom/go');
|
|
});
|
|
|
|
it('should handle circular references in args', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
// Create circular reference
|
|
const circularObj: any = { name: 'circular' };
|
|
circularObj.self = circularObj;
|
|
|
|
// We need to access a private method, so we'll create a spy using any type assertions
|
|
const spy = vi.spyOn(provider as any, 'executeGolangScript').mockImplementation(function () {
|
|
return Promise.resolve({ output: 'mocked result' });
|
|
});
|
|
|
|
// This should not throw even with circular references
|
|
const result = await provider.callApi('test prompt', {
|
|
prompt: {
|
|
raw: 'test prompt',
|
|
label: 'test',
|
|
},
|
|
vars: { circular: circularObj },
|
|
});
|
|
|
|
// Verify the result and that executeGolangScript was called
|
|
expect(result).toEqual({ output: 'mocked result' });
|
|
expect(spy).toHaveBeenCalledWith(
|
|
'test prompt',
|
|
expect.objectContaining({
|
|
vars: expect.objectContaining({ circular: expect.anything() }),
|
|
}),
|
|
'call_api',
|
|
);
|
|
|
|
// Restore the original implementation
|
|
spy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('file operations', () => {
|
|
it('should handle directory copy errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
mockReaddirSync.mockImplementation(function () {
|
|
throw new Error('Directory read error');
|
|
});
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('Directory read error');
|
|
});
|
|
|
|
it('should handle file copy errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockCopyFileSync.mockImplementation(function () {
|
|
throw new Error('File copy error');
|
|
});
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('File copy error');
|
|
});
|
|
|
|
it('should copy main.go files without transformation', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
const mainGoContent = `
|
|
package main
|
|
|
|
// CallApi declaration
|
|
var CallApi = func(prompt string) string {
|
|
return "test"
|
|
}
|
|
|
|
func main() {
|
|
// Some code
|
|
}`;
|
|
|
|
mockReaddirSync.mockReturnValue([{ name: 'main.go', isDirectory: () => false }] as any);
|
|
|
|
mockReadFileSync.mockImplementation(function (p: fs.PathOrFileDescriptor) {
|
|
if (p.toString().endsWith('main.go')) {
|
|
return mainGoContent;
|
|
}
|
|
return 'other content';
|
|
});
|
|
|
|
const copiedFiles: { src: string; dest: string }[] = [];
|
|
mockCopyFileSync.mockImplementation(function (src: fs.PathLike, dest: fs.PathLike) {
|
|
copiedFiles.push({ src: src.toString(), dest: dest.toString() });
|
|
return undefined;
|
|
});
|
|
|
|
mockExecFile.mockImplementation(((
|
|
_file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => callback(null, { stdout: '{"output":"test"}', stderr: '' }, ''));
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await provider.callApi('test prompt');
|
|
|
|
// Verify that main.go was copied, not transformed
|
|
expect(copiedFiles).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
src: expect.stringContaining('main.go'),
|
|
dest: expect.stringContaining('main.go'),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should correctly handle nested directories in copyDir', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
// Mock a nested directory structure
|
|
mockReaddirSync.mockImplementation(function (p: fs.PathOrFileDescriptor) {
|
|
if (p.toString().includes('nested')) {
|
|
return [{ name: 'nested-file.go', isDirectory: () => false }] as any;
|
|
}
|
|
return [
|
|
{ name: 'test.go', isDirectory: () => false },
|
|
{ name: 'nested', isDirectory: () => true },
|
|
] as any;
|
|
});
|
|
|
|
// Track created directories and copied files
|
|
const createdDirs: string[] = [];
|
|
const copiedFiles: { src: string; dest: string }[] = [];
|
|
|
|
mockMkdirSync.mockImplementation(function (p: fs.PathLike) {
|
|
createdDirs.push(p.toString());
|
|
return undefined;
|
|
});
|
|
|
|
mockCopyFileSync.mockImplementation(function (src: fs.PathLike, dest: fs.PathLike) {
|
|
copiedFiles.push({ src: src.toString(), dest: dest.toString() });
|
|
return undefined;
|
|
});
|
|
|
|
mockExecFile.mockImplementation(((
|
|
_file: string,
|
|
_args: any[],
|
|
optionsOrCallback: any,
|
|
maybeCallback?: any,
|
|
) => {
|
|
const callback =
|
|
typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
|
|
if (!callback) {
|
|
return {} as any;
|
|
}
|
|
setImmediate(() => callback(null, { stdout: '{"output":"test"}', stderr: '' }, ''));
|
|
return {} as any;
|
|
}) as any);
|
|
|
|
await provider.callApi('test prompt');
|
|
|
|
// Check that nested directories were created
|
|
expect(createdDirs).toEqual(expect.arrayContaining(['/tmp/golang-provider-xyz']));
|
|
// Check that nested files were copied
|
|
expect(copiedFiles).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
dest: expect.stringContaining('nested-file.go'),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should handle copyFileSync errors', async () => {
|
|
const provider = new GolangProvider('script.go', {
|
|
config: { basePath: '/absolute/path/to' },
|
|
});
|
|
|
|
mockReaddirSync.mockReturnValue([{ name: 'main.go', isDirectory: () => false }] as any);
|
|
|
|
mockCopyFileSync.mockImplementation(function () {
|
|
throw new Error('Failed to copy file');
|
|
});
|
|
|
|
await expect(provider.callApi('test prompt')).rejects.toThrow('Failed to copy file');
|
|
});
|
|
});
|
|
});
|