CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m32s
Every Module Package v1 now declares the module's license and carries its text. packageFormat stays 1; pre-ADR-0051 metadata fails closed. - Contracts: ekklesia.package.json requires an SPDX `license` expression, validated with spdx-expression-parse, with UNLICENSED and non-expressions refused. The `files` list must hold exactly one non-empty root LICENSE. The package contracts move to the @ekklesia/contracts/module-package subpath, so the SPDX parser and its data (spdx-exceptions and spdx-license-ids ship no licence files) never reach a module bundle through the SDK's re-export of the contracts root. - Packager: reads the source package.json's string SPDX declaration and never invents one. It requires an explicit --license-file (a non-empty regular file, no symlink, no parent search, no download) and copies its bytes unchanged to LICENSE, listed with size and SHA-256 and covered by the archive digest. THIRD_PARTY_NOTICES stays third-party only, and bundled Ekklesia code stays in `incorporates`. - Verifier: rejects missing or invalid license metadata, and a missing, unlisted, duplicated, or corrupted LICENSE, through the schema and the existing strict inventory. - Template: packaged as AGPL-3.0-or-later with the repository's canonical LICENSE, passed explicitly (Dockerfile, smoke, docs). Packing it twice gives byte-identical archives. Started by Codex; reviewed and completed in Claude Code (empty license-text refusal, schema non-empty LICENSE, symlink and empty-file tests). ADR 0051 is Accepted. Co-Authored-By: Claude Opus 5.5 <[email protected]>
208 lines
7.8 KiB
TypeScript
208 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
MODULE_PACKAGE_FORMAT,
|
|
MODULE_PACKAGE_LICENSE_FILENAME,
|
|
MODULE_PACKAGE_METADATA_FILENAME,
|
|
modulePackageArchiveFileName,
|
|
modulePackageRuntimePackage,
|
|
safeParseModulePackageLicenseExpression,
|
|
safeParseModulePackageMetadata,
|
|
safeParseModulePackageRuntimePackage,
|
|
safeParseModulePackageTarget,
|
|
} from "../src/module-package.js";
|
|
|
|
const digest = "a".repeat(64);
|
|
|
|
function metadata(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
return {
|
|
packageFormat: 1,
|
|
module: { id: "example", version: "0.1.0" },
|
|
license: "AGPL-3.0-or-later",
|
|
target: "any",
|
|
runtime: { node: ">=24.11.0 <25" },
|
|
provenance: {
|
|
revision: "86ae907f59458508e3277666cdd214f5bd7c1078",
|
|
packager: { name: "@ekklesia/module-pack", version: "0.1.2" },
|
|
},
|
|
incorporates: { "@ekklesia/contracts": "0.1.2", "@ekklesia/module-sdk": "0.1.2" },
|
|
transitional: { databaseRuntime: { "drizzle-orm": "0.45.2" } },
|
|
files: [
|
|
{ path: "LICENSE", size: 9, sha256: digest },
|
|
{ path: "THIRD_PARTY_NOTICES", size: 10, sha256: digest },
|
|
{ path: "dist/server/index.js", size: 20, sha256: digest },
|
|
{ path: "ekklesia.module.json", size: 30, sha256: digest },
|
|
{ path: "package.json", size: 40, sha256: digest },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("Ekklesia Module Package v1 metadata (ADR 0049)", () => {
|
|
it("names the format, its metadata file, and the recommended archive file name", () => {
|
|
expect(MODULE_PACKAGE_FORMAT).toBe(1);
|
|
expect(MODULE_PACKAGE_LICENSE_FILENAME).toBe("LICENSE");
|
|
expect(MODULE_PACKAGE_METADATA_FILENAME).toBe("ekklesia.package.json");
|
|
expect(modulePackageArchiveFileName("example", "0.1.0", "any")).toBe(
|
|
"example-0.1.0-any.tar.gz",
|
|
);
|
|
});
|
|
|
|
it("accepts SPDX license expressions and rejects non-expressions without a policy allowlist", () => {
|
|
for (const expression of [
|
|
"AGPL-3.0-or-later",
|
|
"MIT OR Apache-2.0",
|
|
"(MIT OR Apache-2.0) AND BSD-3-Clause",
|
|
]) {
|
|
expect(safeParseModulePackageLicenseExpression(expression).success, expression).toBe(true);
|
|
}
|
|
for (const expression of ["", "UNLICENSED", "MIT OR", "not a license", {}]) {
|
|
expect(safeParseModulePackageLicenseExpression(expression).success, String(expression)).toBe(
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("requires the package license declaration and canonical LICENSE payload entry", () => {
|
|
const withoutLicense = metadata();
|
|
delete withoutLicense.license;
|
|
expect(safeParseModulePackageMetadata(withoutLicense).success).toBe(false);
|
|
|
|
const withoutLicenseFile = metadata();
|
|
withoutLicenseFile.files = (withoutLicenseFile.files as { path: string }[]).filter(
|
|
(file) => file.path !== "LICENSE",
|
|
);
|
|
expect(safeParseModulePackageMetadata(withoutLicenseFile).success).toBe(false);
|
|
|
|
const emptyLicenseFile = metadata();
|
|
emptyLicenseFile.files = (emptyLicenseFile.files as { path: string; size: number }[]).map(
|
|
(file) => (file.path === "LICENSE" ? { ...file, size: 0 } : file),
|
|
);
|
|
expect(safeParseModulePackageMetadata(emptyLicenseFile).success).toBe(false);
|
|
});
|
|
|
|
it("accepts a complete any-target package description", () => {
|
|
expect(safeParseModulePackageMetadata(metadata()).success).toBe(true);
|
|
});
|
|
|
|
it("accepts a package that incorporates nothing and needs no transitional facts", () => {
|
|
const { transitional: _transitional, ...rest } = metadata();
|
|
expect(safeParseModulePackageMetadata({ ...rest, incorporates: {} }).success).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
["an unknown top-level field", { signature: "x" }],
|
|
["format 2", { packageFormat: 2 }],
|
|
["a missing source revision", { provenance: { packager: { name: "p", version: "1.0.0" } } }],
|
|
[
|
|
"an abbreviated revision",
|
|
{ provenance: { revision: "86ae907", packager: { name: "p", version: "1.0.0" } } },
|
|
],
|
|
["a non-semver module version", { module: { id: "example", version: "1.0" } }],
|
|
["an invalid module id", { module: { id: "Example", version: "0.1.0" } }],
|
|
["an invalid Node range", { runtime: { node: "soon" } }],
|
|
["native runtime facts on an any target", { runtime: { node: ">=24", nodeApi: 9 } }],
|
|
["a non-Ekklesia incorporated package", { incorporates: { zod: "4.4.3" } }],
|
|
[
|
|
"an incorporated range instead of a version",
|
|
{ incorporates: { "@ekklesia/module-sdk": "^0.1.0" } },
|
|
],
|
|
["an unknown transitional key", { transitional: { orm: {} } }],
|
|
["an unknown database runtime", { transitional: { databaseRuntime: { prisma: "6.0.0" } } }],
|
|
])("rejects %s", (_label, overrides) => {
|
|
expect(safeParseModulePackageMetadata(metadata(overrides)).success).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
["an absolute path", "/etc/passwd"],
|
|
["a parent segment", "dist/../../secret"],
|
|
["a dot segment", "./package.json"],
|
|
["an empty segment", "dist//index.js"],
|
|
["a backslash", "dist\\index.js"],
|
|
["a NUL byte", "dist/\u0000"],
|
|
["the metadata file itself", "ekklesia.package.json"],
|
|
])("rejects a file entry with %s", (_label, path) => {
|
|
const files = [{ path, size: 1, sha256: digest }];
|
|
expect(safeParseModulePackageMetadata(metadata({ files })).success).toBe(false);
|
|
});
|
|
|
|
it("requires file entries in byte-wise path order, without duplicates", () => {
|
|
const entry = (path: string) => ({ path, size: 1, sha256: digest });
|
|
expect(
|
|
safeParseModulePackageMetadata(metadata({ files: [entry("b"), entry("a")] })).success,
|
|
).toBe(false);
|
|
expect(
|
|
safeParseModulePackageMetadata(metadata({ files: [entry("a"), entry("a")] })).success,
|
|
).toBe(false);
|
|
// Byte-wise, not locale order: upper case sorts before lower case.
|
|
expect(
|
|
safeParseModulePackageMetadata(
|
|
metadata({ files: [entry("LICENSE"), entry("THIRD"), entry("dist")] }),
|
|
).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
["an uppercase digest", { sha256: "A".repeat(64) }],
|
|
["a short digest", { sha256: "a".repeat(63) }],
|
|
["a negative size", { size: -1 }],
|
|
["a fractional size", { size: 1.5 }],
|
|
])("rejects a file entry with %s", (_label, override) => {
|
|
const files = [{ path: "package.json", size: 1, sha256: digest, ...override }];
|
|
expect(safeParseModulePackageMetadata(metadata({ files })).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("module package targets", () => {
|
|
it.each([
|
|
"any",
|
|
"linux-x64-glibc",
|
|
"linux-arm64-glibc",
|
|
"linux-x64-musl",
|
|
"darwin-arm64",
|
|
])("accepts %s", (target) => {
|
|
expect(safeParseModulePackageTarget(target).success).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
"",
|
|
"ANY",
|
|
"linux-x64",
|
|
"linux-x64-gnu",
|
|
"darwin-arm64-glibc",
|
|
"linux",
|
|
"any-x64",
|
|
])("rejects %j", (target) => {
|
|
expect(safeParseModulePackageTarget(target).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("the generated runtime package.json", () => {
|
|
it("holds exactly what module discovery reads", () => {
|
|
expect(
|
|
modulePackageRuntimePackage({ name: "@ekklesia/module-template", version: "0.1.0" }),
|
|
).toEqual({
|
|
name: "@ekklesia/module-template",
|
|
version: "0.1.0",
|
|
private: true,
|
|
type: "module",
|
|
ekklesia: { layer: "module" },
|
|
});
|
|
});
|
|
|
|
it("accepts the generated shape and nothing more", () => {
|
|
const generated = modulePackageRuntimePackage({ name: "@scope/example", version: "1.2.3" });
|
|
expect(safeParseModulePackageRuntimePackage(generated).success).toBe(true);
|
|
for (const extra of [
|
|
{ dependencies: { "@ekklesia/module-sdk": "workspace:*" } },
|
|
{ scripts: { build: "tsc" } },
|
|
{ exports: { ".": "./dist/server/index.js" } },
|
|
{ imports: { "#x": "./x.js" } },
|
|
]) {
|
|
expect(safeParseModulePackageRuntimePackage({ ...generated, ...extra }).success).toBe(false);
|
|
}
|
|
expect(safeParseModulePackageRuntimePackage({ ...generated, type: "commonjs" }).success).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|