mirror of
https://github.com/Done-0/fuck-u-code.git
synced 2026-09-03 06:34:37 +08:00
- git clone now uses execFile (no shell) and rejects URLs with shell metacharacters; removed dead parseRepoName. - config show masks apiKey; config file chmod 0600 after writing keys. - CLI launcher reports signal kills as failures and supports FUCK_U_CODE_MAX_MEMORY (default 4096MB). - File discovery: well-known non-code dirs are defaults, not hard overrides; user-included dirs are analyzed. - Nested .gitignore loading uses forward slashes so it works on Windows. - Deduplicated analysis output rendering; fixed fetch timeout cleanup; aligned concurrency default to 8; enabled zh_TW locale end to end. - Fixed pre-existing lint errors and Windows-flaky MCP tests; added git-utils, secrets, file-discovery tests; release workflow now runs tests.
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { mkdtemp, mkdir, writeFile, rm, stat } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { gitClone, isValidGitUrl, removeTempDir } from '../../src/utils/git.js';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
describe('git utils', () => {
|
|
let tempRoot: string;
|
|
|
|
beforeEach(async () => {
|
|
tempRoot = await mkdtemp(join(tmpdir(), 'fuck-u-code-git-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('isValidGitUrl', () => {
|
|
it('accepts https, ssh and local paths', () => {
|
|
expect(isValidGitUrl('https://github.com/user/repo.git')).toBe(true);
|
|
expect(isValidGitUrl('git@github.com:user/repo.git')).toBe(true);
|
|
expect(isValidGitUrl('./local/repo')).toBe(true);
|
|
expect(isValidGitUrl('~/repo')).toBe(true);
|
|
});
|
|
|
|
it('rejects shell metacharacters and whitespace', () => {
|
|
expect(isValidGitUrl('https://example.com/repo;rm -rf /')).toBe(false);
|
|
expect(isValidGitUrl('https://example.com/repo$(id)')).toBe(false);
|
|
expect(isValidGitUrl('https://example.com/repo`id`')).toBe(false);
|
|
expect(isValidGitUrl('https://example.com/repo | cat')).toBe(false);
|
|
expect(isValidGitUrl('https://example.com/repo "quoted"')).toBe(false);
|
|
expect(isValidGitUrl('https://example.com/repo\n')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('gitClone', () => {
|
|
it('clones a local repository', async () => {
|
|
const source = join(tempRoot, 'source');
|
|
await mkdir(source);
|
|
await writeFile(join(source, 'file.txt'), 'hello');
|
|
await execFileAsync('git', ['init', '-q', source]);
|
|
await execFileAsync('git', ['-C', source, 'add', '.']);
|
|
await execFileAsync('git', [
|
|
'-C',
|
|
source,
|
|
'-c',
|
|
'user.email=test@example.com',
|
|
'-c',
|
|
'user.name=test',
|
|
'commit',
|
|
'-qm',
|
|
'init',
|
|
]);
|
|
|
|
const target = join(tempRoot, 'clone');
|
|
const result = await gitClone(source, { targetDir: target, timeout: 30000 });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.targetDir).toBe(target);
|
|
await expect(stat(join(target, 'file.txt'))).resolves.toBeTruthy();
|
|
}, 30000);
|
|
|
|
it('never interprets a URL through a shell', async () => {
|
|
const marker = join(tempRoot, 'pwned');
|
|
const result = await gitClone(`./repo;echo pwned > ${marker}`, {
|
|
targetDir: join(tempRoot, 'out'),
|
|
timeout: 10000,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
await expect(stat(marker)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('removeTempDir', () => {
|
|
it('removes an existing directory and tolerates a missing one', async () => {
|
|
const dir = join(tempRoot, 'temp-dir');
|
|
await mkdir(dir);
|
|
await writeFile(join(dir, 'x.txt'), 'x');
|
|
|
|
await expect(removeTempDir(dir)).resolves.toBe(true);
|
|
await expect(removeTempDir(join(tempRoot, 'missing'))).resolves.toBe(true);
|
|
});
|
|
});
|
|
});
|