import { strict as assert } from 'node:assert';
import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import {
contractSnapshotJsonSpecifier,
contractSnapshotTypesSpecifier,
} from '@internal/framework-components/control';
import { computeMigrationHash } from '@internal/migration-tools/hash';
import {
applyMigrationsRootPlan,
discoverMigrationsRoots,
formatSummary,
MigrationLayoutAbortError,
migrateMigrationsRoots,
planMigrationsRoot,
rewriteImportSpecifiers,
} from './migrate-migrations-layout.mjs';
function fakeHash(seed) {
return createHash('sha256').update(seed).digest('hex');
}
function fakeContractJson(storageHash, extra = {}) {
return { storage: { storageHash }, namespaces: {}, ...extra };
}
const CONTRACT_DTS = 'export interface Contract {\n readonly storageHash: string;\n}\n';
function baselineMigrationTs() {
return (
"import { MigrationCLI } from '@internal/cli/migration-cli';\n" +
"import { Migration } from '@internal/family-mongo/migration';\n" +
"import type { Contract as End } from './end-contract';\n" +
"import endContract from './end-contract.json' with { type: 'json' };\n" +
'\n' +
'class M extends Migration<never, End> {\n' +
' override readonly endContractJson = endContract;\n' +
' override get operations() {\n' +
' return [];\n' +
' }\n' +
'}\n' +
'\n' +
'export default M;\n' +
'MigrationCLI.run(import.meta.url, M);\n'
);
}
function deltaMigrationTs() {
return (
"import { MigrationCLI } from '@internal/cli/migration-cli';\n" +
"import { Migration } from '@internal/family-mongo/migration';\n" +
"import type { Contract as End } from './end-contract';\n" +
"import endContract from './end-contract.json' with { type: 'json' };\n" +
"import type { Contract as Start } from './start-contract';\n" +
"import startContract from './start-contract.json' with { type: 'json' };\n" +
'\n' +
'class M extends Migration<Start, End> {\n' +
' override readonly startContractJson = startContract;\n' +
' override readonly endContractJson = endContract;\n' +
' override get operations() {\n' +
' return [];\n' +
' }\n' +
'}\n' +
'\n' +
'export default M;\n' +
'MigrationCLI.run(import.meta.url, M);\n'
);
}
async function writeMigrationPackage(dir, { from, to, ts }) {
await mkdir(dir, { recursive: true });
const ops = [];
const metadataSansHash = {
from,
to,
providedInvariants: [],
createdAt: '2026-01-01T00:00:00.000Z',
};
const migrationHash = computeMigrationHash(metadataSansHash, ops);
const metadata = { ...metadataSansHash, migrationHash };
await writeFile(join(dir, 'migration.json'), `${JSON.stringify(metadata, null, 2)}\n`);
await writeFile(join(dir, 'ops.json'), `${JSON.stringify(ops, null, 2)}\n`);
await writeFile(join(dir, 'migration.ts'), ts);
await writeFile(join(dir, 'end-contract.json'), `${JSON.stringify(fakeContractJson(to))}\n`);
await writeFile(join(dir, 'end-contract.d.ts'), CONTRACT_DTS);
if (from !== null) {
await writeFile(
join(dir, 'start-contract.json'),
`${JSON.stringify(fakeContractJson(from))}\n`,
);
await writeFile(join(dir, 'start-contract.d.ts'), CONTRACT_DTS);
}
return metadata;
}
* Two-migration app-space chain plus a one-package extension space with a
* per-space head contract, mirroring the shapes in
* `examples/prisma-8-demo/migrations`. Returns the hashes used so tests
* can locate the resulting store entries.
*/
async function buildFixture(root) {
const hashA = fakeHash('contract-a');
const hashB = fakeHash('contract-b');
const hashC = fakeHash('contract-c');
const pkg1Dir = join(root, 'app', '20260101T0000_initial');
const pkg2Dir = join(root, 'app', '20260102T0000_second');
const extPkgDir = join(root, 'ext', '20260103T0000_ext_migration');
const extDir = join(root, 'ext');
await writeMigrationPackage(pkg1Dir, { from: null, to: hashA, ts: baselineMigrationTs() });
await writeMigrationPackage(pkg2Dir, { from: hashA, to: hashB, ts: deltaMigrationTs() });
await writeMigrationPackage(extPkgDir, { from: null, to: hashC, ts: baselineMigrationTs() });
await mkdir(join(extDir, 'refs'), { recursive: true });
await writeFile(
join(extDir, 'refs', 'head.json'),
`${JSON.stringify({ hash: hashC, invariants: [] }, null, 2)}\n`,
);
await writeFile(join(extDir, 'contract.json'), `${JSON.stringify(fakeContractJson(hashC))}\n`);
await writeFile(join(extDir, 'contract.d.ts'), CONTRACT_DTS);
return { hashA, hashB, hashC, pkg1Dir, pkg2Dir, extPkgDir, extDir };
}
async function writeRefPair(refsDir, name, hash) {
await writeFile(
join(refsDir, `${name}.json`),
`${JSON.stringify({ hash, invariants: [] }, null, 2)}\n`,
);
await writeFile(
join(refsDir, `${name}.contract.json`),
`${JSON.stringify(fakeContractJson(hash))}\n`,
);
await writeFile(join(refsDir, `${name}.contract.d.ts`), CONTRACT_DTS);
}
async function withTempDir(fn) {
const dir = await mkdtemp(join(tmpdir(), 'migrate-migrations-layout-test-'));
try {
await fn(dir);
} finally {
await rm(dir, { recursive: true, force: true });
}
}
function storageHashHexOf(hash) {
return hash;
}
describe('rewriteImportSpecifiers', () => {
it('rewrites a baseline end-only import block', () => {
const toHash = fakeHash('aa');
const rewritten = rewriteImportSpecifiers(baselineMigrationTs(), {
snapshotsImportPath: '../../snapshots',
toHash,
fromHash: null,
});
assert.equal(rewritten.includes("'./end-contract"), false);
assert.equal(
rewritten.includes(`'${contractSnapshotJsonSpecifier('../../snapshots', toHash)}'`),
true,
);
assert.equal(
rewritten.includes(`'${contractSnapshotTypesSpecifier('../../snapshots', toHash)}'`),
true,
);
});
it('rewrites a start+end import block, leaving symbols untouched', () => {
const toHash = fakeHash('bb');
const fromHash = fakeHash('aa');
const rewritten = rewriteImportSpecifiers(deltaMigrationTs(), {
snapshotsImportPath: '../snapshots',
toHash,
fromHash,
});
assert.equal(rewritten.includes("'./start-contract"), false);
assert.equal(rewritten.includes("'./end-contract"), false);
assert.equal(
rewritten.includes(`'${contractSnapshotJsonSpecifier('../snapshots', toHash)}'`),
true,
);
assert.equal(
rewritten.includes(`'${contractSnapshotTypesSpecifier('../snapshots', fromHash)}'`),
true,
);
assert.match(rewritten, /import startContract from/);
assert.match(rewritten, /class M extends Migration<Start, End>/);
});
});
describe('migrateMigrationsRoots — happy path', () => {
it('populates the store, deletes siblings, rewrites migration.ts, leaves migration.json/head.json byte-identical', async () => {
await withTempDir(async (root) => {
const { hashA, hashB, hashC, pkg1Dir, pkg2Dir, extPkgDir, extDir } = await buildFixture(root);
const migrationJsonABefore = await readFile(join(pkg1Dir, 'migration.json'), 'utf8');
const migrationJsonBBefore = await readFile(join(pkg2Dir, 'migration.json'), 'utf8');
const headRefBefore = await readFile(join(extDir, 'refs', 'head.json'), 'utf8');
const [summary] = await migrateMigrationsRoots([root]);
assert.equal(summary.packagesProcessed, 3);
assert.equal(summary.spacesProcessed, 1);
assert.equal(summary.storeEntriesWritten, 3);
assert.equal(summary.storeEntriesAlreadyPresent, 2);
assert.equal(summary.filesDeleted, 2 + 4 + 2 + 2);
assert.equal(summary.migrationHashesVerified, 3);
for (const hash of [hashA, hashB, hashC]) {
const storeDir = join(root, 'snapshots', storageHashHexOf(hash));
const contractJsonPath = join(storeDir, 'contract.json');
const contractDtsPath = join(storeDir, 'contract.d.ts');
const contractJsonText = await readFile(contractJsonPath, 'utf8');
const contractDtsText = await readFile(contractDtsPath, 'utf8');
assert.deepEqual(JSON.parse(contractJsonText), fakeContractJson(hash));
assert.equal(contractJsonText.endsWith('\n'), true);
assert.equal(contractJsonText.trimEnd().includes('\n'), false);
assert.equal(contractDtsText, CONTRACT_DTS);
}
for (const [dir, names] of [
[pkg1Dir, ['end-contract.json', 'end-contract.d.ts']],
[
pkg2Dir,
['end-contract.json', 'end-contract.d.ts', 'start-contract.json', 'start-contract.d.ts'],
],
[extDir, ['contract.json', 'contract.d.ts']],
]) {
for (const name of names) {
await assert.rejects(readFile(join(dir, name)), { code: 'ENOENT' });
}
}
const pkg1Ts = await readFile(join(pkg1Dir, 'migration.ts'), 'utf8');
assert.equal(pkg1Ts.includes("'./end-contract"), false);
assert.equal(
pkg1Ts.includes(`'${contractSnapshotJsonSpecifier('../../snapshots', hashA)}'`),
true,
);
assert.equal(
pkg1Ts.includes(`'${contractSnapshotTypesSpecifier('../../snapshots', hashA)}'`),
true,
);
const pkg2Ts = await readFile(join(pkg2Dir, 'migration.ts'), 'utf8');
assert.equal(pkg2Ts.includes("'./end-contract"), false);
assert.equal(pkg2Ts.includes("'./start-contract"), false);
assert.equal(
pkg2Ts.includes(`'${contractSnapshotJsonSpecifier('../../snapshots', hashB)}'`),
true,
);
assert.equal(
pkg2Ts.includes(`'${contractSnapshotJsonSpecifier('../../snapshots', hashA)}'`),
true,
);
const extPkgTs = await readFile(join(extPkgDir, 'migration.ts'), 'utf8');
assert.equal(
extPkgTs.includes(`'${contractSnapshotJsonSpecifier('../../snapshots', hashC)}'`),
true,
);
assert.equal(await readFile(join(pkg1Dir, 'migration.json'), 'utf8'), migrationJsonABefore);
assert.equal(await readFile(join(pkg2Dir, 'migration.json'), 'utf8'), migrationJsonBBefore);
assert.equal(await readFile(join(extDir, 'refs', 'head.json'), 'utf8'), headRefBefore);
});
});
it('is a no-op on a second run over an already-migrated tree', async () => {
await withTempDir(async (root) => {
await buildFixture(root);
await migrateMigrationsRoots([root]);
const [summary] = await migrateMigrationsRoots([root]);
assert.equal(summary.packagesProcessed, 3);
assert.equal(summary.spacesProcessed, 0);
assert.equal(summary.storeEntriesWritten, 0);
assert.equal(summary.storeEntriesAlreadyPresent, 0);
assert.equal(summary.filesDeleted, 0);
assert.equal(summary.migrationHashesVerified, 3);
});
});
});
describe('migrateMigrationsRoots — abort on inner hash mismatch', () => {
it('aborts before writing or deleting anything, anywhere in the root', async () => {
await withTempDir(async (root) => {
const { pkg1Dir, pkg2Dir, extDir } = await buildFixture(root);
await writeFile(
join(pkg2Dir, 'start-contract.json'),
`${JSON.stringify(fakeContractJson(fakeHash('wrong-hash')))}\n`,
);
await assert.rejects(migrateMigrationsRoots([root]), MigrationLayoutAbortError);
await assert.rejects(readdir(join(root, 'snapshots')), { code: 'ENOENT' });
for (const [dir, names] of [
[pkg1Dir, ['end-contract.json', 'end-contract.d.ts']],
[
pkg2Dir,
['end-contract.json', 'end-contract.d.ts', 'start-contract.json', 'start-contract.d.ts'],
],
[extDir, ['contract.json', 'contract.d.ts']],
]) {
for (const name of names) {
await assert.doesNotReject(readFile(join(dir, name)));
}
}
const pkg1Ts = await readFile(join(pkg1Dir, 'migration.ts'), 'utf8');
assert.equal(pkg1Ts.includes("'./end-contract.json'"), true);
});
});
it('planMigrationsRoot rejects and applyMigrationsRootPlan is never reached', async () => {
await withTempDir(async (root) => {
const { pkg1Dir } = await buildFixture(root);
await writeFile(
join(pkg1Dir, 'end-contract.json'),
`${JSON.stringify(fakeContractJson(fakeHash('wrong-hash')))}\n`,
);
await assert.rejects(planMigrationsRoot(root), MigrationLayoutAbortError);
});
});
});
describe('migrateMigrationsRoots — ref-paired snapshot folding', () => {
it('folds ref-paired snapshots into the store, deletes the pair, leaves the pointer untouched', async () => {
await withTempDir(async (root) => {
const { hashA, extDir } = await buildFixture(root);
const hashD = fakeHash('contract-d');
const refsDir = join(extDir, 'refs');
await writeRefPair(refsDir, 'db', hashA);
await writeRefPair(refsDir, 'staging', hashD);
const dbPointerBefore = await readFile(join(refsDir, 'db.json'), 'utf8');
const stagingPointerBefore = await readFile(join(refsDir, 'staging.json'), 'utf8');
const [summary] = await migrateMigrationsRoots([root]);
assert.equal(summary.refsProcessed, 2);
assert.equal(await readFile(join(refsDir, 'db.json'), 'utf8'), dbPointerBefore);
assert.equal(await readFile(join(refsDir, 'staging.json'), 'utf8'), stagingPointerBefore);
for (const name of [
'db.contract.json',
'db.contract.d.ts',
'staging.contract.json',
'staging.contract.d.ts',
]) {
await assert.rejects(readFile(join(refsDir, name)), { code: 'ENOENT' });
}
const storeDir = join(root, 'snapshots', storageHashHexOf(hashD));
assert.deepEqual(
JSON.parse(await readFile(join(storeDir, 'contract.json'), 'utf8')),
fakeContractJson(hashD),
);
assert.equal(await readFile(join(storeDir, 'contract.d.ts'), 'utf8'), CONTRACT_DTS);
const [summary2] = await migrateMigrationsRoots([root]);
assert.equal(summary2.refsProcessed, 0);
assert.equal(summary2.storeEntriesWritten, 0);
assert.equal(summary2.filesDeleted, 0);
});
});
it('aborts when a ref-paired contract.json has no sibling pointer', async () => {
await withTempDir(async (root) => {
const { extDir } = await buildFixture(root);
const refsDir = join(extDir, 'refs');
const hash = fakeHash('orphan');
await writeFile(
join(refsDir, 'orphan.contract.json'),
`${JSON.stringify(fakeContractJson(hash))}\n`,
);
await writeFile(join(refsDir, 'orphan.contract.d.ts'), CONTRACT_DTS);
await assert.rejects(migrateMigrationsRoots([root]), MigrationLayoutAbortError);
await assert.rejects(readdir(join(root, 'snapshots')), { code: 'ENOENT' });
await assert.doesNotReject(readFile(join(refsDir, 'orphan.contract.json')));
await assert.doesNotReject(readFile(join(refsDir, 'orphan.contract.d.ts')));
});
});
it('aborts when the ref-paired contract hash disagrees with the pointer', async () => {
await withTempDir(async (root) => {
const { hashA, extDir } = await buildFixture(root);
const refsDir = join(extDir, 'refs');
await writeRefPair(refsDir, 'db', hashA);
await writeFile(
join(refsDir, 'db.contract.json'),
`${JSON.stringify(fakeContractJson(fakeHash('wrong-hash')))}\n`,
);
await assert.rejects(migrateMigrationsRoots([root]), MigrationLayoutAbortError);
await assert.rejects(readdir(join(root, 'snapshots')), { code: 'ENOENT' });
await assert.doesNotReject(readFile(join(refsDir, 'db.json')));
await assert.doesNotReject(readFile(join(refsDir, 'db.contract.json')));
});
});
it('aborts when the ref-paired contract.d.ts is missing', async () => {
await withTempDir(async (root) => {
const { extDir } = await buildFixture(root);
const refsDir = join(extDir, 'refs');
const hash = fakeHash('nodts');
await writeFile(
join(refsDir, 'nodts.json'),
`${JSON.stringify({ hash, invariants: [] }, null, 2)}\n`,
);
await writeFile(
join(refsDir, 'nodts.contract.json'),
`${JSON.stringify(fakeContractJson(hash))}\n`,
);
await assert.rejects(migrateMigrationsRoots([root]), MigrationLayoutAbortError);
await assert.rejects(readdir(join(root, 'snapshots')), { code: 'ENOENT' });
await assert.doesNotReject(readFile(join(refsDir, 'nodts.contract.json')));
});
});
});
describe('discoverMigrationsRoots', () => {
it('finds a deep consumer-project root via app/*/migration.json, not its space subdir', async () => {
await withTempDir(async (start) => {
const root = join(start, 'examples', 'demo', 'migrations');
await buildFixture(root);
const roots = await discoverMigrationsRoots(start);
assert.deepEqual(roots, [root]);
});
});
it('finds a shallow extension-repo root via */migration.json', async () => {
await withTempDir(async (start) => {
const root = join(start, 'packages', '3-extensions', 'demo-ext', 'migrations');
await writeMigrationPackage(join(root, '20260101T0000_initial'), {
from: null,
to: fakeHash('shallow-a'),
ts: baselineMigrationTs(),
});
const roots = await discoverMigrationsRoots(start);
assert.deepEqual(roots, [root]);
});
});
it('ignores a directory named migrations with no migration packages inside', async () => {
await withTempDir(async (start) => {
await mkdir(join(start, 'docs', 'migrations'), { recursive: true });
await writeFile(join(start, 'docs', 'migrations', 'notes.md'), '# not a migrations root\n');
const roots = await discoverMigrationsRoots(start);
assert.deepEqual(roots, []);
});
});
});
describe('formatSummary', () => {
it('renders per-root and total lines', () => {
const text = formatSummary([
{
root: '/repo/examples/demo/migrations',
packagesProcessed: 2,
spacesProcessed: 1,
refsProcessed: 2,
storeEntriesWritten: 3,
storeEntriesAlreadyPresent: 1,
filesDeleted: 8,
migrationHashesVerified: 2,
},
]);
assert.match(text, /processed 1 root\(s\)/);
assert.match(text, /refs: 2/);
assert.match(text, /store writes: 3 \(1 already present\)/);
assert.match(text, /TOTAL: 1 root\(s\), 3 store writes \(1 already present\), 8 files deleted/);
});
});
describe('applyMigrationsRootPlan', () => {
it('aborts if ops.json changes underneath a planned package (defence in depth)', async () => {
await withTempDir(async (root) => {
const { pkg1Dir } = await buildFixture(root);
const plan = await planMigrationsRoot(root);
await writeFile(join(pkg1Dir, 'ops.json'), '[{"op":"tampered"}]\n');
await assert.rejects(applyMigrationsRootPlan(plan), MigrationLayoutAbortError);
});
});
});