232 lines
10 KiB
TypeScript
232 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { capabilityId as id, valueType, validateWorkspaceRevision } from "../src/capability-model/index.js";
|
|
import {
|
|
contentDigest,
|
|
contentDigestAsync,
|
|
planEvolution,
|
|
reviewSemanticMajors,
|
|
runtimeContracts,
|
|
storageContracts,
|
|
storageChangeRequiresMigration,
|
|
} from "../src/capability-model/evolution.js";
|
|
import {
|
|
capabilityFixtureSource,
|
|
capabilityResourceSources,
|
|
compileCapabilityFixture,
|
|
makeValidCapabilityWorkspace,
|
|
} from "./fixtures/capability-model.js";
|
|
|
|
test("QX carries stable conformance IDs and implementation semantic majors", () => {
|
|
const result = compileCapabilityFixture({
|
|
workspace: capabilityFixtureSource.replace(
|
|
"conform Project as Named {",
|
|
'conform Project as Named id "conformance:project:named" semantic-major 3 {',
|
|
),
|
|
todo: capabilityResourceSources.todo.replace(
|
|
'revision "package:todo-runtime@1" {',
|
|
'revision "package:todo-runtime@1" semantic-major 2 {',
|
|
),
|
|
});
|
|
assert.ok(result.ok, JSON.stringify(result));
|
|
assert.equal(result.workspace.conformances[0]!.id, "conformance:project:named");
|
|
assert.equal(result.workspace.conformances[0]!.semanticMajor, 3);
|
|
assert.equal(result.workspace.packageImports[0]!.semanticMajor, 2);
|
|
});
|
|
|
|
test("invalid majors and duplicate owner identities are rejected", () => {
|
|
const workspace = makeValidCapabilityWorkspace();
|
|
workspace.packageImports[0]!.semanticMajor = 0;
|
|
workspace.conformances[0]!.semanticMajor = 1.5;
|
|
workspace.conformances[0]!.id = id.conformance("same");
|
|
workspace.conformances[1]!.id = id.conformance("same");
|
|
const issues = validateWorkspaceRevision(workspace);
|
|
assert.equal(issues.filter((entry) => entry.code === "invalid-semantic-major").length, 2);
|
|
assert.ok(issues.some((entry) => entry.code === "duplicate-conformance-id"));
|
|
});
|
|
|
|
test("hashing is canonical and rejects values outside the persisted JSON contract", () => {
|
|
assert.equal(contentDigest({ z: 1, a: { y: 3, b: 2 } }), contentDigest({ a: { b: 2, y: 3 }, z: 1 }));
|
|
assert.notEqual(contentDigest([1, 2]), contentDigest([2, 1]));
|
|
assert.throws(() => contentDigest(NaN));
|
|
assert.throws(() => contentDigest(new Date()));
|
|
});
|
|
|
|
test("cooperative hashing preserves canonical identities and services the event loop", async () => {
|
|
const sparse = [1, 2, 3];
|
|
delete sparse[1];
|
|
const values: unknown[] = [
|
|
null,
|
|
false,
|
|
-0,
|
|
"中\\n\ud800",
|
|
sparse,
|
|
{ z: 1, a: { omitted: undefined, b: [false, 1e100] } },
|
|
];
|
|
const shared = { text: "中😀".repeat(22000) };
|
|
values.push({ first: shared, second: shared });
|
|
for (const value of values) assert.equal(await contentDigestAsync(value), contentDigest(value));
|
|
for (const value of [undefined, NaN, Infinity, new Date(), [undefined]])
|
|
await assert.rejects(contentDigestAsync(value));
|
|
const cyclic: { next?: unknown } = {};
|
|
cyclic.next = cyclic;
|
|
await assert.rejects(contentDigestAsync(cyclic), /cyclic JSON/);
|
|
const large = Array.from({ length: 50000 }, (_, index) => ({ z: "definition".repeat(8), a: index }));
|
|
const expected = contentDigest(large);
|
|
let serviced = false;
|
|
const timer = setTimeout(() => {
|
|
serviced = true;
|
|
}, 0);
|
|
try {
|
|
assert.equal(await contentDigestAsync(large), expected);
|
|
assert.equal(serviced, true, "large verification must let host renewals and RPCs run");
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
});
|
|
|
|
test("a workspace root change and display names do not restart package runtimes", () => {
|
|
const before = makeValidCapabilityWorkspace();
|
|
const after = structuredClone(before);
|
|
after.id = id.workspaceRevision("next");
|
|
after.sourceRootCommit = "f".repeat(40);
|
|
after.atoms[0]!.displayName = "Renamed";
|
|
after.sharedAttachments[0]!.displayName = "Renamed storage";
|
|
after.conformances.reverse();
|
|
after.interfaceImports.reverse();
|
|
const report = planEvolution(before, after, { allowLegacy: true });
|
|
assert.deepEqual(
|
|
report.runtimeActions.map((entry) => entry.action),
|
|
["keep"],
|
|
);
|
|
assert.deepEqual(report.storageChanges, []);
|
|
assert.deepEqual(report.packageChecks, []);
|
|
});
|
|
|
|
test("consumer changes preserve unrelated resource owners", () => {
|
|
const before = makeValidCapabilityWorkspace();
|
|
before.packageImports.push({
|
|
packageId: id.package("package:resource-owner"),
|
|
revisionId: id.packageRevision("package:resource-owner@1"),
|
|
displayName: "ResourceOwner",
|
|
source: { repository: "https://example.org/owner.git", commit: "a".repeat(40) },
|
|
exports: [
|
|
{
|
|
kind: "function",
|
|
id: id.packageExport("export:owner:ping"),
|
|
displayName: "ping",
|
|
inputType: valueType.unit,
|
|
outputType: valueType.unit,
|
|
dependencyPorts: [],
|
|
},
|
|
],
|
|
});
|
|
const after = structuredClone(before);
|
|
after.packageImports[0]!.source.commit = "e".repeat(40);
|
|
const actions = planEvolution(before, after, { allowLegacy: true }).runtimeActions;
|
|
assert.equal(actions.find((entry) => entry.groupId === "package:resource-owner")!.action, "keep");
|
|
assert.equal(actions.find((entry) => entry.groupId === "package:todo-runtime")!.action, "replace");
|
|
});
|
|
|
|
test("storage defaults and ownership changes invalidate consumers without requiring a major", () => {
|
|
const before = makeValidCapabilityWorkspace();
|
|
const after = structuredClone(before);
|
|
const slot = after.sharedAttachments[0]!;
|
|
assert.equal(slot.kind, "state");
|
|
if (slot.kind === "state") slot.defaultValue = "Different default";
|
|
const report = planEvolution(before, after, { allowLegacy: true });
|
|
assert.equal(report.storageChanges.length, 1);
|
|
assert.deepEqual(report.migrationRequired, []);
|
|
assert.equal(report.runtimeActions[0]!.action, "replace");
|
|
assert.deepEqual(report.reviews, []);
|
|
assert.notDeepEqual(storageContracts(before), storageContracts(after));
|
|
});
|
|
|
|
test("semantic review receipts cover exact consumer/provider contracts", () => {
|
|
const before = makeValidCapabilityWorkspace();
|
|
before.conformances[0]!.id = id.conformance("conformance:project:named");
|
|
const after = structuredClone(before);
|
|
after.conformances[0]!.semanticMajor = 2;
|
|
const report = planEvolution(before, after, { allowLegacy: true });
|
|
assert.equal(report.reviews.length, 1);
|
|
assert.equal(report.reviews[0]!.accepted, false);
|
|
const receipt = {
|
|
requirementDigest: report.reviews[0]!.requirementDigest,
|
|
decision: "accepted-unchanged" as const,
|
|
rationale: "Reviewed the semantic change against summary behavior",
|
|
agentId: "workspace-agent",
|
|
};
|
|
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, true);
|
|
after.packageImports[0]!.source.commit = "f".repeat(40);
|
|
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, false);
|
|
});
|
|
|
|
test("execution semantic review is independent of storage migration and process action planning", () => {
|
|
const before = makeValidCapabilityWorkspace();
|
|
before.conformances[0]!.id = id.conformance("conformance:project:named");
|
|
const after = structuredClone(before);
|
|
after.conformances[0]!.semanticMajor = 2;
|
|
const report = reviewSemanticMajors(before, after);
|
|
assert.equal(report.checkerVersion, "quixos-semantic-review-v1");
|
|
assert.equal(report.reviews.length, 1);
|
|
assert.equal(report.reviews[0]!.accepted, false);
|
|
assert.equal("migrationRequired" in report, false);
|
|
assert.equal("runtimeActions" in report, false);
|
|
assert.equal(report.blockers.length, 1);
|
|
const receipt = {
|
|
requirementDigest: report.reviews[0]!.requirementDigest,
|
|
decision: "accepted-unchanged" as const,
|
|
rationale: "The consumer only displays this provider's unchanged label contract",
|
|
agentId: "fixture-agent",
|
|
};
|
|
assert.deepEqual(reviewSemanticMajors(before, after, [receipt]).blockers, []);
|
|
const oldReceipt = {
|
|
...receipt,
|
|
requirementDigest: planEvolution(before, after, { allowLegacy: true }).reviews[0]!.requirementDigest,
|
|
};
|
|
assert.equal(reviewSemanticMajors(before, after, [oldReceipt]).reviews[0]!.accepted, false);
|
|
after.packageImports[0]!.source.commit = "f".repeat(40);
|
|
assert.equal(reviewSemanticMajors(before, after, [receipt]).reviews[0]!.accepted, false);
|
|
assert.deepEqual(reviewSemanticMajors(null, after).blockers, []);
|
|
assert.throws(
|
|
() => reviewSemanticMajors(before, { ...after, workspaceId: id.workspace("different") }),
|
|
/different workspace/,
|
|
);
|
|
const earlier = structuredClone(after);
|
|
earlier.conformances[0]!.semanticMajor = 1;
|
|
assert.ok(reviewSemanticMajors(after, earlier).blockers.some((message) => message.includes("decreases")));
|
|
});
|
|
|
|
test("evolution enrollment is explicit and never erases legacy ownership", () => {
|
|
const workspace = makeValidCapabilityWorkspace();
|
|
const report = planEvolution(workspace, workspace);
|
|
assert.ok(report.blockers.some((entry) => entry.includes("workspace-shared")));
|
|
assert.ok(report.blockers.some((entry) => entry.includes("authored ID")));
|
|
assert.equal(runtimeContracts(workspace).length, 1);
|
|
assert.throws(
|
|
() => planEvolution(workspace, { ...workspace, workspaceId: id.workspace("other") }),
|
|
/different workspace/,
|
|
);
|
|
});
|
|
|
|
test("automatic preservation distinguishes additions and defaults from incompatible storage", () => {
|
|
const workspace = makeValidCapabilityWorkspace();
|
|
const before = storageContracts(workspace).find((entry) => entry.kind === "state")!;
|
|
const next = structuredClone(before);
|
|
const value = next.definition as Record<string, unknown>;
|
|
value.defaultValue = { displayName: "important user data" };
|
|
assert.equal(storageChangeRequiresMigration(before, next, new Set()), false);
|
|
next.ownerId = "another-owner";
|
|
assert.equal(storageChangeRequiresMigration(before, next, new Set()), true);
|
|
assert.equal(storageChangeRequiresMigration(before, undefined, new Set()), true);
|
|
delete value.defaultValue;
|
|
assert.equal(storageChangeRequiresMigration(undefined, next, new Set([value.attachedTo as string])), true);
|
|
assert.equal(storageChangeRequiresMigration(undefined, next, new Set()), false);
|
|
const slot = workspace.sharedAttachments.find((entry) => entry.kind === "state")!;
|
|
if (slot.kind !== "state") throw new Error("fixture slot");
|
|
slot.defaultValue = { displayName: "one" };
|
|
const first = storageContracts(workspace);
|
|
slot.defaultValue = { displayName: "two" };
|
|
assert.notDeepEqual(storageContracts(workspace), first, "user data must not be stripped as schema metadata");
|
|
});
|