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

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