feat: add module runtime lifecycle
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user