Skip to content

Commit dac067d

Browse files
committed
improve tests and upgrade fuz_util peer dep
1 parent 620c7ad commit dac067d

33 files changed

Lines changed: 201 additions & 68 deletions

.changeset/quick-jeans-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@fuzdev/gro': minor
3+
---
4+
5+
upgrade fuz_util peer dep

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
},
6161
"peerDependencies": {
6262
"@fuzdev/blake3_wasm": "^0.1.0",
63-
"@fuzdev/fuz_util": ">=0.52.1",
63+
"@fuzdev/fuz_util": ">=0.53.0",
6464
"@sveltejs/kit": "^2",
6565
"esbuild": "^0.27.0",
6666
"svelte": "^5",
@@ -87,7 +87,7 @@
8787
"@fuzdev/fuz_ui": "^0.185.2",
8888
"@fuzdev/fuz_util": "^0.53.0",
8989
"@jridgewell/trace-mapping": "^0.3.31",
90-
"@ryanatkn/eslint-config": "^0.9.0",
90+
"@ryanatkn/eslint-config": "^0.10.0",
9191
"@sveltejs/adapter-static": "^3.0.10",
9292
"@sveltejs/kit": "^2.53.0",
9393
"@sveltejs/package": "^2.5.7",

src/lib/build_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const BuildOutputEntry = z.strictObject({
2323
path: z
2424
.string()
2525
.meta({description: "relative path from project root (e.g., 'build/index.html')."}),
26-
hash: z.string().meta({description: 'SHA-256 hash of file contents'}),
26+
hash: z.string().meta({description: 'BLAKE3 hash of file contents'}),
2727
size: z.number().meta({description: 'file size in bytes'}),
2828
mtime: z.number().meta({description: 'modification time in milliseconds since epoch'}),
2929
ctime: z.number().meta({

src/routes/library.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
},
7272
"peerDependencies": {
7373
"@fuzdev/blake3_wasm": "^0.1.0",
74-
"@fuzdev/fuz_util": ">=0.52.1",
74+
"@fuzdev/fuz_util": ">=0.53.0",
7575
"@sveltejs/kit": "^2",
7676
"esbuild": "^0.27.0",
7777
"svelte": "^5",
@@ -98,7 +98,7 @@
9898
"@fuzdev/fuz_ui": "^0.185.2",
9999
"@fuzdev/fuz_util": "^0.53.0",
100100
"@jridgewell/trace-mapping": "^0.3.31",
101-
"@ryanatkn/eslint-config": "^0.9.0",
101+
"@ryanatkn/eslint-config": "^0.10.0",
102102
"@sveltejs/adapter-static": "^3.0.10",
103103
"@sveltejs/kit": "^2.53.0",
104104
"@sveltejs/package": "^2.5.7",

src/test/build_cache.cache_validation.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ describe('is_build_cache_valid', () => {
108108
expect(log.debug).toHaveBeenCalledWith('Build cache invalid: git commit changed');
109109
});
110110

111+
test('uses pre-computed git_commit and skips git_current_commit_hash', async () => {
112+
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
113+
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
114+
const {readFile} = vi.mocked(await import('node:fs/promises'));
115+
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
116+
117+
const metadata = create_mock_build_cache_metadata({
118+
git_commit: 'precomputed_abc',
119+
build_cache_config_hash: 'jkl012',
120+
});
121+
122+
vi.mocked(fs_exists).mockResolvedValue(true);
123+
vi.mocked(readFile).mockResolvedValue(JSON.stringify(metadata));
124+
vi.mocked(hash_blake3).mockReturnValue('jkl012');
125+
126+
const config = await create_mock_config();
127+
const log = create_mock_logger();
128+
129+
const result = await is_build_cache_valid(config, log, 'precomputed_abc');
130+
131+
expect(result).toBe(true);
132+
expect(git_current_commit_hash).not.toHaveBeenCalled();
133+
});
134+
111135
test('returns false when build_cache_config hash differs', async () => {
112136
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
113137
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));

src/test/build_cache.concurrency.test.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ describe('race condition: cache file modification during validation', () => {
5252
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
5353
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
5454

55-
const initial_metadata = create_mock_build_cache_metadata({git_commit: 'abc123'});
55+
// hash_blake3 mock returns 'hash123', so config's build_cache_config_hash will be 'hash123'
56+
const initial_metadata = create_mock_build_cache_metadata({
57+
git_commit: 'abc123',
58+
build_cache_config_hash: 'hash123',
59+
});
5660
const modified_metadata = create_mock_build_cache_metadata({git_commit: 'def456'});
5761

5862
// Simulate cache file being modified during validation
@@ -72,12 +76,12 @@ describe('race condition: cache file modification during validation', () => {
7276
const config = await create_mock_config();
7377
const log = create_mock_logger();
7478

75-
// This represents a very unlikely race condition, but system should handle gracefully
79+
// Metadata is loaded once and used throughout validation,
80+
// so later file modifications don't affect the result
7681
const result = await is_build_cache_valid(config, log);
7782

78-
// Cache validation should happen with the initially loaded metadata
79-
// The result depends on when the validation happens vs when file is modified
80-
expect(typeof result).toBe('boolean');
83+
// initial metadata matches current git commit, so cache is valid
84+
expect(result).toBe(true);
8185
});
8286

8387
test('handles concurrent cache writes', async () => {
@@ -86,19 +90,14 @@ describe('race condition: cache file modification during validation', () => {
8690
const metadata1 = create_mock_build_cache_metadata({git_commit: 'commit1'});
8791
const metadata2 = create_mock_build_cache_metadata({git_commit: 'commit2'});
8892

89-
let write_count = 0;
90-
vi.mocked(writeFile).mockImplementation(() => {
91-
write_count++;
92-
// Simulate concurrent writes - not expected in practice but should not crash
93-
return Promise.resolve();
94-
});
93+
vi.mocked(mkdir).mockResolvedValue(undefined);
94+
vi.mocked(writeFile).mockResolvedValue(undefined);
9595

96-
// Try to save two different cache states
97-
await save_build_cache_metadata(metadata1);
98-
await save_build_cache_metadata(metadata2);
96+
// actually concurrent writes via Promise.all
97+
await Promise.all([save_build_cache_metadata(metadata1), save_build_cache_metadata(metadata2)]);
9998

100-
// Both should complete without throwing
101-
expect(write_count).toBe(2);
99+
// both should complete without throwing
100+
expect(writeFile).toHaveBeenCalledTimes(2);
102101
expect(mkdir).toHaveBeenCalledTimes(2);
103102
});
104103

@@ -108,7 +107,11 @@ describe('race condition: cache file modification during validation', () => {
108107
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
109108
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
110109

111-
const metadata = create_mock_build_cache_metadata({git_commit: 'abc123'});
110+
// hash_blake3 mock returns 'hash123', so config's build_cache_config_hash will be 'hash123'
111+
const metadata = create_mock_build_cache_metadata({
112+
git_commit: 'abc123',
113+
build_cache_config_hash: 'hash123',
114+
});
112115

113116
vi.mocked(fs_exists).mockResolvedValue(true);
114117
vi.mocked(readFile).mockResolvedValue(JSON.stringify(metadata));
@@ -118,19 +121,15 @@ describe('race condition: cache file modification during validation', () => {
118121
const config = await create_mock_config();
119122
const log = create_mock_logger();
120123

121-
// Simulate multiple concurrent cache validation operations
122-
// In practice this shouldn't happen, but system should handle gracefully
124+
// multiple concurrent validations should all succeed
123125
const validations = await Promise.all([
124126
is_build_cache_valid(config, log),
125127
is_build_cache_valid(config, log),
126128
is_build_cache_valid(config, log),
127129
]);
128130

129-
// All validations should complete without throwing
130-
expect(validations).toHaveLength(3);
131-
validations.forEach((result) => {
132-
expect(typeof result).toBe('boolean');
133-
});
131+
// all validations should return true since metadata matches
132+
expect(validations).toEqual([true, true, true]);
134133
});
135134

136135
// Note: Git commit changing during build is tested at the integration level

src/test/build_cache.creation.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,35 @@ describe('create_build_cache_metadata', () => {
295295
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('Not in a git repository'));
296296
});
297297

298+
test('uses pre-passed build_dirs and skips discovery', async () => {
299+
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
300+
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
301+
const {readdir, stat, readFile} = vi.mocked(await import('node:fs/promises'));
302+
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
303+
304+
vi.mocked(git_current_commit_hash).mockResolvedValue('abc123');
305+
vi.mocked(fs_exists).mockResolvedValue(true);
306+
vi.mocked(readdir).mockImplementation((path: any) => {
307+
if (path === 'custom_out') {
308+
return Promise.resolve([mock_file_entry('app.js')] as any);
309+
}
310+
return Promise.resolve([] as any);
311+
});
312+
vi.mocked(stat).mockResolvedValue(mock_file_stats());
313+
vi.mocked(readFile).mockResolvedValue(Buffer.from('content'));
314+
vi.mocked(hash_blake3).mockReturnValue('hash_val');
315+
316+
const config = await create_mock_config();
317+
const log = create_mock_logger();
318+
319+
const result = await create_build_cache_metadata(config, log, 'abc123', ['custom_out']);
320+
321+
expect(result.outputs).toHaveLength(1);
322+
expect(result.outputs[0]!.path).toBe('custom_out/app.js');
323+
// readdir('.') is for discovery — should not be called when build_dirs is provided
324+
expect(readdir).not.toHaveBeenCalledWith('.');
325+
});
326+
298327
test('includes correct build_cache_config_hash', async () => {
299328
const {git_current_commit_hash} = await import('@fuzdev/fuz_util/git.js');
300329
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));

src/test/build_cache.discovery.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,26 @@ describe('discover_build_output_dirs', () => {
133133
expect(result).not.toContain('dist_config.json');
134134
});
135135

136+
test('skips dist_ entry when stat fails during iteration', async () => {
137+
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
138+
const {readdir, stat} = vi.mocked(await import('node:fs/promises'));
139+
140+
vi.mocked(fs_exists).mockResolvedValue(false);
141+
vi.mocked(readdir).mockResolvedValue(['dist_vanished', 'dist_ok'] as any);
142+
vi.mocked(stat).mockImplementation((path: any) => {
143+
if (String(path) === 'dist_vanished') {
144+
return Promise.reject(new Error('ENOENT: no such file or directory'));
145+
}
146+
return Promise.resolve({isDirectory: () => true} as any);
147+
});
148+
149+
const result = await discover_build_output_dirs();
150+
151+
// vanished entry is skipped, surviving entry is included
152+
expect(result).toContain('dist_ok');
153+
expect(result).not.toContain('dist_vanished');
154+
});
155+
136156
test('skips non-dist_ prefixed directories', async () => {
137157
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
138158
const {readdir, stat} = vi.mocked(await import('node:fs/promises'));
@@ -160,6 +180,12 @@ describe('collect_build_outputs', () => {
160180
vi.clearAllMocks();
161181
});
162182

183+
test('returns empty array for empty dirs array', async () => {
184+
const result = await collect_build_outputs([]);
185+
186+
expect(result).toEqual([]);
187+
});
188+
163189
test('hashes all files in build directory', async () => {
164190
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
165191
const {readdir, readFile, stat} = vi.mocked(await import('node:fs/promises'));

src/test/build_cache.file_validation.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ describe('validate_build_cache', () => {
6565

6666
test('returns false when output file is missing', async () => {
6767
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
68+
const {stat, readFile} = vi.mocked(await import('node:fs/promises'));
69+
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
6870

6971
const metadata = create_mock_build_cache_metadata({
7072
outputs: [create_mock_output_entry('build/index.html')],
@@ -75,11 +77,16 @@ describe('validate_build_cache', () => {
7577
const result = await validate_build_cache(metadata);
7678

7779
expect(result).toBe(false);
80+
// should not attempt stat or hash for missing files
81+
expect(stat).not.toHaveBeenCalled();
82+
expect(readFile).not.toHaveBeenCalled();
83+
expect(hash_blake3).not.toHaveBeenCalled();
7884
});
7985

8086
test('returns false when output file size differs (fast path)', async () => {
8187
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
82-
const {stat} = vi.mocked(await import('node:fs/promises'));
88+
const {stat, readFile} = vi.mocked(await import('node:fs/promises'));
89+
const {hash_blake3} = await import('@fuzdev/fuz_util/hash_blake3.js');
8390

8491
const metadata = create_mock_build_cache_metadata({
8592
outputs: [create_mock_output_entry('build/index.html', {hash: 'expected_hash', size: 1024})],
@@ -91,6 +98,9 @@ describe('validate_build_cache', () => {
9198
const result = await validate_build_cache(metadata);
9299

93100
expect(result).toBe(false);
101+
// fast path: size mismatch skips expensive hashing
102+
expect(readFile).not.toHaveBeenCalled();
103+
expect(hash_blake3).not.toHaveBeenCalled();
94104
});
95105

96106
test('returns false when output file hash does not match', async () => {
@@ -180,6 +190,14 @@ describe('validate_build_cache', () => {
180190
expect(result).toBe(false);
181191
});
182192

193+
test('returns true when metadata has no outputs', async () => {
194+
const metadata = create_mock_build_cache_metadata({outputs: []});
195+
196+
const result = await validate_build_cache(metadata);
197+
198+
expect(result).toBe(true);
199+
});
200+
183201
test('returns false when file becomes inaccessible during hash validation', async () => {
184202
const {fs_exists} = vi.mocked(await import('@fuzdev/fuz_util/fs.js'));
185203
const {readFile, stat} = vi.mocked(await import('node:fs/promises'));

0 commit comments

Comments
 (0)