import { describe, expect, it } from "vitest"; import { isVersionCompatible, isModuleConfigurationBlockedReason, MODULE_BLOCKED_REASONS, MODULE_CONFIGURATION_BLOCKED_REASONS, MODULE_DEPENDENCY_KINDS, MODULE_MANIFEST_VERSION, MODULE_RUNTIME_STATES, packageRelativePathSchema, parseModuleManifest, parsePermissionId, safeParseModuleManifest, } from "../src/index.js"; function validManifest(): Record { return { manifestVersion: 5, 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: [ { id: "calendar.events.read", baseline: "restricted", description: "Read calendar events.", }, ], 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; delete input.permissions; 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" }); first.permissions.push({ id: parsePermissionId("calendar.events.write"), baseline: "restricted", description: "Write calendar events.", }); expect(second.dependencies.modules.required).toEqual({}); expect(second.dependencies.capabilities.required).toEqual({}); expect(second.storage.buckets).toEqual([]); expect(second.permissions).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: [ { id: "media.items.read", baseline: "restricted", description: "Read media items." }, ], }; 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.id"]), ); } }); 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 a duplicate permission id at its exact issue path", () => { const manifest = { ...validManifest(), permissions: [ { id: "calendar.events.read", baseline: "restricted", description: "Read calendar events.", }, { id: "calendar.events.read", baseline: "authenticated", description: "Read calendar events, again.", }, ], }; const result = safeParseModuleManifest(manifest); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues.map((issue) => issue.path.join("."))).toContain( "permissions.1.id", ); } }); 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("rejects a version 2 manifest rather than silently interpreting it as a later version", () => { // manifestVersion 2's own permissions shape (a bare string array) is also what every later // version must reject — this manifest is invalid twice over, deliberately, since a real // version 2 manifest is exactly this shape. const version2Manifest = { ...validManifest(), manifestVersion: 2, permissions: ["calendar.events.read"], }; expect(safeParseModuleManifest(version2Manifest).success).toBe(false); }); it("rejects a version 3 manifest rather than silently interpreting it as a later version", () => { const version3Manifest = { ...validManifest(), manifestVersion: 3 }; expect(safeParseModuleManifest(version3Manifest).success).toBe(false); }); it("rejects a version 4 manifest rather than silently interpreting it as version 5", () => { // A manifest declaring no configuration is otherwise byte-identical between version 4 and // version 5: only the version literal separates them, and it is not reinterpreted (ADR 0029). const version4Manifest = { ...validManifest(), manifestVersion: 4 }; expect(safeParseModuleManifest(version4Manifest).success).toBe(false); }); it("accepts exactly one manifest version, so no document can be read as two versions", () => { expect(MODULE_MANIFEST_VERSION).toBe(5); for (const manifestVersion of [1, 2, 3, 4, 6, "5", 5.5, null]) { expect(safeParseModuleManifest({ ...validManifest(), manifestVersion }).success).toBe(false); } expect( safeParseModuleManifest({ ...validManifest(), manifestVersion: MODULE_MANIFEST_VERSION }) .success, ).toBe(true); }); it("rejects a bare permission-id string array instead of complete declarations", () => { const manifest = { ...validManifest(), permissions: ["calendar.events.read"] }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("defaults to an empty permission list when omitted", () => { const input = validManifest(); delete input.permissions; const manifest = parseModuleManifest(input); expect(manifest.permissions).toEqual([]); }); it.each([ "public", "authenticated", "restricted", ] as const)("accepts a %s permission declaration and preserves its exact baseline", (baseline) => { const manifest = parseModuleManifest({ ...validManifest(), permissions: [{ id: "calendar.events.read", baseline, description: "Read events." }], }); expect(manifest.permissions).toEqual([ { id: "calendar.events.read", baseline, description: "Read events." }, ]); }); it("rejects a permission declaration with a missing baseline", () => { const manifest = { ...validManifest(), permissions: [{ id: "calendar.events.read", description: "Read events." }], }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("rejects a permission declaration with an invalid baseline", () => { const manifest = { ...validManifest(), permissions: [ { id: "calendar.events.read", baseline: "superuser", description: "Read events." }, ], }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("rejects a permission declaration with a missing or blank description", () => { const missingDescription = { ...validManifest(), permissions: [{ id: "calendar.events.read", baseline: "restricted" }], }; const blankDescription = { ...validManifest(), permissions: [{ id: "calendar.events.read", baseline: "restricted", description: " " }], }; expect(safeParseModuleManifest(missingDescription).success).toBe(false); expect(safeParseModuleManifest(blankDescription).success).toBe(false); }); it("rejects a permission declaration with an oversized description", () => { const manifest = { ...validManifest(), permissions: [ { id: "calendar.events.read", baseline: "restricted", description: "x".repeat(241) }, ], }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("rejects a permission declaration with an unknown property", () => { const manifest = { ...validManifest(), permissions: [ { id: "calendar.events.read", baseline: "restricted", description: "Read events.", scope: "calendar-event:123", }, ], }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("rejects a malformed permission id", () => { const manifest = { ...validManifest(), permissions: [ { id: "Calendar.Events.Read", baseline: "restricted", description: "Read events." }, ], }; expect(safeParseModuleManifest(manifest).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 exactly the operational lifecycle vocabulary the platform implements", () => { // Every name here is produced by real code and observable through the lifecycle read model // (ADR 0028). The nine-stage `MODULE_LIFECYCLE_STAGES` vocabulary this replaced named states // nothing ever entered, including a `failed` state that startup atomicity makes unobservable — // asserting the exact sets is what keeps this contract from drifting back into aspiration. expect(MODULE_RUNTIME_STATES).toEqual(["running", "disabled", "blocked"]); expect(MODULE_RUNTIME_STATES).not.toContain("failed"); expect(MODULE_BLOCKED_REASONS).toEqual([ "required_dependency_disabled", "required_dependency_blocked", "configuration_missing", "configuration_invalid", "secret_missing", ]); // The three configuration reasons are a subset, and each corresponds to a real implemented // check with a distinct operator fix (ADR 0029) — the only justification for a separate code. expect(MODULE_CONFIGURATION_BLOCKED_REASONS).toEqual([ "configuration_missing", "configuration_invalid", "secret_missing", ]); for (const reason of MODULE_CONFIGURATION_BLOCKED_REASONS) { expect(isModuleConfigurationBlockedReason(reason)).toBe(true); } expect(isModuleConfigurationBlockedReason("required_dependency_disabled")).toBe(false); expect(MODULE_DEPENDENCY_KINDS).toEqual(["module", "capability"]); }); describe("module-owned UI entrypoint (ADR 0021)", () => { it("accepts a backend-only module that declares no UI at all", () => { const manifest = parseModuleManifest(validManifest()); expect(manifest.entrypoints.ui).toBeUndefined(); expect(manifest.entrypoints.server).toBe("./dist/server/index.js"); }); it("accepts a declared compiled UI entrypoint alongside the server entrypoint", () => { const manifest = parseModuleManifest({ ...validManifest(), entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" }, }); expect(manifest.entrypoints.ui).toBe("./dist/ui/index.js"); // Declaring UI never alters the backend contract: a product module is one module, not two. expect(manifest.entrypoints.server).toBe("./dist/server/index.js"); }); it.each([ ["../outside/ui.js", "escapes the package boundary"], ["/absolute/ui.js", "is an absolute path"], ["./dist/../../ui.js", "traverses out through a parent segment"], ["file:///dist/ui/index.js", "is a URL rather than a package-relative path"], ["./dist\\ui\\index.js", "uses backslash separators"], ["./dist/ui/index.js?raw", "carries a query string"], ["", "is empty"], ])("rejects a UI entrypoint that %s", (ui) => { const manifest = { ...validManifest(), entrypoints: { server: "./dist/server/index.js", ui }, }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it.each([ 42, null, true, ["./dist/ui/index.js"], { entry: "./dist/ui/index.js" }, ])("rejects a non-string UI entrypoint %s", (ui) => { const manifest = { ...validManifest(), entrypoints: { server: "./dist/server/index.js", ui }, }; expect(safeParseModuleManifest(manifest).success).toBe(false); }); it("still rejects unknown entrypoint keys, so UI is the only new one", () => { for (const unknownEntrypoint of ["dashboard", "surface", "client", "web", "renderers"]) { const manifest = { ...validManifest(), entrypoints: { server: "./dist/server/index.js", [unknownEntrypoint]: "./dist/ui/index.js", }, }; expect(safeParseModuleManifest(manifest).success).toBe(false); } }); it("declares no grouping, folder, parent, or menu field for a module to own", () => { // Dashboard organization is presentation configuration, never module architecture // (ADR 0021). The manifest must offer no place to encode a hierarchy. for (const presentationField of [ "group", "folder", "parent", "menu", "navigation", "icon", "order", ]) { const manifest = { ...validManifest(), [presentationField]: "media" }; expect(safeParseModuleManifest(manifest).success).toBe(false); } }); it("keeps the module dependency graph flat: UI adds no parent/child relationship", () => { const manifest = parseModuleManifest({ ...validManifest(), entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" }, }); // A module that ships UI still expresses relationships only through genuine module and // capability dependencies — the same two mechanisms a backend-only module uses. expect(Object.keys(manifest.dependencies.modules.required)).toEqual([]); expect(Object.keys(manifest.dependencies.modules.optional)).toEqual([]); expect(Object.keys(manifest)).not.toContain("children"); }); it("still supports a genuine module dependency for a module that ships UI", () => { const manifest = parseModuleManifest({ ...validManifest(), dependencies: { modules: { required: { people: "^0.1.0" } }, capabilities: { required: { "people.records": "^2.0.0" } }, }, entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" }, }); expect(manifest.dependencies.modules.required).toEqual({ people: "^0.1.0" }); expect(manifest.dependencies.capabilities.required).toEqual({ "people.records": "^2.0.0" }); }); }); });