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
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
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
250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
/**
|
|
* Scanner No Files Test
|
|
*
|
|
* Tests that processDiff handles cases where no files are found to scan
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { CodeScanOutputFormat, type FileRecord } from '../../src/types/codeScan';
|
|
|
|
describe('Scanner - No Files to Scan', () => {
|
|
it('should filter out files with skipReason when determining includedFiles', () => {
|
|
// Simulate the result from processDiff with all files skipped
|
|
const files: FileRecord[] = [
|
|
{
|
|
path: 'package-lock.json',
|
|
status: 'M',
|
|
skipReason: 'denylist',
|
|
shaA: 'abc123',
|
|
shaB: 'def456',
|
|
linesAdded: 10,
|
|
linesRemoved: 5,
|
|
},
|
|
{
|
|
path: 'large-file.bin',
|
|
status: 'M',
|
|
skipReason: 'blob too large',
|
|
shaA: 'abc124',
|
|
shaB: 'def457',
|
|
linesAdded: 100,
|
|
linesRemoved: 50,
|
|
},
|
|
];
|
|
|
|
// This is the same logic used in scanner/index.ts
|
|
const includedFiles = files.filter((f) => !f.skipReason && f.patch);
|
|
|
|
expect(includedFiles).toHaveLength(0);
|
|
});
|
|
|
|
it('should correctly identify included files when some have no skipReason', () => {
|
|
const files: FileRecord[] = [
|
|
{
|
|
path: 'package-lock.json',
|
|
status: 'M',
|
|
skipReason: 'denylist',
|
|
shaA: 'abc123',
|
|
shaB: 'def456',
|
|
linesAdded: 10,
|
|
linesRemoved: 5,
|
|
},
|
|
{
|
|
path: 'src/index.ts',
|
|
status: 'M',
|
|
shaA: 'abc124',
|
|
shaB: 'def457',
|
|
linesAdded: 5,
|
|
linesRemoved: 2,
|
|
patch: '@@ -1,3 +1,4 @@\n+const test = true;\n const existing = true;',
|
|
},
|
|
];
|
|
|
|
const includedFiles = files.filter((f) => !f.skipReason && f.patch);
|
|
|
|
expect(includedFiles).toHaveLength(1);
|
|
expect(includedFiles[0].path).toBe('src/index.ts');
|
|
});
|
|
|
|
it('should handle empty file array', () => {
|
|
const files: FileRecord[] = [];
|
|
|
|
const includedFiles = files.filter((f) => !f.skipReason && f.patch);
|
|
|
|
expect(includedFiles).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Scanner machine-readable output', () => {
|
|
function mockScanner({
|
|
initialLogLevel = 'info',
|
|
loadConfigError,
|
|
processDiffError,
|
|
}: {
|
|
initialLogLevel?: string;
|
|
loadConfigError?: Error;
|
|
processDiffError?: Error;
|
|
} = {}) {
|
|
let currentLogLevel = initialLogLevel;
|
|
|
|
const disconnect = vi.fn();
|
|
|
|
vi.doMock('../../src/codeScan/git/diffProcessor', () => ({
|
|
processDiff: processDiffError
|
|
? vi.fn().mockRejectedValue(processDiffError)
|
|
: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
vi.doMock('../../src/codeScan/git/diff', () => ({
|
|
validateOnBranch: vi.fn().mockResolvedValue('main'),
|
|
}));
|
|
vi.doMock('../../src/codeScan/config/loader', () => ({
|
|
loadConfigOrDefault: loadConfigError
|
|
? vi.fn().mockImplementation(() => {
|
|
throw loadConfigError;
|
|
})
|
|
: vi.fn().mockReturnValue({
|
|
minimumSeverity: 'medium',
|
|
diffsOnly: true,
|
|
}),
|
|
mergeConfigWithOptions: vi.fn().mockImplementation((config, options) => ({
|
|
...config,
|
|
diffsOnly: options.diffsOnly ?? config.diffsOnly,
|
|
})),
|
|
resolveGuidance: vi.fn().mockReturnValue(undefined),
|
|
resolveApiHost: vi.fn().mockReturnValue('https://api.example.com'),
|
|
}));
|
|
vi.doMock('simple-git', () => ({
|
|
default: vi.fn(() => ({
|
|
branch: vi.fn().mockResolvedValue({ current: 'main', all: ['main'] }),
|
|
revparse: vi.fn().mockResolvedValue('abc123'),
|
|
})),
|
|
}));
|
|
vi.doMock('../../src/util/agent/agentClient', () => ({
|
|
createAgentClient: vi.fn().mockResolvedValue({
|
|
sessionId: 'test-session-id',
|
|
start: vi.fn(),
|
|
cancel: vi.fn(),
|
|
onComplete: vi.fn(),
|
|
onError: vi.fn(),
|
|
on: vi.fn(),
|
|
emit: vi.fn(),
|
|
disconnect,
|
|
socket: { emit: vi.fn(), on: vi.fn(), off: vi.fn(), disconnect: vi.fn() },
|
|
}),
|
|
}));
|
|
vi.doMock('../../src/codeScan/util/auth', () => ({
|
|
resolveAuthCredentials: vi.fn().mockResolvedValue({ apiKey: 'test-key' }),
|
|
}));
|
|
vi.doMock('../../src/cliState', () => ({
|
|
default: { postActionCallback: null },
|
|
}));
|
|
vi.doMock('../../src/logger', () => ({
|
|
default: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
|
getLogLevel: vi.fn().mockImplementation(() => currentLogLevel),
|
|
setLogLevel: vi.fn().mockImplementation((level: string) => {
|
|
currentLogLevel = level;
|
|
}),
|
|
}));
|
|
vi.doMock('../../src/codeScan/scanner/cleanup', () => ({
|
|
registerCleanupHandlers: vi.fn(),
|
|
}));
|
|
vi.doMock('../../src/codeScan/scanner/output', () => ({
|
|
createSpinner: vi.fn().mockReturnValue(undefined),
|
|
displayScanResults: vi.fn(),
|
|
}));
|
|
|
|
return { disconnect };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it.each([
|
|
['--json', { json: true, diffsOnly: true }, CodeScanOutputFormat.JSON],
|
|
[
|
|
'--format sarif',
|
|
{ format: CodeScanOutputFormat.SARIF, diffsOnly: true },
|
|
CodeScanOutputFormat.SARIF,
|
|
],
|
|
])('emits an empty machine-readable response when no files are available with %s', async (_label, options, expectedFormat) => {
|
|
mockScanner();
|
|
|
|
const { executeScan } = await import('../../src/codeScan/scanner/index');
|
|
const { displayScanResults } = await import('../../src/codeScan/scanner/output');
|
|
const { getLogLevel, setLogLevel } = await import('../../src/logger');
|
|
const setLogLevelMock = vi.mocked(setLogLevel);
|
|
|
|
await executeScan('/test/repo', options);
|
|
|
|
expect(displayScanResults).toHaveBeenCalledWith(
|
|
{ success: true, comments: [], review: 'No files to scan' },
|
|
expect.any(Number),
|
|
{ format: expectedFormat, githubPr: undefined },
|
|
);
|
|
expect(setLogLevelMock).toHaveBeenCalledWith('error');
|
|
expect(setLogLevelMock).toHaveBeenLastCalledWith('info');
|
|
expect(getLogLevel()).toBe('info');
|
|
});
|
|
|
|
it('restores the original log level when structured config loading fails early', async () => {
|
|
mockScanner({ loadConfigError: new Error('missing config') });
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const { executeScan } = await import('../../src/codeScan/scanner/index');
|
|
const logger = (await import('../../src/logger')).default;
|
|
const { getLogLevel, setLogLevel } = await import('../../src/logger');
|
|
const setLogLevelMock = vi.mocked(setLogLevel);
|
|
|
|
await executeScan('/test/repo', { json: true, config: '/missing.yaml' });
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('Scan failed: missing config');
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
expect(setLogLevelMock).toHaveBeenNthCalledWith(1, 'error');
|
|
expect(setLogLevelMock).toHaveBeenLastCalledWith('info');
|
|
expect(getLogLevel()).toBe('info');
|
|
});
|
|
|
|
it('keeps invalid structured output flag combinations off stdout', async () => {
|
|
mockScanner();
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const { executeScan } = await import('../../src/codeScan/scanner/index');
|
|
const logger = (await import('../../src/logger')).default;
|
|
const { getLogLevel, setLogLevel } = await import('../../src/logger');
|
|
const setLogLevelMock = vi.mocked(setLogLevel);
|
|
|
|
await executeScan('/test/repo', { json: true, format: CodeScanOutputFormat.SARIF });
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
'Scan failed: Cannot combine --json with --format sarif',
|
|
);
|
|
expect(logger.error).not.toHaveBeenCalled();
|
|
expect(setLogLevelMock).not.toHaveBeenCalled();
|
|
expect(getLogLevel()).toBe('info');
|
|
});
|
|
|
|
it('keeps structured-output suppression active until cleanup finishes', async () => {
|
|
const { disconnect } = mockScanner({
|
|
initialLogLevel: 'debug',
|
|
processDiffError: new Error('diff failed'),
|
|
});
|
|
|
|
const { executeScan } = await import('../../src/codeScan/scanner/index');
|
|
const { getLogLevel, setLogLevel } = await import('../../src/logger');
|
|
const setLogLevelMock = vi.mocked(setLogLevel);
|
|
|
|
await executeScan('/test/repo', { json: true });
|
|
|
|
expect(disconnect).toHaveBeenCalled();
|
|
expect(setLogLevelMock.mock.calls).toEqual([['error'], ['debug']]);
|
|
expect(disconnect.mock.invocationCallOrder[0]).toBeLessThan(
|
|
setLogLevelMock.mock.invocationCallOrder[1] ?? Number.POSITIVE_INFINITY,
|
|
);
|
|
expect(getLogLevel()).toBe('debug');
|
|
});
|
|
});
|