0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
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) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
import * as fs from 'fs';
|
|
|
|
import * as yaml from 'js-yaml';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
readGlobalConfig,
|
|
writeGlobalConfig,
|
|
writeGlobalConfigPartial,
|
|
} from '../src/globalConfig/globalConfig';
|
|
|
|
// Helper function to validate UUID format
|
|
function isValidUUID(uuid: string): boolean {
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(uuid);
|
|
}
|
|
|
|
vi.mock('fs', () => ({
|
|
readFileSync: vi.fn(),
|
|
writeFileSync: vi.fn(),
|
|
statSync: vi.fn(),
|
|
readdirSync: vi.fn(),
|
|
existsSync: vi.fn(),
|
|
mkdirSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('proxy-agent', () => ({
|
|
ProxyAgent: vi.fn().mockImplementation(() => ({})),
|
|
}));
|
|
|
|
vi.mock('glob', () => ({
|
|
globSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/database');
|
|
|
|
describe('Global Config', () => {
|
|
let globalConfig: {
|
|
readGlobalConfig: typeof readGlobalConfig;
|
|
writeGlobalConfig: typeof writeGlobalConfig;
|
|
writeGlobalConfigPartial: typeof writeGlobalConfigPartial;
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
globalConfig = await import('../src/globalConfig/globalConfig');
|
|
});
|
|
|
|
const mockConfig = { id: 'test-id', account: { email: 'test@example.com' } };
|
|
|
|
describe('readGlobalConfig', () => {
|
|
describe('when config file exists', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(fs.existsSync).mockImplementation(
|
|
(path) =>
|
|
path.toString().includes('promptfoo.yaml') || path.toString().includes('.promptfoo'),
|
|
);
|
|
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
|
});
|
|
|
|
it('should read and parse the existing config file', () => {
|
|
const result = globalConfig.readGlobalConfig();
|
|
expect(fs.readFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
'utf-8',
|
|
);
|
|
expect(result).toEqual(mockConfig);
|
|
});
|
|
|
|
it('should handle empty config file by returning config with generated ID', () => {
|
|
vi.mocked(fs.readFileSync).mockReturnValue('');
|
|
|
|
const result = globalConfig.readGlobalConfig();
|
|
|
|
expect(result).toEqual({ id: expect.any(String) });
|
|
expect(result.id).toBeDefined();
|
|
expect(typeof result.id).toBe('string');
|
|
expect(isValidUUID(result.id!)).toBe(true);
|
|
});
|
|
|
|
it('should generate and save ID when existing config lacks an ID', () => {
|
|
const configWithoutId = { account: { email: 'test@example.com' } };
|
|
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(configWithoutId));
|
|
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
|
|
|
|
const result = globalConfig.readGlobalConfig();
|
|
|
|
expect(result.id).toBeDefined();
|
|
expect(typeof result.id).toBe('string');
|
|
expect(isValidUUID(result.id!)).toBe(true);
|
|
expect(result.account).toEqual(configWithoutId.account);
|
|
// Should have written the config with the new ID
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
expect.stringContaining(`id: ${result.id}`),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('when config file does not exist', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
|
|
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);
|
|
});
|
|
|
|
it('should create new config directory and file with generated UUID', () => {
|
|
const result = globalConfig.readGlobalConfig();
|
|
|
|
expect(fs.existsSync).toHaveBeenCalledTimes(2);
|
|
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true });
|
|
expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
expect.any(String),
|
|
);
|
|
expect(result).toEqual({ id: expect.any(String) });
|
|
expect(result.id).toBeDefined();
|
|
expect(typeof result.id).toBe('string');
|
|
expect(isValidUUID(result.id!)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw error if config file is invalid YAML', () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockReturnValue('invalid: yaml: content:');
|
|
|
|
expect(() => globalConfig.readGlobalConfig()).toThrow(/bad indentation of a mapping entry/);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('writeGlobalConfig', () => {
|
|
it('should write config to file in YAML format', () => {
|
|
globalConfig.writeGlobalConfig({
|
|
...mockConfig,
|
|
});
|
|
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
expect.stringContaining('account:'),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('writeGlobalConfigPartial', () => {
|
|
beforeEach(() => {
|
|
// Setup initial config
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
yaml.dump({
|
|
account: { email: 'old@example.com' },
|
|
cloud: { apiKey: 'old-key', apiHost: 'old-host' },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should merge new config with existing config', () => {
|
|
const partialConfig = {
|
|
account: { email: 'new@example.com' },
|
|
};
|
|
|
|
globalConfig.writeGlobalConfigPartial(partialConfig);
|
|
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
expect.stringMatching(/email: new@example\.com.*apiKey: old-key/s),
|
|
);
|
|
});
|
|
|
|
it('should remove keys when value is falsy', () => {
|
|
const partialConfig = {
|
|
cloud: undefined,
|
|
};
|
|
|
|
globalConfig.writeGlobalConfigPartial(partialConfig);
|
|
|
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
expect.stringContaining('promptfoo.yaml'),
|
|
expect.not.stringContaining('apiKey: old-key'),
|
|
);
|
|
});
|
|
|
|
it('should update specific keys while preserving others', () => {
|
|
const partialConfig = {
|
|
cloud: { apiKey: 'new-key' },
|
|
};
|
|
|
|
globalConfig.writeGlobalConfigPartial(partialConfig);
|
|
|
|
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[1][1] as string;
|
|
const writtenConfig = yaml.load(writeCall) as any;
|
|
|
|
expect(writtenConfig.cloud.apiKey).toBe('new-key');
|
|
expect(writtenConfig.account.email).toBe('old@example.com');
|
|
});
|
|
});
|
|
});
|