Files
renovate/tools/agents/hooks/utils/git.spec.ts
Sebastian Poxhofer 7436b80cda test(linter): add renovate/no-redundant-mock-reset rule (#44557)
ci(linter): add renovate/no-redundant-mock-reset rule

Vitest sets mockReset: true globally in vitest.config.mts, which resets
all mocks before each test. This makes vi.resetAllMocks() /
vi.clearAllMocks() and per-mock .mockReset() / .mockClear() calls inside
beforeEach/afterEach hooks redundant. Add a custom oxlint rule (enabled
for **/*.spec.ts and test/**) flagging such calls, and remove the 40
redundant calls it found. restoreAllMocks/mockRestore are not flagged
because restoreMocks is not enabled globally.

Two sites keep an inline disable with a reason: nested beforeEach hooks
in filter-checks.spec.ts and onboarding/pr/index.spec.ts intentionally
call mockReset() to discard once-values queued by an outer beforeEach.

Requested by maintainer review feedback (review comment 3052324877).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:26:20 +00:00

109 lines
2.9 KiB
TypeScript

import { getChangedFiles, getRepoRoot } from './git.ts';
const mockGit = vi.hoisted(() => {
const obj = {
raw: vi.fn(),
revparse: vi.fn(),
diff: vi.fn(),
env: vi.fn(),
};
obj.env.mockReturnValue(obj);
return obj;
});
vi.mock('simple-git', () => ({
simpleGit: vi.fn(() => mockGit),
}));
beforeEach(() => {
mockGit.env.mockReturnValue(mockGit);
});
it('returns changed files using merge-base with origin/main', async () => {
mockGit.raw.mockResolvedValueOnce('abc1234\n');
mockGit.diff.mockResolvedValueOnce('lib/foo.ts\nlib/bar.ts\n');
const result = await getChangedFiles();
expect(mockGit.raw).toHaveBeenCalledWith([
'merge-base',
'origin/main',
'HEAD',
]);
expect(mockGit.diff).toHaveBeenCalledWith([
'--name-only',
'--diff-filter=ACMR',
'abc1234',
]);
expect(result).toEqual(['lib/foo.ts', 'lib/bar.ts']);
});
it('falls back to upstream tracking branch when origin/main is not available', async () => {
mockGit.raw.mockRejectedValueOnce(new Error('no origin/main'));
mockGit.revparse.mockResolvedValueOnce('upstream/feature\n');
mockGit.diff.mockResolvedValueOnce('lib/foo.ts\n');
const result = await getChangedFiles();
expect(mockGit.revparse).toHaveBeenCalledWith([
'--abbrev-ref',
'--symbolic-full-name',
'@{u}',
]);
expect(mockGit.diff).toHaveBeenCalledWith([
'--name-only',
'--diff-filter=ACMR',
'upstream/feature',
]);
expect(result).toEqual(['lib/foo.ts']);
});
it('falls back to HEAD when neither origin/main nor upstream is available', async () => {
mockGit.raw.mockRejectedValueOnce(new Error('no origin/main'));
mockGit.revparse.mockRejectedValueOnce(new Error('no upstream'));
mockGit.diff.mockResolvedValueOnce('lib/foo.ts\n');
const result = await getChangedFiles();
expect(mockGit.diff).toHaveBeenCalledWith([
'--name-only',
'--diff-filter=ACMR',
'HEAD',
]);
expect(result).toEqual(['lib/foo.ts']);
});
it('returns empty array when no files changed', async () => {
mockGit.raw.mockResolvedValueOnce('abc1234\n');
mockGit.diff.mockResolvedValueOnce('');
const result = await getChangedFiles();
expect(result).toEqual([]);
});
it('getRepoRoot returns trimmed path when git reports a toplevel', async () => {
mockGit.revparse.mockResolvedValueOnce('/home/user/repo\n');
const result = await getRepoRoot('/home/user/repo/sub');
expect(mockGit.revparse).toHaveBeenCalledWith(['--show-toplevel']);
expect(result).toBe('/home/user/repo');
});
it('getRepoRoot returns null when not inside a git repo', async () => {
mockGit.revparse.mockRejectedValueOnce(new Error('not a git repo'));
const result = await getRepoRoot('/tmp/not-a-repo');
expect(result).toBeNull();
});
it('getRepoRoot returns null when output is empty', async () => {
mockGit.revparse.mockResolvedValueOnce('');
const result = await getRepoRoot();
expect(result).toBeNull();
});