import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { checkBoundaries } from "../scripts/check-boundaries.js"; const testDirectory = path.dirname(fileURLToPath(import.meta.url)); const fixtures = path.join(testDirectory, "fixtures", "boundaries"); describe("repository dependency boundaries", () => { it("accepts public contracts below Core", async () => { const result = await checkBoundaries(path.join(fixtures, "legal")); expect(result.violations).toEqual([]); }); it("accepts generated module composition only through the generated bootstrap boundary", async () => { const result = await checkBoundaries(path.join(fixtures, "legal-generated-bootstrap")); expect(result.violations).toEqual([]); }); it("honors explicit pnpm workspace exclusions", async () => { const result = await checkBoundaries(path.join(fixtures, "workspace-exclusion")); expect(result.violations).toEqual([]); }); it.each([ ["core-imports-module", "core-is-feature-agnostic"], ["app-imports-module", "apps-are-composition-roots"], ["module-imports-core", "modules-use-public-platform-contracts"], ["module-imports-surface", "modules-use-public-platform-contracts"], ["package-boundary-direction", "modules-use-public-platform-contracts"], ["cross-module-import", "modules-do-not-import-other-modules"], ["module-schema-imports-module", "modules-do-not-import-other-modules"], ["surface-imports-module", "surfaces-use-public-contracts"], ["cross-surface-import", "surfaces-are-independent"], ["builder-imports-module", "builder-composes-contracts"], ["contracts-import-core", "contracts-do-not-depend-upward"], ["sdk-imports-core", "sdks-do-not-depend-on-implementations"], ["shared-role-order", "sdks-do-not-depend-on-implementations"], ["package-without-exports", "workspace-imports-use-public-exports"], ["private-package-import", "workspace-imports-use-public-exports"], ["non-workspace-version", "internal-dependencies-use-workspace-protocol"], ["workspace-alias", "workspace-dependency-aliases-are-forbidden"], ["undeclared-workspace-dependency", "workspace-dependencies-are-declared"], ["duplicate-package-name", "workspace-package-names-are-unique"], ["unknown-shared-role", "shared-packages-declare-known-roles"], ["workspace-unsupported-pattern", "workspace-patterns-use-supported-syntax"], ["package-cycle", "no-circular-package-dependencies"], ["module-has-ui", "modules-do-not-own-ui"], ["module-ui-framework", "modules-do-not-own-ui"], ["module-ui-aliased-framework", "modules-do-not-own-ui"], ["module-ui-imports-core", "modules-use-public-platform-contracts"], ["module-ui-bundler-plugin", "modules-do-not-own-ui"], ["module-imports-ui-host", "modules-use-public-platform-contracts"], ["module-imports-ui-package", "modules-use-public-platform-contracts"], ["cross-package-relative", "workspace-imports-use-public-exports"], ["cycle", "no-circular-source-dependencies"], ])("rejects %s", async (fixture, expectedRule) => { const result = await checkBoundaries(path.join(fixtures, fixture)); expect(result.violations.map((violation) => violation.rule)).toContain(expectedRule); }); it("rejects stylesheet and case-insensitive view files inside a module, but not TSX", async () => { const result = await checkBoundaries(path.join(fixtures, "module-has-ui")); const uiViolations = result.violations.filter( (violation) => violation.rule === "modules-do-not-own-ui", ); expect(uiViolations).toHaveLength(2); expect(uiViolations.map((violation) => violation.from)).toEqual( expect.arrayContaining([ "modules/calendar/src/calendar.css", "modules/calendar/src/view.ASTRO", ]), ); // React is the supported module UI authoring model (ADR 0022), so a module's own `.tsx` is // legitimate source and must no longer be reported. expect(uiViolations.map((violation) => violation.from)).not.toContain( "modules/calendar/src/calendar.tsx", ); }); it("rejects unsupported UI frameworks, their imports, and view formats inside a module", async () => { const result = await checkBoundaries(path.join(fixtures, "module-ui-framework")); const uiViolations = result.violations.filter( (violation) => violation.rule === "modules-do-not-own-ui", ); expect(uiViolations).toHaveLength(3); expect(uiViolations.map((violation) => violation.message)).toEqual( expect.arrayContaining([ "Module package depends on UI framework preact.", "Module source imports UI framework preact.", ]), ); // The fixture also aliases `compat` to react, which is now the supported framework and is // therefore no longer a violation. expect(uiViolations.map((violation) => violation.message).join(" ")).not.toContain("compat"); }); it("still rejects module UI source that reaches into Core internals", async () => { // Allowing `.tsx` inside a module changed which *formats* a module may contain. It did not // change which layers a module may import: module UI is subject to the identical layer rules // as module backend source. const result = await checkBoundaries(path.join(fixtures, "module-ui-imports-core")); expect(result.violations).toEqual([ { from: "modules/announcements/src/ui/editor.tsx", message: "module code may not import core code directly.", rule: "modules-use-public-platform-contracts", to: "core/src/index.ts", }, ]); }); it("accepts the canonical module UI dependency set: React, the UI SDK, and the build tool", async () => { // Mirrors what `templates/module` actually declares (ADR 0023). The build tool is a `tooling` // shared package, a role modules were already permitted to depend on, so the module UI build // pipeline required no boundary relaxation at all. const result = await checkBoundaries(path.join(fixtures, "module-owns-react-ui")); expect(result.violations).toEqual([]); }); it("keeps the host loader out of module packages", async () => { // `@ekklesia/module-ui-host` is host infrastructure. A module that depended on it would bundle // loader code into its own artifact and blur the author/host split (ADR 0024), so the // `module-ui-host` role is deliberately absent from the roles a module may depend on. const result = await checkBoundaries(path.join(fixtures, "module-imports-ui-host")); expect(result.violations.map((violation) => violation.rule)).toEqual([ "modules-use-public-platform-contracts", "modules-use-public-platform-contracts", ]); }); it("still rejects a module reaching for a bundler plugin ecosystem", async () => { // Module authors do not configure a bundler; `@ekklesia/module-ui-build` owns that entirely. const result = await checkBoundaries(path.join(fixtures, "module-ui-bundler-plugin")); expect(result.violations.map((violation) => violation.message)).toEqual([ "Module package depends on UI framework @vitejs/plugin-react.", ]); }); it("rejects an unsupported framework aliased behind the supported one", async () => { const result = await checkBoundaries(path.join(fixtures, "module-ui-aliased-framework")); expect(result.violations.map((violation) => violation.message)).toEqual([ "Module package depends on UI framework vue aliased as react.", ]); }); it("requires workspace: ranges in every dependency field", async () => { const result = await checkBoundaries(path.join(fixtures, "non-workspace-version")); expect( result.violations.filter( (violation) => violation.rule === "internal-dependencies-use-workspace-protocol", ), ).toHaveLength(4); }); it("resolves forbidden workspace aliases before applying layer rules", async () => { const result = await checkBoundaries(path.join(fixtures, "workspace-alias")); expect( result.violations.filter( (violation) => violation.rule === "workspace-dependency-aliases-are-forbidden", ), ).toHaveLength(2); expect(result.violations.map((violation) => violation.rule)).toEqual( expect.arrayContaining([ "workspace-dependency-aliases-are-forbidden", "modules-use-public-platform-contracts", ]), ); }); it("prevents contracts and SDKs from depending upward through shared packages", async () => { const result = await checkBoundaries(path.join(fixtures, "shared-role-order")); expect(result.violations.map((violation) => violation.rule)).toEqual( expect.arrayContaining([ "contracts-do-not-depend-upward", "sdks-do-not-depend-on-implementations", ]), ); }); });