import { describe, expect, it } from "vitest"; import { isVersionCompatible, MODULE_LIFECYCLE_STAGES, packageRelativePathSchema, parseModuleManifest, safeParseModuleManifest, } from "../src/index.js"; function validManifest(): Record { return { manifestVersion: 2, id: "calendar", name: "Calendar", version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0", }, capabilities: [{ id: "calendar.events", version: "2.3.0" }], permissions: ["calendar.events.read"], storage: { buckets: [{ id: "assets", description: "Calendar-owned event assets." }], }, entrypoints: { server: "./dist/server/index.js", }, }; } describe("module manifest contract", () => { it("normalizes a valid manifest with safe defaults", () => { const manifest = parseModuleManifest(validManifest()); expect(manifest.id).toBe("calendar"); expect(manifest.dependencies).toEqual({ capabilities: { optional: {}, required: {} }, modules: { optional: {}, required: {} }, }); expect(manifest.storage.buckets).toEqual([ { id: "assets", description: "Calendar-owned event assets." }, ]); }); it("creates isolated default collections for every parsed manifest", () => { const input = validManifest(); delete input.storage; const first = parseModuleManifest(input); const second = parseModuleManifest(input); first.dependencies.modules.required.people = "^0.1.0"; 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.storage.buckets).toEqual([]); }); it("rejects unknown and presentation-oriented manifest properties", () => { const manifest = { ...validManifest(), renderers: ["calendar-month"] }; const result = safeParseModuleManifest(manifest); expect(result.success).toBe(false); }); it("requires module-owned capability and permission namespaces", () => { const manifest = { ...validManifest(), capabilities: [{ id: "media.items", version: "1.0.0" }], permissions: ["media.items.read"], }; 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.id", "permissions.0"]), ); } }); it("rejects duplicate declarations and dependency overlap", () => { const manifest = { ...validManifest(), dependencies: { modules: { required: { people: "^1.0.0" }, optional: { people: "^1.0.0" }, }, capabilities: { required: { "people.records": "^1.0.0" }, optional: { "people.records": "^1.0.0" }, }, }, 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); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues).toHaveLength(4); } }); it("rejects module and capability self-dependencies", () => { const manifest = { ...validManifest(), dependencies: { modules: { required: { calendar: "^0.1.0" } }, 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", "file:///server.ts", "./src/../server.ts", "./src\\server.ts", "./src/server.ts?raw", ])("rejects unsafe package entrypoint %s", (entrypoint) => { expect(packageRelativePathSchema.safeParse(entrypoint).success).toBe(false); }); it("allows package directories whose names merely begin with two dots", () => { expect(packageRelativePathSchema.safeParse("./..data/server.ts").success).toBe(true); }); it("uses full semantic-version compatibility ranges", () => { expect(isVersionCompatible("0.1.5", "^0.1.0")).toBe(true); expect(isVersionCompatible("0.2.0", "^0.1.0")).toBe(false); expect(isVersionCompatible("not-a-version", "*")).toBe(false); }); it("publishes the complete lifecycle vocabulary", () => { expect(MODULE_LIFECYCLE_STAGES).toContain("discovered"); expect(MODULE_LIFECYCLE_STAGES).toContain("failed"); }); });