Files
repomix-mirror/tests/core/security/validateFileSafety.test.ts
Kazuki Yamada f67731056a test: Round-3 PR review feedback
- validateFileSafety: pin the negative path of `if (config.security.enableSecurityCheck)`
  — every other test enabled the check, so a regression that always runs
  the security check would have passed silently.
- unifiedWorker:
  - Add a positive workerData=securityCheck + ambiguous-task case so the
    pair (override + this) distinguishes "inference always wins" from
    "inference wins only when it yields a value".
  - Stop pretending the handler-cache test verifies caching. Both branches
    of `if (cached) return cached;` end with the same Map.set, and Node's
    own module cache makes the dynamic import effectively free, so the
    cache is unobservable from outside without exposing internals.
    Renamed to "repeated calls" with a comment explaining the limitation.
- fileSystemReadDirectoryTool: translate the pre-existing Japanese comment
  to English per CLAUDE.md.
- TokenCounter: extract `LoadEncodingFn` type alias instead of the
  unusual `typeof loadEncoding`, so a signature drift between the local
  function and the deps field would surface at the type level.
2026-04-26 22:47:21 +09:00

116 lines
5.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import type { RepomixConfigMerged } from '../../../src/config/configSchema.js';
import type { RawFile } from '../../../src/core/file/fileTypes.js';
import type { SuspiciousFileResult } from '../../../src/core/security/securityCheck.js';
import { validateFileSafety } from '../../../src/core/security/validateFileSafety.js';
import { logger } from '../../../src/shared/logger.js';
import type { RepomixProgressCallback } from '../../../src/shared/types.js';
describe('validateFileSafety', () => {
it('should validate file safety and return safe files and paths', async () => {
const rawFiles: RawFile[] = [
{ path: 'file1.txt', content: 'content1' },
{ path: 'file2.txt', content: 'content2' },
{ path: 'file3.txt', content: 'content3' },
];
const safeRawFiles = [rawFiles[0], rawFiles[1]];
const config: RepomixConfigMerged = {
security: { enableSecurityCheck: true },
} as RepomixConfigMerged;
const progressCallback: RepomixProgressCallback = vi.fn();
const suspiciousFilesResults: SuspiciousFileResult[] = [
{ filePath: 'file2.txt', messages: ['something suspicious.'], type: 'file' },
];
const deps = {
runSecurityCheck: vi.fn().mockResolvedValue(suspiciousFilesResults),
filterOutUntrustedFiles: vi.fn().mockReturnValue(safeRawFiles),
};
const result = await validateFileSafety(rawFiles, progressCallback, config, undefined, undefined, deps);
expect(deps.runSecurityCheck).toHaveBeenCalledWith(rawFiles, progressCallback, undefined, undefined);
expect(deps.filterOutUntrustedFiles).toHaveBeenCalledWith(rawFiles, suspiciousFilesResults);
expect(result).toEqual({
safeRawFiles,
safeFilePaths: ['file1.txt', 'file2.txt'],
suspiciousFilesResults,
suspiciousGitDiffResults: [],
suspiciousGitLogResults: [],
});
});
describe('git content warnings', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('warns about each suspicious git diff/log entry with singular/plural form', async () => {
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
const config: RepomixConfigMerged = {
security: { enableSecurityCheck: true },
} as RepomixConfigMerged;
const progressCallback: RepomixProgressCallback = vi.fn();
const allResults: SuspiciousFileResult[] = [
{ filePath: 'work_tree', messages: ['leaked key'], type: 'gitDiff' },
{ filePath: 'staged', messages: ['leaked key', 'token'], type: 'gitDiff' },
{ filePath: 'commit_log', messages: ['secret', 'pw', 'token'], type: 'gitLog' },
];
const deps = {
runSecurityCheck: vi.fn().mockResolvedValue(allResults),
filterOutUntrustedFiles: vi.fn().mockReturnValue([]),
};
const result = await validateFileSafety([], progressCallback, config, undefined, undefined, deps);
expect(result.suspiciousGitDiffResults).toHaveLength(2);
expect(result.suspiciousGitLogResults).toHaveLength(1);
const warnings = warnSpy.mock.calls.map((c) => String(c[0]));
// Header lines for each section
expect(warnings).toContain('Security issues found in Git diffs, but they will still be included in the output');
expect(warnings).toContain('Security issues found in Git logs, but they will still be included in the output');
// Singular form for 1 message, plural for >1
expect(warnings).toContain(' - work_tree: 1 issue detected');
expect(warnings).toContain(' - staged: 2 issues detected');
expect(warnings).toContain(' - commit_log: 3 issues detected');
});
it('does not log a header when no suspicious git content is found', async () => {
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
const config: RepomixConfigMerged = {
security: { enableSecurityCheck: true },
} as RepomixConfigMerged;
await validateFileSafety([], vi.fn(), config, undefined, undefined, {
runSecurityCheck: vi.fn().mockResolvedValue([]),
filterOutUntrustedFiles: vi.fn().mockReturnValue([]),
});
expect(warnSpy).not.toHaveBeenCalled();
});
});
it('skips runSecurityCheck entirely when enableSecurityCheck is false', async () => {
// Pin the negative path of the `if (config.security.enableSecurityCheck)` guard.
// Dropping the guard would still pass every other test in this file because
// they all enable the check; this one fails if the guard ever regresses.
const config: RepomixConfigMerged = {
security: { enableSecurityCheck: false },
} as RepomixConfigMerged;
const rawFiles: RawFile[] = [{ path: 'file1.txt', content: 'content' }];
const deps = {
runSecurityCheck: vi.fn(),
filterOutUntrustedFiles: vi.fn().mockReturnValue(rawFiles),
};
const result = await validateFileSafety(rawFiles, vi.fn(), config, undefined, undefined, deps);
expect(deps.runSecurityCheck).not.toHaveBeenCalled();
expect(result.suspiciousFilesResults).toEqual([]);
expect(result.suspiciousGitDiffResults).toEqual([]);
expect(result.suspiciousGitLogResults).toEqual([]);
expect(result.safeFilePaths).toEqual(['file1.txt']);
});
});