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
582 lines
21 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|