import assert from 'node:assert/strict'; import { access, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import { buildWeb } from './build-web.mjs'; const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `build-web-${process.pid}`); async function fixture(name) { const root = path.join(fixtureRoot, name); await mkdir(path.join(root, 'apps', 'web', '.next'), { recursive: true }); return root; } async function exists(target) { try { await access(target); return true; } catch { return false; } } test.after(async () => { await rm(fixtureRoot, { recursive: true, force: true }); }); test('a successful web build atomically publishes its source and symlink certification', async () => { const root = await fixture('success'); const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'); const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest'); await buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => {} }); assert.deepEqual(JSON.parse(await readFile(marker, 'utf8')), { version: 1, sourceFingerprint: 'certified', symlinkManifestHash: '8a5a375cea6a55d24bd5f875856da63feba33adbefb15a92a0007719b84bcf11', }); assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n'); }); test('a failed web build leaves no certification marker', async () => { const root = await fixture('failure'); const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'); const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest'); await writeFile(marker, 'stale\n'); await writeFile(manifest, 'stale\n'); await assert.rejects( buildWeb({ root, fingerprint: async () => 'before', runBuild: async () => { throw new Error('build failed'); }, }), /build failed/, ); assert.equal(await exists(marker), false); assert.equal(await exists(manifest), false); }); test('overlapping web builds are serialized while the marker remains absent', async () => { const root = await fixture('overlap'); const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'); const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest'); await writeFile(marker, 'stale\n'); await writeFile(manifest, 'stale\n'); let releaseFirst; let secondEntered = false; const firstEntered = new Promise((resolve) => { releaseFirst = resolve; }); let markFirstEntered; const firstStarted = new Promise((resolve) => { markFirstEntered = resolve; }); const first = buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => { markFirstEntered(); await firstEntered; }, }); await firstStarted; const second = buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => { secondEntered = true; }, }); await new Promise((resolve) => setTimeout(resolve, 75)); assert.equal(secondEntered, false); assert.equal(await exists(marker), false); assert.equal(await exists(manifest), false); releaseFirst(); await Promise.all([first, second]); assert.equal(secondEntered, true); assert.equal(JSON.parse(await readFile(marker, 'utf8')).sourceFingerprint, 'certified'); assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n'); }); test('a build that replaces .next with a symbolic link cannot publish outside the checkout', async () => { const root = await fixture('symbolic-next'); const nextDir = path.join(root, 'apps', 'web', '.next'); const outside = path.join(root, 'outside-generated'); await mkdir(outside); await assert.rejects( buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => { await rm(nextDir, { recursive: true }); await symlink(outside, nextDir); }, }), /must be a real directory/, ); assert.equal(await exists(path.join(outside, '.mosaic-source-hash')), false); assert.equal(await exists(path.join(outside, '.mosaic-symlink-manifest')), false); }); test('inputs changed during a web build are not certified', async () => { const root = await fixture('changed-inputs'); const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'); const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest'); const fingerprints = ['before', 'after']; await assert.rejects( buildWeb({ root, fingerprint: async () => fingerprints.shift(), runBuild: async () => {}, }), /inputs changed during next build/, ); assert.equal(await exists(marker), false); assert.equal(await exists(manifest), false); });