Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

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');
});
});
});