BMAD-METHOD/test/unit/file-ops/copy-directory.test.js

244 lines
9.5 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FileOps } from '../../../tools/cli/lib/file-ops.js';
import { createTempDir, cleanupTempDir, createTestFile } from '../../helpers/temp-dir.js';
import fs from 'fs-extra';
import path from 'node:path';
describe('FileOps', () => {
describe('copyDirectory()', () => {
const fileOps = new FileOps();
let tmpDir;
let sourceDir;
let destDir;
beforeEach(async () => {
tmpDir = await createTempDir();
sourceDir = path.join(tmpDir, 'source');
destDir = path.join(tmpDir, 'dest');
await fs.ensureDir(sourceDir);
await fs.ensureDir(destDir);
});
afterEach(async () => {
await cleanupTempDir(tmpDir);
});
describe('basic copying', () => {
it('should copy a single file', async () => {
await createTestFile(sourceDir, 'test.txt', 'content');
await fileOps.copyDirectory(sourceDir, destDir);
const destFile = path.join(destDir, 'test.txt');
expect(await fs.pathExists(destFile)).toBe(true);
expect(await fs.readFile(destFile, 'utf8')).toBe('content');
});
it('should copy multiple files', async () => {
await createTestFile(sourceDir, 'file1.txt', 'content1');
await createTestFile(sourceDir, 'file2.md', 'content2');
await createTestFile(sourceDir, 'file3.json', '{}');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'file1.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'file2.md'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'file3.json'))).toBe(true);
});
it('should copy nested directory structure', async () => {
await createTestFile(sourceDir, 'root.txt', 'root');
await createTestFile(sourceDir, 'level1/file.txt', 'level1');
await createTestFile(sourceDir, 'level1/level2/deep.txt', 'deep');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'root.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'level1', 'file.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'level1', 'level2', 'deep.txt'))).toBe(true);
});
it('should create destination directory if it does not exist', async () => {
const newDest = path.join(tmpDir, 'new-dest');
await createTestFile(sourceDir, 'test.txt', 'content');
await fileOps.copyDirectory(sourceDir, newDest);
expect(await fs.pathExists(newDest)).toBe(true);
expect(await fs.pathExists(path.join(newDest, 'test.txt'))).toBe(true);
});
});
describe('overwrite behavior', () => {
it('should overwrite existing files by default', async () => {
await createTestFile(sourceDir, 'file.txt', 'new content');
await createTestFile(destDir, 'file.txt', 'old content');
await fileOps.copyDirectory(sourceDir, destDir);
const content = await fs.readFile(path.join(destDir, 'file.txt'), 'utf8');
expect(content).toBe('new content');
});
it('should preserve file content when overwriting', async () => {
await createTestFile(sourceDir, 'data.json', '{"new": true}');
await createTestFile(destDir, 'data.json', '{"old": true}');
await createTestFile(destDir, 'keep.txt', 'preserve this');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.readFile(path.join(destDir, 'data.json'), 'utf8')).toBe('{"new": true}');
// Files not in source should be preserved
expect(await fs.pathExists(path.join(destDir, 'keep.txt'))).toBe(true);
});
});
describe('filtering with shouldIgnore', () => {
it('should filter out .git directories', async () => {
await createTestFile(sourceDir, 'file.txt', 'content');
await createTestFile(sourceDir, '.git/config', 'git config');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'file.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, '.git'))).toBe(false);
});
it('should filter out node_modules directories', async () => {
await createTestFile(sourceDir, 'package.json', '{}');
await createTestFile(sourceDir, 'node_modules/lib/code.js', 'code');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'package.json'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'node_modules'))).toBe(false);
});
it('should filter out *.swp and *.tmp files', async () => {
await createTestFile(sourceDir, 'document.txt', 'content');
await createTestFile(sourceDir, 'document.txt.swp', 'vim swap');
await createTestFile(sourceDir, 'temp.tmp', 'temporary');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'document.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'document.txt.swp'))).toBe(false);
expect(await fs.pathExists(path.join(destDir, 'temp.tmp'))).toBe(false);
});
it('should filter out .DS_Store files', async () => {
await createTestFile(sourceDir, 'file.txt', 'content');
await createTestFile(sourceDir, '.DS_Store', 'mac metadata');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'file.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, '.DS_Store'))).toBe(false);
});
});
describe('edge cases', () => {
it('should handle empty source directory', async () => {
await fileOps.copyDirectory(sourceDir, destDir);
const files = await fs.readdir(destDir);
expect(files).toHaveLength(0);
});
it('should handle Unicode filenames', async () => {
await createTestFile(sourceDir, '测试.txt', 'chinese');
await createTestFile(sourceDir, 'файл.json', 'russian');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, '测试.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'файл.json'))).toBe(true);
});
it('should handle filenames with special characters', async () => {
await createTestFile(sourceDir, 'file with spaces.txt', 'content');
await createTestFile(sourceDir, 'special-chars!@#.md', 'content');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, 'file with spaces.txt'))).toBe(true);
expect(await fs.pathExists(path.join(destDir, 'special-chars!@#.md'))).toBe(true);
});
it('should handle very deep directory nesting', async () => {
const deepPath = Array.from({ length: 10 }, (_, i) => `level${i}`).join('/');
await createTestFile(sourceDir, `${deepPath}/deep.txt`, 'very deep');
await fileOps.copyDirectory(sourceDir, destDir);
expect(await fs.pathExists(path.join(destDir, ...deepPath.split('/'), 'deep.txt'))).toBe(true);
});
it('should preserve file permissions', async () => {
const execFile = path.join(sourceDir, 'script.sh');
await fs.writeFile(execFile, '#!/bin/bash\necho "test"');
await fs.chmod(execFile, 0o755); // Make executable
await fileOps.copyDirectory(sourceDir, destDir);
const destFile = path.join(destDir, 'script.sh');
const stats = await fs.stat(destFile);
// Check if file is executable (user execute bit)
expect((stats.mode & 0o100) !== 0).toBe(true);
});
it('should handle large number of files', async () => {
// Create 50 files
const promises = Array.from({ length: 50 }, (_, i) => createTestFile(sourceDir, `file${i}.txt`, `content ${i}`));
await Promise.all(promises);
await fileOps.copyDirectory(sourceDir, destDir);
const destFiles = await fs.readdir(destDir);
expect(destFiles).toHaveLength(50);
});
});
describe('content integrity', () => {
it('should preserve file content exactly', async () => {
const content = 'Line 1\nLine 2\nLine 3\n';
await createTestFile(sourceDir, 'file.txt', content);
await fileOps.copyDirectory(sourceDir, destDir);
const copiedContent = await fs.readFile(path.join(destDir, 'file.txt'), 'utf8');
expect(copiedContent).toBe(content);
});
it('should preserve binary file content', async () => {
const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
await fs.writeFile(path.join(sourceDir, 'binary.dat'), buffer);
await fileOps.copyDirectory(sourceDir, destDir);
const copiedBuffer = await fs.readFile(path.join(destDir, 'binary.dat'));
expect(copiedBuffer).toEqual(buffer);
});
it('should preserve UTF-8 content', async () => {
const utf8Content = 'Hello 世界 🌍';
await createTestFile(sourceDir, 'utf8.txt', utf8Content);
await fileOps.copyDirectory(sourceDir, destDir);
const copied = await fs.readFile(path.join(destDir, 'utf8.txt'), 'utf8');
expect(copied).toBe(utf8Content);
});
it('should preserve empty files', async () => {
await createTestFile(sourceDir, 'empty.txt', '');
await fileOps.copyDirectory(sourceDir, destDir);
const content = await fs.readFile(path.join(destDir, 'empty.txt'), 'utf8');
expect(content).toBe('');
});
});
});
});