fix: harden empty asset materialization

This commit is contained in:
2026-09-02 01:09:21 +03:00
parent 49a4f75c8d
commit bc851e398b
3 changed files with 134 additions and 35 deletions

View File

@@ -0,0 +1,58 @@
import { lstat, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
assertPreparedAssetDirectoryIdentity,
materializeEmptyAssetSet,
} from '../../scripts/materialize-empty-asset-set.mjs';
const temporaryRoots = [];
const createScratchRepository = async () => {
const root = await mkdtemp(join(tmpdir(), 'portfolio-empty-assets-'));
temporaryRoots.push(root);
await mkdir(join(root, 'content/public'), { recursive: true });
await writeFile(join(root, 'content/public/asset-manifest.json'), '{"assets":[]}');
return root;
};
afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
describe('empty asset materialization', () => {
it('creates an empty prepared-asset directory in the supplied repository root', async () => {
const root = await createScratchRepository();
await materializeEmptyAssetSet(root);
expect((await lstat(join(root, 'inputs/prepared-assets'))).isDirectory()).toBe(true);
});
it('rejects a symbolic-link prepared-asset directory', async () => {
const root = await createScratchRepository();
const target = join(root, 'outside');
await mkdir(target);
await mkdir(join(root, 'inputs'), { recursive: true });
await symlink(target, join(root, 'inputs/prepared-assets'));
await expect(materializeEmptyAssetSet(root)).rejects.toThrow(/non-symlink directory/i);
});
it('rejects unexpected prepared-asset members in its supplied repository root', async () => {
const root = await createScratchRepository();
const preparedAssetsPath = join(root, 'inputs/prepared-assets');
await mkdir(preparedAssetsPath, { recursive: true });
await writeFile(join(preparedAssetsPath, 'unexpected.webp'), 'not an asset');
await expect(materializeEmptyAssetSet(root)).rejects.toThrow(/must be empty/i);
});
it('rejects a prepared-asset directory replaced after its identity was captured', async () => {
const root = await createScratchRepository();
const preparedAssetsPath = join(root, 'inputs/prepared-assets');
await mkdir(preparedAssetsPath, { recursive: true });
const original = await lstat(preparedAssetsPath);
await rm(preparedAssetsPath, { recursive: true });
await mkdir(preparedAssetsPath);
await expect(assertPreparedAssetDirectoryIdentity(preparedAssetsPath, original))
.rejects.toThrow(/identity changed/i);
});
});