import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { chmod, mkdir, rm, symlink, utimes, writeFile } from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import { runPreflight, sourceFingerprint } from './preflight.mjs'; const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `preflight-${process.pid}`); const requiredBins = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest']; async function fixture(name) { const root = path.join(fixtureRoot, name); await mkdir(path.join(root, 'apps', 'web', 'src', 'app'), { recursive: true }); await writeFile(path.join(root, 'apps', 'web', 'src', 'app', 'page.tsx'), 'export default 1;\n'); return root; } async function installRequiredBins(root) { const binDir = path.join(root, 'node_modules', '.bin'); await mkdir(binDir, { recursive: true }); await Promise.all( requiredBins.map(async (name) => { const target = path.join(binDir, name); await writeFile(target, ''); await chmod(target, 0o755); }), ); } async function certifyGeneratedState(root, links = []) { const nextDir = path.join(root, 'apps', 'web', '.next'); await mkdir(nextDir, { recursive: true }); const manifest = `${JSON.stringify({ version: 1, links })}\n`; const manifestHash = createHash('sha256').update(manifest).digest('hex'); await writeFile(path.join(nextDir, '.mosaic-symlink-manifest'), manifest); await writeFile( path.join(nextDir, '.mosaic-source-hash'), `${JSON.stringify({ version: 1, sourceFingerprint: await sourceFingerprint(root), symlinkManifestHash: manifestHash, })}\n`, ); } test.after(async () => { await rm(fixtureRoot, { recursive: true, force: true }); }); test('missing dependencies have a dedicated exit code and install remediation', async () => { const root = await fixture('missing-deps'); const result = await runPreflight({ root }); assert.equal(result.code, 42); assert.match(result.message, /MOSAIC_PREFLIGHT_MISSING_DEPS/); assert.match(result.message, /run pnpm install/i); }); test('a partial dependency install keeps the dedicated missing-deps result', async () => { const root = await fixture('partial-deps'); await mkdir(path.join(root, 'node_modules', '.bin'), { recursive: true }); await writeFile(path.join(root, 'node_modules', '.bin', 'tsc'), '', { mode: 0o755 }); const result = await runPreflight({ root }); assert.equal(result.code, 42); assert.match(result.message, /turbo/); }); test('a dangling required dependency shim keeps the dedicated missing-deps result', async () => { const root = await fixture('dangling-deps'); await installRequiredBins(root); const turbo = path.join(root, 'node_modules', '.bin', 'turbo'); await rm(turbo); await symlink(path.join(root, 'node_modules', 'missing-turbo'), turbo); const result = await runPreflight({ root }); assert.equal(result.code, 42); assert.match(result.message, /turbo/); }); test('installed dependencies pass when generated state is absent', async () => { const root = await fixture('clean'); await installRequiredBins(root); assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' }); }); test('foreign-owned generated Next state is identified separately from source errors', async () => { const root = await fixture('foreign-next'); await installRequiredBins(root); const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts'); await mkdir(path.dirname(generated), { recursive: true }); await writeFile(generated, 'generated output'); const result = await runPreflight({ root, uid: (process.getuid?.() ?? 0) + 1 }); assert.equal(result.code, 43); assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/); assert.match(result.message, /foreign-owned/); }); test('a generated marker mismatch is identified separately from source errors', async () => { const root = await fixture('stale-next'); await installRequiredBins(root); const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts'); await mkdir(path.dirname(generated), { recursive: true }); await writeFile(generated, 'stale generated output'); await writeFile(path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'), 'old-source'); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/); assert.match(result.message, /apps\/web\/\.next/); assert.match(result.message, /pnpm clean:generated/); }); test('generated-state symbolic links are accepted only when exactly build-certified', async (t) => { await t.test('apps/web/.next itself is rejected when it is a symbolic link', async () => { const root = await fixture('symbolic-next-root'); await installRequiredBins(root); await writeFile(path.join(root, 'outside-generated'), 'not a Next build\n'); await symlink(path.join(root, 'outside-generated'), path.join(root, 'apps', 'web', '.next')); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/); assert.match(result.message, /symbolic link/); }); await t.test('apps/web/.next is rejected when it is not a directory', async () => { const root = await fixture('non-directory-next-root'); await installRequiredBins(root); await writeFile(path.join(root, 'apps', 'web', '.next'), 'not a Next build\n'); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/); assert.match(result.message, /real directory/); }); await t.test('an added descendant symlink is rejected', async () => { const root = await fixture('symbolic-next-added'); await installRequiredBins(root); await certifyGeneratedState(root); await symlink('/etc/hosts', path.join(root, 'apps', 'web', '.next', 'reviewer-symlink')); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /symbolic-link manifest/); }); await t.test('a removed certified descendant symlink is rejected', async () => { const root = await fixture('symbolic-next-removed'); await installRequiredBins(root); const link = path.join(root, 'apps', 'web', '.next', 'dependency-link'); await mkdir(path.dirname(link), { recursive: true }); await symlink('../dependency-one', link); await certifyGeneratedState(root, [{ path: 'dependency-link', target: '../dependency-one' }]); await rm(link); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /symbolic-link manifest/); }); await t.test('a retargeted certified descendant symlink is rejected', async () => { const root = await fixture('symbolic-next-retargeted'); await installRequiredBins(root); const link = path.join(root, 'apps', 'web', '.next', 'dependency-link'); await mkdir(path.dirname(link), { recursive: true }); await symlink('../dependency-one', link); await certifyGeneratedState(root, [{ path: 'dependency-link', target: '../dependency-one' }]); await rm(link); await symlink('../dependency-two', link); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /symbolic-link manifest/); }); await t.test('a manifest edited to whitelist a rogue symlink is rejected', async () => { const root = await fixture('symbolic-next-tampered-manifest'); await installRequiredBins(root); await certifyGeneratedState(root); const nextDir = path.join(root, 'apps', 'web', '.next'); await symlink('/etc/hosts', path.join(nextDir, 'reviewer-symlink')); await writeFile( path.join(nextDir, '.mosaic-symlink-manifest'), `${JSON.stringify({ version: 1, links: [{ path: 'reviewer-symlink', target: '/etc/hosts' }], })}\n`, ); const result = await runPreflight({ root }); assert.equal(result.code, 43); assert.match(result.message, /symbolic-link manifest/); }); await t.test('unchanged canonical-style descendant symlinks are accepted', async () => { const root = await fixture('symbolic-next-certified'); await installRequiredBins(root); const link = path.join( root, 'apps', 'web', '.next', 'standalone', 'node_modules', 'dependency', ); await mkdir(path.dirname(link), { recursive: true }); await symlink('../.pnpm/dependency', link); await certifyGeneratedState(root, [ { path: 'standalone/node_modules/dependency', target: '../.pnpm/dependency' }, ]); assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed', }); }); }); test('the source fingerprint includes inherited TypeScript configuration', async () => { const root = await fixture('inherited-typescript-config'); const config = path.join(root, 'tsconfig.base.json'); await writeFile(config, '{"compilerOptions":{"strict":true}}\n'); const first = await sourceFingerprint(root); await writeFile(config, '{"compilerOptions":{"strict":false}}\n'); const second = await sourceFingerprint(root); assert.notEqual(first, second); }); test('the source fingerprint rejects symbolic-link build inputs', async () => { const root = await fixture('symbolic-source'); await writeFile(path.join(root, 'outside.ts'), 'export default 1;\n'); await symlink(path.join(root, 'outside.ts'), path.join(root, 'apps', 'web', 'src', 'linked.ts')); await assert.rejects(sourceFingerprint(root), /must not be a symbolic link/); }); test('the source fingerprint includes expanded public web build environment', async () => { const root = await fixture('public-build-environment'); const envFile = path.join(root, 'apps', 'web', '.env.production'); await writeFile( envFile, 'RM01_GATEWAY_URL=https://one.example\nNEXT_PUBLIC_RM01_URL=$RM01_GATEWAY_URL\n', ); const first = await sourceFingerprint(root); await writeFile( envFile, 'RM01_GATEWAY_URL=https://two.example\nNEXT_PUBLIC_RM01_URL=$RM01_GATEWAY_URL\n', ); const second = await sourceFingerprint(root); assert.notEqual(first, second); }); test('a matching generation marker accepts incremental output with mixed mtimes', async () => { const root = await fixture('incremental-next'); await installRequiredBins(root); const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts'); await mkdir(path.dirname(generated), { recursive: true }); await writeFile(generated, 'unchanged generated output'); await utimes(generated, new Date('2020-01-01T00:00:00Z'), new Date('2020-01-01T00:00:00Z')); const fresh = path.join(root, 'apps', 'web', '.next', 'types', 'routes.ts'); await writeFile(fresh, 'fresh generated output'); await certifyGeneratedState(root); assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' }); });