Files
quixos-protocol/test/migrations.test.ts

76 lines
2.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
contentDigest,
validateMigrationCatalog,
selectMigrationPath,
type MigrationCatalog,
} from "../src/capability-model/index.js";
const before = { fields: ["name"] },
after = { fields: ["first", "last"] };
const from = contentDigest(before),
to = contentDigest(after);
const catalog = (): MigrationCatalog => ({
schemaVersion: 1,
contracts: { [from]: before, [to]: after },
migrations: [
{
id: "split-name",
scopeId: "person-name",
from,
to,
implementation: {
exportId: "migrate-name",
file: "src/migrate-name.ts",
digest: contentDigest("implementation"),
},
predecessors: [],
ports: [
{ name: "source", view: "old", access: ["read"], contractDigest: from },
{ name: "target", view: "new", access: ["write"], contractDigest: to },
],
},
],
});
test("published migrations retain exact old contracts and explicit local bindings", () => {
const value = validateMigrationCatalog(catalog(), new Set(["migrate-name"]));
const selection = {
scopeId: "person-name",
from,
to,
path: ["split-name"],
bindings: { source: "old-slot", target: "new-slot" },
};
const result = selectMigrationPath(value, selection);
assert.equal(result[0].alreadyApplied, false);
assert.equal(
selectMigrationPath(value, selection, new Map([["split-name", result[0].digest]]))[0].alreadyApplied,
true,
);
const changed = catalog();
changed.migrations[0].implementation.digest = contentDigest("new code");
assert.throws(
() => selectMigrationPath(changed, selection, new Map([["split-name", result[0].digest]])),
/different code/,
);
assert.throws(() => selectMigrationPath(value, { ...selection, path: [] }), /does not cover/);
assert.throws(() => selectMigrationPath(value, { ...selection, bindings: {} }), /Missing local/);
});
test("old migration views cannot be writable and contract hashes are verified", () => {
const value = catalog();
value.migrations[0].ports[0].access = ["write"];
assert.throws(() => validateMigrationCatalog(value), /read-only/);
const corrupt = catalog();
corrupt.contracts[from] = {};
assert.throws(() => validateMigrationCatalog(corrupt), /digest mismatch/);
});
test("migration history rejects cycles and non-boolean compatibility promises", () => {
const cyclic = catalog();
cyclic.migrations[0].predecessors = ["split-name"];
assert.throws(() => validateMigrationCatalog(cyclic), /Cyclic/);
const misleading = catalog();
(misleading.migrations[0] as unknown as { preservesOldReaders: string }).preservesOldReaders = "false";
assert.throws(() => validateMigrationCatalog(misleading), /must be booleans/);
});