feat(smartfs.directory): Add directory treeHash: deterministic content-based hashing of directory trees with streaming and algorithm option

This commit is contained in:
2025-12-02 08:35:37 +00:00
parent c73a931dad
commit ebc7de4503
6 changed files with 242 additions and 28 deletions

View File

@@ -256,6 +256,117 @@ tap.test('should handle file watching', async () => {
});
});
// --- treeHash tests ---
tap.test('should compute treeHash for a directory', async () => {
const dirPath = path.join(tempDir, 'hash-test');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('content1');
await smartFs.file(path.join(dirPath, 'file2.txt')).write('content2');
const hash = await smartFs.directory(dirPath).treeHash();
expect(hash).toBeTruthy();
expect(typeof hash).toEqual('string');
expect(hash.length).toEqual(64); // SHA-256 produces 64 hex chars
});
tap.test('treeHash should be deterministic (same content = same hash)', async () => {
const dirPath = path.join(tempDir, 'hash-deterministic');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'a.txt')).write('aaa');
await smartFs.file(path.join(dirPath, 'b.txt')).write('bbb');
const hash1 = await smartFs.directory(dirPath).treeHash();
const hash2 = await smartFs.directory(dirPath).treeHash();
expect(hash1).toEqual(hash2);
});
tap.test('treeHash should change when file content changes', async () => {
const dirPath = path.join(tempDir, 'hash-content-change');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.txt')).write('original');
const hashBefore = await smartFs.directory(dirPath).treeHash();
await smartFs.file(path.join(dirPath, 'file.txt')).write('modified');
const hashAfter = await smartFs.directory(dirPath).treeHash();
expect(hashBefore).not.toEqual(hashAfter);
});
tap.test('treeHash should change when file is added', async () => {
const dirPath = path.join(tempDir, 'hash-file-add');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file1.txt')).write('content1');
const hashBefore = await smartFs.directory(dirPath).treeHash();
await smartFs.file(path.join(dirPath, 'file2.txt')).write('content2');
const hashAfter = await smartFs.directory(dirPath).treeHash();
expect(hashBefore).not.toEqual(hashAfter);
});
tap.test('treeHash should work recursively', async () => {
const dirPath = path.join(tempDir, 'hash-recursive');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'root.txt')).write('root');
await smartFs.directory(path.join(dirPath, 'sub')).create();
await smartFs.file(path.join(dirPath, 'sub', 'nested.txt')).write('nested');
// Non-recursive should only include root.txt
const hashNonRecursive = await smartFs.directory(dirPath).treeHash();
// Recursive should include both files
const hashRecursive = await smartFs.directory(dirPath).recursive().treeHash();
expect(hashNonRecursive).not.toEqual(hashRecursive);
});
tap.test('treeHash should respect filter', async () => {
const dirPath = path.join(tempDir, 'hash-filter');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.ts')).write('typescript');
await smartFs.file(path.join(dirPath, 'file.js')).write('javascript');
// Hash only .ts files
const hashTs = await smartFs.directory(dirPath).filter(/\.ts$/).treeHash();
// Hash only .js files
const hashJs = await smartFs.directory(dirPath).filter(/\.js$/).treeHash();
// Should be different since they're hashing different files
expect(hashTs).not.toEqual(hashJs);
});
tap.test('treeHash should support different algorithms', async () => {
const dirPath = path.join(tempDir, 'hash-algorithm');
await smartFs.directory(dirPath).create();
await smartFs.file(path.join(dirPath, 'file.txt')).write('test content');
const sha256 = await smartFs.directory(dirPath).treeHash({ algorithm: 'sha256' });
const sha512 = await smartFs.directory(dirPath).treeHash({ algorithm: 'sha512' });
expect(sha256.length).toEqual(64); // SHA-256 = 64 hex chars
expect(sha512.length).toEqual(128); // SHA-512 = 128 hex chars
expect(sha256).not.toEqual(sha512);
});
tap.test('treeHash of empty directory should return consistent hash', async () => {
const dirPath = path.join(tempDir, 'hash-empty');
await smartFs.directory(dirPath).create();
const hash1 = await smartFs.directory(dirPath).treeHash();
const hash2 = await smartFs.directory(dirPath).treeHash();
expect(hash1).toEqual(hash2);
expect(hash1.length).toEqual(64);
});
tap.test('cleanup temp directory', async () => {
await fs.rm(tempDir, { recursive: true, force: true });
expect(true).toEqual(true);