feat(smartfs.directory): Add directory treeHash: deterministic content-based hashing of directory trees with streaming and algorithm option
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user