import assert from 'node:assert/strict'; import { access, mkdir, readFile, rm, 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 fingerprint', async () => { const root = await fixture('success'); const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'); await buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => {} }); assert.equal(await readFile(marker, 'utf8'), 'certified\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'); await writeFile(marker, '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); }); 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'); await writeFile(marker, '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); releaseFirst(); await Promise.all([first, second]); assert.equal(secondEntered, true); assert.equal(await readFile(marker, 'utf8'), 'certified\n'); }); 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 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); });