import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { discoverModules, ModuleDiscoveryError, type ModuleDiscoveryIssueCode, } from "../src/modules/index.js"; interface ModuleFixtureOptions { directory?: string; manifest?: Record; packageJson?: Record; serverSource?: string; /** Extra package-relative files to write before discovery runs, so a fixture can publish a * compiled UI artifact (or a deliberately uncompiled one) alongside its server entrypoint. */ extraFiles?: Record; } const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), ); }); async function createModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-modules-")); temporaryDirectories.push(directory); return directory; } function manifestFor(id: string): Record { return { manifestVersion: 7, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0", }, capabilities: [{ id: `${id}.records`, version: "1.0.0" }], permissions: [ { id: `${id}.records.read`, baseline: "restricted", description: "Read records." }, ], entrypoints: { server: "./dist/server/index.js", }, }; } async function createModule( modulesDirectory: string, id: string, options: ModuleFixtureOptions = {}, ): Promise { const directoryName = options.directory ?? id; const moduleDirectory = path.join(modulesDirectory, directoryName); const manifest = options.manifest ?? manifestFor(id); const packageJson = options.packageJson ?? { name: `@test/module-${id}`, version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }; await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await Promise.all([ writeFile(path.join(moduleDirectory, "package.json"), JSON.stringify(packageJson)), writeFile(path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify(manifest)), writeFile( path.join(moduleDirectory, "dist/server/index.js"), options.serverSource ?? `export default { id: ${JSON.stringify(id)}, version: "0.1.0" };`, ), ]); for (const [relativePath, contents] of Object.entries(options.extraFiles ?? {})) { const target = path.join(moduleDirectory, relativePath); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, contents); } } async function discoveryError( modulesDirectory: string, versions: { moduleSdkVersion?: string; platformVersion?: string } = {}, ): Promise { try { await discoverModules({ moduleSdkVersion: versions.moduleSdkVersion ?? "0.1.0", modulesDirectory, platformVersion: versions.platformVersion ?? "0.1.0", }); } catch (error) { if (error instanceof ModuleDiscoveryError) { return error; } throw error; } throw new Error("Expected module discovery to fail."); } function issueCodes(error: ModuleDiscoveryError): ModuleDiscoveryIssueCode[] { return error.issues.map((issue) => issue.code); } describe("module discovery", () => { it("treats a missing or empty modules directory as zero installed modules", async () => { const parent = await createModulesDirectory(); const missing = path.join(parent, "missing"); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory: missing, platformVersion: "0.1.0", }), ).resolves.toEqual([]); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory: parent, platformVersion: "0.1.0", }), ).resolves.toEqual([]); }); it("discovers static manifests and orders dependencies deterministically", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "calendar", { manifest: { ...manifestFor("calendar"), dependencies: { modules: { required: { people: "^0.1.0" } }, capabilities: { required: { "people.records": "^2.0.0" } }, }, storage: { buckets: [{ id: "assets" }, { id: "exports" }] }, }, serverSource: 'throw new Error("discovery must never execute server code");', }); await createModule(modulesDirectory, "people", { manifest: { ...manifestFor("people"), capabilities: [{ id: "people.records", version: "2.1.0" }], }, }); const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(modules.map((module) => module.manifest.id)).toEqual(["people", "calendar"]); expect(modules[1]?.manifest.storage.buckets.map((bucket) => bucket.id)).toEqual([ "assets", "exports", ]); expect(Object.isFrozen(modules[1]?.manifest)).toBe(true); const mutableBuckets = modules[1]?.manifest.storage.buckets as unknown as Array<{ id: string }>; expect(() => mutableBuckets.push({ id: "tampered" })).toThrow(TypeError); expect(modules[1]?.manifest.permissions).toEqual([ { id: "calendar.records.read", baseline: "restricted", description: "Read records." }, ]); expect(Object.isFrozen(modules[1]?.manifest.permissions)).toBe(true); expect(Object.isFrozen(modules[1]?.manifest.permissions[0])).toBe(true); const mutablePermissions = modules[1]?.manifest.permissions as unknown as Array<{ id: string; }>; expect(() => mutablePermissions.push({ id: "tampered" })).toThrow(TypeError); }); it("aggregates malformed packages and manifests without partial discovery", async () => { const modulesDirectory = await createModulesDirectory(); await mkdir(path.join(modulesDirectory, "missing")); await createModule(modulesDirectory, "invalid", { manifest: { ...manifestFor("invalid"), renderers: ["not-allowed"] }, packageJson: { name: "@test/module-invalid", version: "not-semver", type: "commonjs", }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toEqual( expect.arrayContaining([ "MODULE_MANIFEST_INVALID", "MODULE_MANIFEST_MISSING", "MODULE_PACKAGE_INVALID", "MODULE_PACKAGE_MISSING", ]), ); }); it("aggregates malformed JSON without exposing machine-specific paths", async () => { const modulesDirectory = await createModulesDirectory(); const moduleDirectory = path.join(modulesDirectory, "broken-json"); await mkdir(moduleDirectory); await Promise.all([ writeFile(path.join(moduleDirectory, "package.json"), "{"), writeFile(path.join(moduleDirectory, "ekklesia.module.json"), "["), ]); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toEqual( expect.arrayContaining(["MODULE_MANIFEST_INVALID", "MODULE_PACKAGE_INVALID"]), ); expect(error.issues.every((issue) => !issue.message.includes(modulesDirectory))).toBe(true); }); it("requires package and manifest versions to match", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "mismatch", { packageJson: { name: "@test/module-mismatch", version: "0.2.0", type: "module", ekklesia: { layer: "module" }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_VERSION_MISMATCH"); }); it("rejects duplicate module IDs and package names", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "one", { manifest: { ...manifestFor("one"), id: "shared", capabilities: [{ id: "shared.records", version: "1.0.0" }], permissions: [ { id: "shared.records.read", baseline: "restricted", description: "Read records." }, ], }, packageJson: { name: "@test/duplicate", version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }, }); await createModule(modulesDirectory, "two", { manifest: { ...manifestFor("two"), id: "shared", capabilities: [{ id: "shared.records", version: "1.0.0" }], permissions: [ { id: "shared.records.read", baseline: "restricted", description: "Read records." }, ], }, packageJson: { name: "@test/duplicate", version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toEqual( expect.arrayContaining(["MODULE_ID_DUPLICATE", "MODULE_PACKAGE_NAME_DUPLICATE"]), ); }); it("enforces platform and Module SDK compatibility", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "future", { manifest: { ...manifestFor("future"), compatibility: { platform: ">=1.0.0", moduleSdk: ">=2.0.0" }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toEqual( expect.arrayContaining(["MODULE_PLATFORM_INCOMPATIBLE", "MODULE_SDK_INCOMPATIBLE"]), ); }); it("allows missing optional dependencies but rejects missing required dependencies", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "optional-consumer", { manifest: { ...manifestFor("optional-consumer"), dependencies: { modules: { optional: { absent: "^1.0.0" } } }, }, }); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).resolves.toHaveLength(1); await createModule(modulesDirectory, "required-consumer", { manifest: { ...manifestFor("required-consumer"), dependencies: { modules: { required: { absent: "^1.0.0" } } }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_DEPENDENCY_MISSING"); }); it("rejects installed dependencies with incompatible versions", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "people"); await createModule(modulesDirectory, "calendar", { manifest: { ...manifestFor("calendar"), dependencies: { modules: { optional: { people: "^2.0.0" } } }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_DEPENDENCY_INCOMPATIBLE"); }); it("allows absent optional capabilities but rejects missing required capabilities", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "optional-consumer", { manifest: { ...manifestFor("optional-consumer"), dependencies: { capabilities: { optional: { "people.records": "^1.0.0" } } }, }, }); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).resolves.toHaveLength(1); await createModule(modulesDirectory, "consumer", { manifest: { ...manifestFor("consumer"), dependencies: { capabilities: { required: { "people.records": "^1.0.0" } } }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_CAPABILITY_MISSING"); }); it("rejects incompatible installed required and optional capabilities", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "people"); await createModule(modulesDirectory, "required-consumer", { manifest: { ...manifestFor("required-consumer"), dependencies: { capabilities: { required: { "people.records": "^2.0.0" } } }, }, }); await createModule(modulesDirectory, "optional-consumer", { manifest: { ...manifestFor("optional-consumer"), dependencies: { capabilities: { optional: { "people.records": "^3.0.0" } } }, }, }); const error = await discoveryError(modulesDirectory); const incompatibilities = error.issues.filter( (issue) => issue.code === "MODULE_CAPABILITY_INCOMPATIBLE", ); expect(incompatibilities).toHaveLength(2); expect(incompatibilities).toEqual( expect.arrayContaining([ expect.objectContaining({ message: "Capability people.records 1.0.0 does not satisfy ^2.0.0.", path: "dependencies.capabilities.required.people.records", }), expect.objectContaining({ message: "Capability people.records 1.0.0 does not satisfy ^3.0.0.", path: "dependencies.capabilities.optional.people.records", }), ]), ); }); it("detects dependency cycles", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "one", { manifest: { ...manifestFor("one"), dependencies: { modules: { required: { two: "^0.1.0" } } }, }, }); await createModule(modulesDirectory, "two", { manifest: { ...manifestFor("two"), dependencies: { modules: { required: { one: "^0.1.0" } } }, }, }); await createModule(modulesDirectory, "three", { manifest: { ...manifestFor("three"), dependencies: { modules: { required: { two: "^0.1.0" } } }, }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_DEPENDENCY_CYCLE"); expect(error.issues.find((issue) => issue.code === "MODULE_DEPENDENCY_CYCLE")?.message).toBe( "Module dependency cycle contains: one, two.", ); }); it("rejects missing entrypoint targets and invalid caller versions", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "missing-server", { manifest: { ...manifestFor("missing-server"), entrypoints: { server: "./dist/server/missing.js", schema: "./dist/schema/missing.js", }, }, }); let error = await discoveryError(modulesDirectory); expect(error.issues.filter((issue) => issue.code === "MODULE_ENTRYPOINT_MISSING")).toHaveLength( 2, ); error = await discoveryError(modulesDirectory, { platformVersion: "invalid" }); expect(issueCodes(error)).toContain("DISCOVERY_CONFIGURATION_INVALID"); }); it("rejects source TypeScript server entrypoints", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "source-server", { manifest: { ...manifestFor("source-server"), entrypoints: { server: "./src/server/index.ts" }, }, }); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", message: "server must reference compiled ESM JavaScript with a .js or .mjs extension.", path: "entrypoints.server", }), ); }); it.each([ "./src/schema/index.ts", "./dist/schema/index.cjs", ])("rejects non-ESM schema entrypoint %s", async (schemaEntrypoint) => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "schema-source", { manifest: { ...manifestFor("schema-source"), entrypoints: { server: "./dist/server/index.js", schema: schemaEntrypoint, }, }, }); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", message: "schema must reference compiled ESM JavaScript with a .js or .mjs extension.", path: "entrypoints.schema", }), ); }); it("accepts an optional compiled ESM schema entrypoint", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "compiled-schema", { manifest: { ...manifestFor("compiled-schema"), entrypoints: { server: "./dist/server/index.js", schema: "./dist/schema/index.mjs", }, }, }); const schemaDirectory = path.join(modulesDirectory, "compiled-schema", "dist/schema"); await mkdir(schemaDirectory, { recursive: true }); await writeFile(path.join(schemaDirectory, "index.mjs"), "export default {};\n"); const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(modules[0]?.entrypoints.schema).toBe(path.join(schemaDirectory, "index.mjs")); }); it("requires executable entrypoints to be files and migrations to be a directory", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "wrong-entrypoint-kind", { manifest: { ...manifestFor("wrong-entrypoint-kind"), entrypoints: { server: "./dist/server.js", schema: "./dist/schema.js", migrations: "./dist/server/index.js", }, }, }); await Promise.all([ mkdir(path.join(modulesDirectory, "wrong-entrypoint-kind", "dist/server.js")), mkdir(path.join(modulesDirectory, "wrong-entrypoint-kind", "dist/schema.js")), ]); const error = await discoveryError(modulesDirectory); expect(error.issues.filter((issue) => issue.code === "MODULE_ENTRYPOINT_INVALID")).toHaveLength( 3, ); }); it("rejects schema entrypoint links that escape the module directory", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "linked-schema", { manifest: { ...manifestFor("linked-schema"), entrypoints: { server: "./dist/server/index.js", schema: "./dist/schema/index.js", }, }, }); const schemaDirectory = path.join(modulesDirectory, "linked-schema", "dist/schema"); const externalSchema = path.join(modulesDirectory, "external-schema.js"); await mkdir(schemaDirectory, { recursive: true }); await writeFile(externalSchema, "export default {};\n"); await symlink("../../../external-schema.js", path.join(schemaDirectory, "index.js")); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", path: "entrypoints.schema", }), ); }); it("rejects package metadata links that escape the module directory", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "linked"); const externalManifest = path.join(modulesDirectory, "external-manifest.json"); const linkedManifest = path.join(modulesDirectory, "linked", "ekklesia.module.json"); await writeFile(externalManifest, JSON.stringify(manifestFor("linked"))); await rm(linkedManifest); await symlink("../external-manifest.json", linkedManifest); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_MANIFEST_INVALID"); }); it("rejects symbolic links masquerading as installed module directories", async () => { const modulesDirectory = await createModulesDirectory(); const externalModules = await createModulesDirectory(); await createModule(externalModules, "outside"); await symlink( path.join(externalModules, "outside"), path.join(modulesDirectory, "linked-module"), "dir", ); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_DIRECTORY_INVALID"); }); it("produces stable diagnostics independent of the absolute modules path", async () => { const firstModulesDirectory = await createModulesDirectory(); const secondModulesDirectory = await createModulesDirectory(); await mkdir(path.join(firstModulesDirectory, "broken")); await mkdir(path.join(secondModulesDirectory, "broken")); const [firstError, secondError] = await Promise.all([ discoveryError(firstModulesDirectory), discoveryError(secondModulesDirectory), ]); expect(firstError.issues).toEqual(secondError.issues); }); describe("compiled jobs entrypoint", () => { function manifestWithJobs(id: string, jobs: string): Record { const manifest = manifestFor(id); manifest.entrypoints = { server: "./dist/server/index.js", jobs }; return manifest; } it.each([ ["./dist/jobs/index.js", "index.js"], ["./dist/jobs/index.mjs", "index.mjs"], ])("accepts the compiled jobs artifact %s", async (jobs, filename) => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "jobs-compiled", { manifest: manifestWithJobs("jobs-compiled", jobs), extraFiles: { [`dist/jobs/${filename}`]: "export default {};\n" }, }); const [discovered] = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered?.entrypoints.jobs).toBe( path.join(discovered?.directory ?? "", "dist/jobs", filename), ); }); it.each([ "./src/jobs/index.ts", "./src/jobs/index.tsx", "./dist/jobs/index.cjs", ])("rejects the uncompiled jobs entrypoint %s", async (jobs) => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "jobs-source", { manifest: manifestWithJobs("jobs-source", jobs), extraFiles: { [jobs.slice(2)]: "export default {};\n" }, }); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", message: "jobs must reference compiled ESM JavaScript with a .js or .mjs extension.", path: "entrypoints.jobs", }), ); }); it("rejects a declared jobs artifact that does not exist", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "jobs-missing", { manifest: manifestWithJobs("jobs-missing", "./dist/jobs/index.js"), }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_MISSING"); }); it("rejects a jobs entrypoint that resolves to a directory", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "jobs-directory", { manifest: manifestWithJobs("jobs-directory", "./dist/jobs.js"), extraFiles: { "dist/jobs.js/placeholder.txt": "not a file entrypoint" }, }); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", message: "jobs must resolve to a file.", path: "entrypoints.jobs", }), ); }); it("rejects a jobs entrypoint linked outside its own module package", async () => { const modulesDirectory = await createModulesDirectory(); const externalDirectory = await createModulesDirectory(); const externalArtifact = path.join(externalDirectory, "foreign-jobs.js"); await writeFile(externalArtifact, "export default {};\n"); await createModule(modulesDirectory, "jobs-linked", { manifest: manifestWithJobs("jobs-linked", "./dist/jobs/index.js"), }); await mkdir(path.join(modulesDirectory, "jobs-linked/dist/jobs"), { recursive: true }); await symlink( externalArtifact, path.join(modulesDirectory, "jobs-linked/dist/jobs/index.js"), ); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_INVALID"); }); it("never imports a jobs artifact merely to discover it", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "jobs-inert", { manifest: manifestWithJobs("jobs-inert", "./dist/jobs/index.js"), // Discovery is inert. If it ever imported a jobs entrypoint this would reject rather than // discover successfully — `jobs` still has no runtime consumer. extraFiles: { "dist/jobs/index.js": "throw new Error('a jobs entrypoint must never be evaluated');", }, }); const discovered = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered).toHaveLength(1); expect(discovered[0]?.entrypoints.jobs).toContain("dist/jobs/index.js"); }); }); describe("module-owned UI entrypoint (ADR 0021)", () => { function manifestWithUi(id: string, ui: string): Record { const manifest = manifestFor(id); manifest.entrypoints = { server: "./dist/server/index.js", ui }; return manifest; } it("resolves a declared compiled UI entrypoint to a canonical path inside the package", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { manifest: manifestWithUi("announcements", "./dist/ui/index.js"), extraFiles: { "dist/ui/index.js": "export default {};" }, }); const [discovered] = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered?.entrypoints.ui).toBe( path.join(discovered?.directory ?? "", "dist/ui/index.js"), ); expect(discovered?.entrypoints.server).toBeDefined(); }); it("omits the resolved UI entrypoint entirely for a backend-only module", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "roster"); const [discovered] = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered?.entrypoints.ui).toBeUndefined(); expect(discovered?.entrypoints.display).toBeUndefined(); expect(Object.hasOwn(discovered?.entrypoints ?? {}, "ui")).toBe(false); expect(Object.hasOwn(discovered?.entrypoints ?? {}, "display")).toBe(false); }); it("rejects a source TypeScript UI entrypoint where compiled output is required", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithUi("signage", "./src/ui/index.ts"), extraFiles: { "src/ui/index.ts": "export default {};" }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_INVALID"); expect(error.issues.map((issue) => issue.path)).toContain("entrypoints.ui"); }); it("rejects a missing UI artifact rather than silently ignoring the declaration", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "video", { manifest: manifestWithUi("video", "./dist/ui/index.js"), }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_MISSING"); }); it("rejects a UI entrypoint that resolves to a directory instead of a file", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "livestream", { manifest: manifestWithUi("livestream", "./dist/ui.js"), extraFiles: { "dist/ui.js/placeholder.txt": "not a file entrypoint" }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_INVALID"); }); it("rejects a UI entrypoint linked outside its own module package", async () => { const modulesDirectory = await createModulesDirectory(); const externalDirectory = await createModulesDirectory(); const externalArtifact = path.join(externalDirectory, "foreign-ui.js"); await writeFile(externalArtifact, "export default {};"); await createModule(modulesDirectory, "calendar", { manifest: manifestWithUi("calendar", "./dist/ui/index.js"), }); await mkdir(path.join(modulesDirectory, "calendar/dist/ui"), { recursive: true }); await symlink(externalArtifact, path.join(modulesDirectory, "calendar/dist/ui/index.js")); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_INVALID"); }); it("never imports a UI artifact merely to discover it", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { manifest: manifestWithUi("announcements", "./dist/ui/index.js"), // Discovery is inert: a throwing artifact must not be evaluated. If discovery ever // imported a UI entrypoint, this would surface as a rejected promise rather than a // successfully discovered module. extraFiles: { "dist/ui/index.js": "throw new Error('a UI entrypoint must never be evaluated');", }, }); const discovered = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered).toHaveLength(1); expect(discovered[0]?.entrypoints.ui).toContain("dist/ui/index.js"); }); }); describe("module-owned display UI entrypoint", () => { function manifestWithDisplay( id: string, artifact: string, ui?: string, ): Record { const manifest = manifestFor(id); manifest.entrypoints = { server: "./dist/server/index.js", ...(ui === undefined ? {} : { ui }), display: { artifact, permission: `${id}.records.read` }, }; return manifest; } it("resolves a display-only artifact and preserves its module-owned permission", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { manifest: manifestWithDisplay("announcements", "./dist/display/index.mjs"), extraFiles: { "dist/display/index.mjs": "throw new Error('discovery must stay inert');", }, }); const [discovered] = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered?.entrypoints.ui).toBeUndefined(); expect(discovered?.entrypoints.display).toEqual({ artifactPath: path.join(discovered?.directory ?? "", "dist/display/index.mjs"), permission: "announcements.records.read", }); expect(Object.isFrozen(discovered?.entrypoints.display)).toBe(true); }); it("resolves independent primary and display artifacts without executing either", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithDisplay("signage", "./dist/display/index.js", "./dist/ui/index.js"), extraFiles: { "dist/display/index.js": "throw new Error('display must stay inert');", "dist/ui/index.js": "throw new Error('primary UI must stay inert');", }, }); const [discovered] = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(discovered?.entrypoints.ui).toContain("dist/ui/index.js"); expect(discovered?.entrypoints.display?.artifactPath).toContain("dist/display/index.js"); }); it("rejects source TypeScript and reports the nested display artifact path", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithDisplay("signage", "./src/display/index.tsx"), extraFiles: { "src/display/index.tsx": "export default {};" }, }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_INVALID"); expect(error.issues).toContainEqual( expect.objectContaining({ path: "entrypoints.display.artifact" }), ); }); it("rejects a declared display whose compiled artifact is missing", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithDisplay("signage", "./dist/display/index.js"), }); const error = await discoveryError(modulesDirectory); expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_MISSING"); expect(error.issues).toContainEqual( expect.objectContaining({ path: "entrypoints.display.artifact" }), ); }); it("rejects distinct configured UI paths that resolve through links to one artifact", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithDisplay("signage", "./dist/display/index.js", "./dist/ui/index.js"), extraFiles: { "dist/shared.js": "export default {};" }, }); const moduleDirectory = path.join(modulesDirectory, "signage"); await Promise.all([ mkdir(path.join(moduleDirectory, "dist/ui"), { recursive: true }), mkdir(path.join(moduleDirectory, "dist/display"), { recursive: true }), ]); await Promise.all([ symlink("../shared.js", path.join(moduleDirectory, "dist/ui/index.js")), symlink("../shared.js", path.join(moduleDirectory, "dist/display/index.js")), ]); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", path: "entrypoints.display.artifact", }), ); }); it("rejects a display artifact that resolves through a link to the server artifact", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "signage", { manifest: manifestWithDisplay("signage", "./dist/display/index.js"), }); const moduleDirectory = path.join(modulesDirectory, "signage"); await mkdir(path.join(moduleDirectory, "dist/display"), { recursive: true }); await symlink("../server/index.js", path.join(moduleDirectory, "dist/display/index.js")); const error = await discoveryError(modulesDirectory); expect(error.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", path: "entrypoints.display.artifact", }), ); }); }); }); /** * A contribution artifact is resolved through the identical loop as every other compiled entrypoint * (ADR 0041), so it inherits every protection `entrypoints.ui` already had rather than receiving a * second, weaker implementation of them. */ describe("browser contribution artifact resolution", () => { function contributingManifest(overrides: Record = {}): Record { return { ...manifestFor("announcements"), contributions: [ { id: "announcements.signage-slide", version: "1.0.0", capability: "signage.slide-renderer", capabilityRange: "^1.0.0", artifact: "./dist/contributions/signage-slide/index.js", ...overrides, }, ], }; } it("resolves a declared contribution to a canonical path inside the package", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { extraFiles: { "dist/contributions/signage-slide/index.js": "export default {};" }, manifest: contributingManifest(), }); const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); const contribution = modules[0]?.contributions[0]; expect(contribution).toMatchObject({ capability: "signage.slide-renderer", capabilityRange: "^1.0.0", id: "announcements.signage-slide", version: "1.0.0", }); expect(contribution?.artifactPath.endsWith("dist/contributions/signage-slide/index.js")).toBe( true, ); expect(path.isAbsolute(contribution?.artifactPath ?? "")).toBe(true); }); it("reports an empty contribution list for a module that contributes nothing", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "calendar"); const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); expect(modules[0]?.contributions).toEqual([]); }); it("rejects a missing contribution artifact", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { manifest: contributingManifest() }); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).rejects.toMatchObject({ issues: expect.arrayContaining([ expect.objectContaining({ code: "MODULE_ENTRYPOINT_MISSING" }), ]), }); }); it("rejects a contribution artifact that is not compiled JavaScript", async () => { const modulesDirectory = await createModulesDirectory(); await createModule(modulesDirectory, "announcements", { extraFiles: { "src/contributions/signage-slide/index.tsx": "export default {};" }, manifest: contributingManifest({ artifact: "./src/contributions/signage-slide/index.tsx" }), }); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).rejects.toMatchObject({ issues: expect.arrayContaining([ expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID" }), ]), }); }); it("rejects a contribution artifact that escapes the package through a link", async () => { const modulesDirectory = await createModulesDirectory(); const outside = await createModulesDirectory(); await writeFile(path.join(outside, "elsewhere.js"), "export default {};"); await createModule(modulesDirectory, "announcements", { manifest: contributingManifest(), }); await mkdir(path.join(modulesDirectory, "announcements/dist/contributions/signage-slide"), { recursive: true, }); await symlink( path.join(outside, "elsewhere.js"), path.join(modulesDirectory, "announcements/dist/contributions/signage-slide/index.js"), ); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).rejects.toMatchObject({ issues: expect.arrayContaining([ expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", path: "contributions.0.artifact", }), ]), }); }); it("rejects two contributions that resolve through a link to the same file", async () => { const modulesDirectory = await createModulesDirectory(); const manifest = { ...manifestFor("announcements"), contributions: [ { id: "announcements.first", version: "1.0.0", capability: "signage.slide-renderer", capabilityRange: "^1.0.0", artifact: "./dist/contributions/first/index.js", }, { id: "announcements.second", version: "1.0.0", capability: "signage.slide-renderer", capabilityRange: "^1.0.0", artifact: "./dist/contributions/second/index.js", }, ], }; await createModule(modulesDirectory, "announcements", { extraFiles: { "dist/contributions/first/index.js": "export default {};" }, manifest, }); await mkdir(path.join(modulesDirectory, "announcements/dist/contributions/second"), { recursive: true, }); await symlink( path.join(modulesDirectory, "announcements/dist/contributions/first/index.js"), path.join(modulesDirectory, "announcements/dist/contributions/second/index.js"), ); await expect( discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }), ).rejects.toMatchObject({ issues: expect.arrayContaining([ expect.objectContaining({ code: "MODULE_ENTRYPOINT_INVALID", path: "contributions" }), ]), }); }); });