feat: add module runtime lifecycle
CI / verify (push) Successful in 2m14s
CI / infrastructure-smoke (push) Failing after 1m36s

This commit is contained in:
ekklesia
2026-07-12 22:49:51 -05:00
parent 57552a21a4
commit 7b448d8706
28 changed files with 1382 additions and 203 deletions
@@ -9,7 +9,7 @@ import {
function validManifest(): Record<string, unknown> {
return {
manifestVersion: 1,
manifestVersion: 2,
id: "calendar",
name: "Calendar",
version: "0.1.0",
@@ -17,13 +17,13 @@ function validManifest(): Record<string, unknown> {
platform: "^0.1.0",
moduleSdk: "^0.1.0",
},
capabilities: ["calendar.events"],
capabilities: [{ id: "calendar.events", version: "2.3.0" }],
permissions: ["calendar.events.read"],
storage: {
buckets: [{ id: "assets", description: "Calendar-owned event assets." }],
},
entrypoints: {
server: "./src/server/index.ts",
server: "./dist/server/index.js",
},
};
}
@@ -34,7 +34,7 @@ describe("module manifest contract", () => {
expect(manifest.id).toBe("calendar");
expect(manifest.dependencies).toEqual({
capabilities: { optional: [], required: [] },
capabilities: { optional: {}, required: {} },
modules: { optional: {}, required: {} },
});
expect(manifest.storage.buckets).toEqual([
@@ -49,11 +49,11 @@ describe("module manifest contract", () => {
const second = parseModuleManifest(input);
first.dependencies.modules.required.people = "^0.1.0";
first.dependencies.capabilities.required.push("people.records");
first.dependencies.capabilities.required["people.records"] = "^1.0.0";
first.storage.buckets.push({ id: "exports" });
expect(second.dependencies.modules.required).toEqual({});
expect(second.dependencies.capabilities.required).toEqual([]);
expect(second.dependencies.capabilities.required).toEqual({});
expect(second.storage.buckets).toEqual([]);
});
@@ -67,7 +67,7 @@ describe("module manifest contract", () => {
it("requires module-owned capability and permission namespaces", () => {
const manifest = {
...validManifest(),
capabilities: ["media.items"],
capabilities: [{ id: "media.items", version: "1.0.0" }],
permissions: ["media.items.read"],
};
const result = safeParseModuleManifest(manifest);
@@ -75,7 +75,7 @@ describe("module manifest contract", () => {
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
expect.arrayContaining(["capabilities.0", "permissions.0"]),
expect.arrayContaining(["capabilities.0.id", "permissions.0"]),
);
}
});
@@ -89,11 +89,14 @@ describe("module manifest contract", () => {
optional: { people: "^1.0.0" },
},
capabilities: {
required: ["people.records"],
optional: ["people.records"],
required: { "people.records": "^1.0.0" },
optional: { "people.records": "^1.0.0" },
},
},
capabilities: ["calendar.events", "calendar.events"],
capabilities: [
{ id: "calendar.events", version: "2.3.0" },
{ id: "calendar.events", version: "2.4.0" },
],
storage: { buckets: [{ id: "assets" }, { id: "assets" }] },
};
const result = safeParseModuleManifest(manifest);
@@ -109,13 +112,45 @@ describe("module manifest contract", () => {
...validManifest(),
dependencies: {
modules: { required: { calendar: "^0.1.0" } },
capabilities: { required: ["calendar.events"] },
capabilities: { required: { "calendar.events": "^2.0.0" } },
},
};
expect(safeParseModuleManifest(manifest).success).toBe(false);
});
it("versions capabilities independently from the module release", () => {
const manifest = parseModuleManifest(validManifest());
expect(manifest.version).toBe("0.1.0");
expect(manifest.capabilities).toEqual([{ id: "calendar.events", version: "2.3.0" }]);
});
it("requires exact capability versions and dependency ranges", () => {
const manifest = {
...validManifest(),
capabilities: [{ id: "calendar.events", version: "^2.0.0" }],
dependencies: {
capabilities: { required: { "people.records": "not-a-range" } },
},
};
const result = safeParseModuleManifest(manifest);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.map((issue) => issue.path.join("."))).toEqual(
expect.arrayContaining([
"capabilities.0.version",
"dependencies.capabilities.required.people.records",
]),
);
}
});
it("rejects obsolete manifest versions instead of guessing a migration", () => {
expect(safeParseModuleManifest({ ...validManifest(), manifestVersion: 1 }).success).toBe(false);
});
it.each([
"../outside.ts",
"/absolute.ts",