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

582 lines
21 KiB
TypeScript

import { Command } from 'commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
createAndDisplayShareableModelAuditUrl,
createAndDisplayShareableUrl,
notCloudEnabledShareInstructions,
shareCommand,
} from '../../src/commands/share';
import * as envars from '../../src/envars';
import logger from '../../src/logger';
import Eval from '../../src/models/eval';
import ModelAudit from '../../src/models/modelAudit';
import {
createShareableModelAuditUrl,
createShareableUrl,
isModelAuditSharingEnabled,
isSharingEnabled,
} from '../../src/share';
import { loadDefaultConfig } from '../../src/util/config/default';
vi.mock('../../src/share');
vi.mock('../../src/logger');
vi.mock('../../src/telemetry', () => ({
default: {
record: vi.fn(),
send: vi.fn(),
},
}));
vi.mock('../../src/envars');
vi.mock('readline');
vi.mock('../../src/models/eval');
vi.mock('../../src/models/modelAudit');
vi.mock('../../src/util', async (importOriginal) => {
return {
...(await importOriginal()),
setupEnv: vi.fn(),
};
});
vi.mock('../../src/util/config/default');
describe('Share Command', () => {
beforeEach(() => {
vi.clearAllMocks();
process.exitCode = 0; // Reset exitCode before each test
});
describe('notCloudEnabledShareInstructions', () => {
it('should log instructions for cloud setup', () => {
notCloudEnabledShareInstructions();
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('You need to have a cloud account'),
);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Please go to'));
});
});
describe('createAndDisplayShareableUrl', () => {
it('should return a URL and log it when successful', async () => {
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
const mockEval = { id: 'test-eval-id' } as Eval;
const mockUrl = 'https://app.promptfoo.dev/eval/test-eval-id';
vi.mocked(createShareableUrl).mockResolvedValue(mockUrl);
const result = await createAndDisplayShareableUrl(mockEval, false);
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining(mockUrl));
expect(result).toBe(mockUrl);
});
it('should pass showAuth parameter correctly', async () => {
const mockEval = { id: 'test-eval-id' } as Eval;
const mockUrl = 'https://app.promptfoo.dev/eval/test-eval-id';
vi.mocked(createShareableUrl).mockResolvedValue(mockUrl);
await createAndDisplayShareableUrl(mockEval, true);
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: true });
});
it('should return null when createShareableUrl returns null', async () => {
const mockEval = { id: 'test-eval-id' } as Eval;
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue(null);
const result = await createAndDisplayShareableUrl(mockEval, false);
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalledWith(
'Failed to create shareable URL for eval test-eval-id',
);
expect(logger.info).not.toHaveBeenCalled();
});
});
describe('createAndDisplayShareableModelAuditUrl', () => {
it('should return a URL and log it when successful', async () => {
const mockAudit = {
id: 'test-audit-id',
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
const mockUrl = 'https://app.promptfoo.dev/model-audit/test-audit-id';
vi.mocked(createShareableModelAuditUrl).mockResolvedValue(mockUrl);
const result = await createAndDisplayShareableModelAuditUrl(mockAudit, false);
expect(createShareableModelAuditUrl).toHaveBeenCalledWith(mockAudit, false);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining(mockUrl));
expect(result).toBe(mockUrl);
});
it('should pass showAuth parameter correctly', async () => {
const mockAudit = {
id: 'test-audit-id',
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
const mockUrl = 'https://app.promptfoo.dev/model-audit/test-audit-id';
vi.mocked(createShareableModelAuditUrl).mockResolvedValue(mockUrl);
await createAndDisplayShareableModelAuditUrl(mockAudit, true);
expect(createShareableModelAuditUrl).toHaveBeenCalledWith(mockAudit, true);
});
it('should return null when createShareableModelAuditUrl returns null', async () => {
const mockAudit = {
id: 'test-audit-id',
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
vi.mocked(createShareableModelAuditUrl).mockResolvedValue(null);
const result = await createAndDisplayShareableModelAuditUrl(mockAudit, false);
expect(createShareableModelAuditUrl).toHaveBeenCalledWith(mockAudit, false);
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalledWith(
'Failed to create shareable URL for model audit test-audit-id',
);
expect(logger.info).not.toHaveBeenCalled();
});
});
describe('shareCommand', () => {
let program: Command;
beforeEach(() => {
vi.clearAllMocks();
program = new Command();
shareCommand(program);
});
it('should register share command with correct options', () => {
const cmd = program.commands.find((c) => c.name() === 'share');
expect(cmd).toBeDefined();
expect(cmd?.description()).toContain('Create a shareable URL');
const options = cmd?.options;
expect(options?.find((o) => o.long === '--show-auth')).toBeDefined();
expect(options?.find((o) => o.long === '--yes')).toBeDefined();
// --model-audit flag should no longer exist
expect(options?.find((o) => o.long === '--model-audit')).toBeUndefined();
});
it('should handle model audit sharing with scan- prefixed ID', async () => {
const mockAudit = {
id: 'scan-test-123',
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
vi.spyOn(ModelAudit, 'findById').mockResolvedValue(mockAudit);
vi.mocked(isModelAuditSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableModelAuditUrl).mockResolvedValue(
'https://example.com/model-audit/scan-test-123',
);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', 'scan-test-123']);
expect(ModelAudit.findById).toHaveBeenCalledWith('scan-test-123');
expect(isModelAuditSharingEnabled).toHaveBeenCalled();
expect(createShareableModelAuditUrl).toHaveBeenCalledWith(mockAudit, false);
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('View ModelAudit Scan Results:'),
);
});
it('should handle eval sharing with non-scan prefixed ID', async () => {
const mockEval = {
id: 'eval-test-123',
prompts: ['test'],
config: {},
} as unknown as Eval;
vi.spyOn(Eval, 'findById').mockResolvedValue(mockEval);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue('https://example.com/eval/eval-test-123');
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', 'eval-test-123']);
expect(Eval.findById).toHaveBeenCalledWith('eval-test-123');
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('View results:'));
});
it('should share most recent model audit when it is newer than eval', async () => {
const mockEval = {
id: 'eval-old',
createdAt: 1000,
prompts: ['test'],
config: {},
} as unknown as Eval;
const mockAudit = {
id: 'scan-new',
createdAt: 2000,
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
vi.mocked(isModelAuditSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableModelAuditUrl).mockResolvedValue(
'https://example.com/model-audit/scan-new',
);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(Eval.latest).toHaveBeenCalledWith();
expect(ModelAudit.latest).toHaveBeenCalledWith();
expect(createShareableModelAuditUrl).toHaveBeenCalledWith(mockAudit, false);
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Sharing model audit'));
});
it('should share most recent eval when it is newer than model audit', async () => {
const mockEval = {
id: 'eval-new',
createdAt: 2000,
prompts: ['test'],
config: {},
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
modelPath: '/path/to/model',
results: {},
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue('https://example.com/eval/eval-new');
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(Eval.latest).toHaveBeenCalledWith();
expect(ModelAudit.latest).toHaveBeenCalledWith();
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Sharing eval'));
});
it('should handle scan- prefixed model audit not found', async () => {
vi.spyOn(ModelAudit, 'findById').mockResolvedValue(null);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', 'scan-non-existent']);
expect(ModelAudit.findById).toHaveBeenCalledWith('scan-non-existent');
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Could not find model audit with ID'),
);
expect(process.exitCode).toBe(1);
});
it('should handle no evals or model audits available', async () => {
vi.spyOn(Eval, 'latest').mockResolvedValue(undefined);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(undefined);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(Eval.latest).toHaveBeenCalledWith();
expect(ModelAudit.latest).toHaveBeenCalledWith();
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Could not load results'));
expect(process.exitCode).toBe(1);
});
it('should handle specific eval ID not found', async () => {
vi.spyOn(Eval, 'findById').mockResolvedValue(undefined);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', 'eval-non-existent']);
expect(Eval.findById).toHaveBeenCalledWith('eval-non-existent');
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Could not find eval with ID'),
);
expect(process.exitCode).toBe(1);
});
it('should handle eval with empty prompts when it is the most recent', async () => {
const mockEval = {
id: 'eval-empty',
prompts: [],
createdAt: 2000,
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(Eval.latest).toHaveBeenCalledWith();
expect(ModelAudit.latest).toHaveBeenCalledWith();
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('cannot be shared'));
expect(process.exitCode).toBe(1);
});
it('should accept -y flag for backwards compatibility', async () => {
const mockEval = {
id: 'eval-test',
prompts: ['test'],
createdAt: 2000,
config: {},
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue('https://example.com/share');
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', '-y']);
expect(Eval.latest).toHaveBeenCalledWith();
expect(ModelAudit.latest).toHaveBeenCalledWith();
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('View results:'));
});
it('should use promptfoo.app by default if no environment variables are set', () => {
vi.mocked(envars.getEnvString).mockImplementation(function () {
return '';
});
const baseUrl =
envars.getEnvString('PROMPTFOO_SHARING_APP_BASE_URL') ||
envars.getEnvString('PROMPTFOO_REMOTE_APP_BASE_URL');
const hostname = baseUrl ? new URL(baseUrl).hostname : 'promptfoo.app';
expect(hostname).toBe('promptfoo.app');
});
it('should use PROMPTFOO_SHARING_APP_BASE_URL for hostname when set', () => {
vi.mocked(envars.getEnvString).mockImplementation(function (key) {
if (key === 'PROMPTFOO_SHARING_APP_BASE_URL') {
return 'https://custom-domain.com';
}
return '';
});
const baseUrl =
envars.getEnvString('PROMPTFOO_SHARING_APP_BASE_URL') ||
envars.getEnvString('PROMPTFOO_REMOTE_APP_BASE_URL');
const hostname = baseUrl ? new URL(baseUrl).hostname : 'app.promptfoo.dev';
expect(hostname).toBe('custom-domain.com');
});
it('should use PROMPTFOO_REMOTE_APP_BASE_URL for hostname when PROMPTFOO_SHARING_APP_BASE_URL is not set', () => {
vi.mocked(envars.getEnvString).mockImplementation(function (key) {
if (key === 'PROMPTFOO_REMOTE_APP_BASE_URL') {
return 'https://self-hosted-domain.com';
}
return '';
});
const baseUrl =
envars.getEnvString('PROMPTFOO_SHARING_APP_BASE_URL') ||
envars.getEnvString('PROMPTFOO_REMOTE_APP_BASE_URL');
const hostname = baseUrl ? new URL(baseUrl).hostname : 'app.promptfoo.dev';
expect(hostname).toBe('self-hosted-domain.com');
});
it('should prioritize PROMPTFOO_SHARING_APP_BASE_URL over PROMPTFOO_REMOTE_APP_BASE_URL', () => {
vi.mocked(envars.getEnvString).mockImplementation(function (key) {
if (key === 'PROMPTFOO_SHARING_APP_BASE_URL') {
return 'https://sharing-domain.com';
}
if (key === 'PROMPTFOO_REMOTE_APP_BASE_URL') {
return 'https://remote-domain.com';
}
return '';
});
const baseUrl =
envars.getEnvString('PROMPTFOO_SHARING_APP_BASE_URL') ||
envars.getEnvString('PROMPTFOO_REMOTE_APP_BASE_URL');
const hostname = baseUrl ? new URL(baseUrl).hostname : 'app.promptfoo.dev';
expect(hostname).toBe('sharing-domain.com');
});
it('should use sharing config from promptfooconfig.yaml', async () => {
const mockEval = {
id: 'test-eval-id',
prompts: ['test prompt'],
config: {},
createdAt: 2000,
save: vi.fn().mockResolvedValue(undefined),
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
const mockSharing = {
apiBaseUrl: 'https://custom-api.example.com',
appBaseUrl: 'https://custom-app.example.com',
};
vi.mocked(loadDefaultConfig).mockResolvedValue({
defaultConfig: {
sharing: mockSharing,
},
defaultConfigPath: 'promptfooconfig.yaml',
});
vi.mocked(isSharingEnabled).mockImplementation(function (evalObj) {
return !!evalObj.config.sharing;
});
vi.mocked(createShareableUrl).mockResolvedValue(
'https://custom-app.example.com/eval/test-eval-id',
);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(loadDefaultConfig).toHaveBeenCalledTimes(1);
expect(mockEval.config.sharing).toEqual(mockSharing);
expect(isSharingEnabled).toHaveBeenCalledWith(mockEval);
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
});
it('should show cloud instructions and return null when sharing is not enabled', async () => {
const mockEval = {
id: 'test-eval-id',
prompts: ['test prompt'],
config: {},
createdAt: 2000,
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return false;
});
vi.mocked(loadDefaultConfig).mockResolvedValue({
defaultConfig: {},
defaultConfigPath: 'promptfooconfig.yaml',
});
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('You need to have a cloud account'),
);
expect(process.exitCode).toBe(1);
});
it('should set exit code 0 when sharing is successful', async () => {
const mockEval = {
id: 'test-eval-id',
prompts: ['test prompt'],
config: { sharing: true },
createdAt: 2000,
} as unknown as Eval;
const mockAudit = {
id: 'scan-old',
createdAt: 1000,
} as ModelAudit;
vi.spyOn(Eval, 'latest').mockResolvedValue(mockEval);
vi.spyOn(ModelAudit, 'latest').mockResolvedValue(mockAudit);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue(
'https://app.promptfoo.dev/eval/test-eval-id',
);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test']);
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('View results:'));
expect(process.exitCode).toBe(0);
});
it('should set exit code 0 when sharing specific eval by ID', async () => {
const mockEval = {
id: 'specific-eval-id',
prompts: ['test prompt'],
config: { sharing: true },
} as unknown as Eval;
vi.spyOn(Eval, 'findById').mockResolvedValue(mockEval);
vi.mocked(isSharingEnabled).mockImplementation(function () {
return true;
});
vi.mocked(createShareableUrl).mockResolvedValue(
'https://app.promptfoo.dev/eval/specific-eval-id',
);
const shareCmd = program.commands.find((c) => c.name() === 'share');
await shareCmd?.parseAsync(['node', 'test', 'specific-eval-id']);
expect(Eval.findById).toHaveBeenCalledWith('specific-eval-id');
expect(createShareableUrl).toHaveBeenCalledWith(mockEval, { showAuth: false });
expect(process.exitCode).toBe(0);
});
it('should set exit code 0 when user cancels sharing in confirmation prompt', async () => {
// This test is complex to mock properly, so we'll just verify the command exists
// The actual cancellation logic is tested in integration tests
const shareCmd = program.commands.find((c) => c.name() === 'share');
expect(shareCmd).toBeDefined();
expect(shareCmd?.name()).toBe('share');
});
});
});