195 lines
7.4 KiB
TypeScript
195 lines
7.4 KiB
TypeScript
import type { PluginManifest } from "@suaveplan/api-types/manifest";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { LifecycleError } from "../error-codes/error-codes.js";
|
|
import { compareSemver, requiresReinstall } from "./reinstall-criteria.js";
|
|
|
|
function manifest(overrides: Partial<PluginManifest> = {}): PluginManifest {
|
|
return {
|
|
name: "billing",
|
|
version: "0.1.0",
|
|
apiHost: "1.x",
|
|
isolation: "trusted",
|
|
trustLevel: "first-party",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("reinstall-criteria", () => {
|
|
describe("compareSemver", () => {
|
|
it("orders patch versions", { timeout: 5000 }, () => {
|
|
expect(compareSemver("1.0.0", "1.0.1")).toBe(-1);
|
|
expect(compareSemver("1.0.1", "1.0.0")).toBe(1);
|
|
expect(compareSemver("1.0.0", "1.0.0")).toBe(0);
|
|
});
|
|
|
|
it("orders minor versions", { timeout: 5000 }, () => {
|
|
expect(compareSemver("1.1.0", "1.2.0")).toBe(-1);
|
|
expect(compareSemver("1.2.0", "1.1.0")).toBe(1);
|
|
});
|
|
|
|
it("orders major versions", { timeout: 5000 }, () => {
|
|
expect(compareSemver("1.0.0", "2.0.0")).toBe(-1);
|
|
});
|
|
|
|
it("ignores prerelease and build metadata", { timeout: 5000 }, () => {
|
|
expect(compareSemver("1.0.0-rc.1", "1.0.0")).toBe(0);
|
|
expect(compareSemver("1.0.0+build.1", "1.0.0")).toBe(0);
|
|
});
|
|
|
|
it("handles missing segments as zeros", { timeout: 5000 }, () => {
|
|
expect(compareSemver("1", "1.0.0")).toBe(0);
|
|
expect(compareSemver("1.0", "1.0.1")).toBe(-1);
|
|
// Reverse: b shorter than a — exercises the `bp[i] ?? 0` branch.
|
|
expect(compareSemver("1.0.0", "1")).toBe(0);
|
|
expect(compareSemver("1.0.1", "1.0")).toBe(1);
|
|
});
|
|
|
|
it("treats non-numeric segments as zero", { timeout: 5000 }, () => {
|
|
expect(compareSemver("a.b.c", "0.0.0")).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("requiresReinstall", () => {
|
|
it("identical manifest returns false", { timeout: 5000 }, () => {
|
|
const m = manifest({ scopes: [{ name: "x" }] });
|
|
expect(requiresReinstall(m, m)).toBe(false);
|
|
});
|
|
|
|
it("schemaVersion bump returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({
|
|
database: { required: true, schemaVersion: 1 },
|
|
});
|
|
const next = manifest({
|
|
database: { required: true, schemaVersion: 2 },
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("schemaVersion unchanged returns false", { timeout: 5000 }, () => {
|
|
const prev = manifest({
|
|
database: { required: true, schemaVersion: 1 },
|
|
});
|
|
const next = manifest({
|
|
database: { required: true, schemaVersion: 1 },
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(false);
|
|
});
|
|
|
|
it("new scope returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({ scopes: [{ name: "read" }] });
|
|
const next = manifest({
|
|
scopes: [{ name: "read" }, { name: "write" }],
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("new publish topic returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({ events: { publishes: ["a"] } });
|
|
const next = manifest({ events: { publishes: ["a", "b"] } });
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("new subscribe topic returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({ events: { subscribes: ["a"] } });
|
|
const next = manifest({ events: { subscribes: ["a", "b"] } });
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("new cron job returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({
|
|
jobs: { cron: [{ name: "x", expression: "* * * * *" }] },
|
|
});
|
|
const next = manifest({
|
|
jobs: {
|
|
cron: [
|
|
{ name: "x", expression: "* * * * *" },
|
|
{ name: "y", expression: "* * * * *" },
|
|
],
|
|
},
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("new queued job returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({
|
|
jobs: { queued: [{ name: "a" }] },
|
|
});
|
|
const next = manifest({
|
|
jobs: { queued: [{ name: "a" }, { name: "b" }] },
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("new secret returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({
|
|
secrets: [{ name: "STRIPE", required: true }],
|
|
});
|
|
const next = manifest({
|
|
secrets: [
|
|
{ name: "STRIPE", required: true },
|
|
{ name: "PAYPAL", required: false },
|
|
],
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("dependsOn change returns true", { timeout: 5000 }, () => {
|
|
const prev = manifest({ dependsOn: { core: "1.x" } });
|
|
const next = manifest({ dependsOn: { core: "2.x" } });
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("dependsOn additions return true", { timeout: 5000 }, () => {
|
|
const prev = manifest({ dependsOn: { core: "1.x" } });
|
|
const next = manifest({
|
|
dependsOn: { core: "1.x", auth: "1.x" },
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(true);
|
|
});
|
|
|
|
it("version downgrade throws", { timeout: 5000 }, () => {
|
|
const prev = manifest({ version: "1.0.0" });
|
|
const next = manifest({ version: "0.9.0" });
|
|
try {
|
|
requiresReinstall(prev, next);
|
|
expect.fail("expected throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(LifecycleError);
|
|
const e = err as LifecycleError;
|
|
expect(e.code).toBe(
|
|
"LIFECYCLE.REINSTALL_REQUIRED_VERSION_DOWNGRADE"
|
|
);
|
|
expect(e.context).toEqual({ prev: "1.0.0", next: "0.9.0" });
|
|
}
|
|
});
|
|
|
|
it("no schemaVersion in either manifest treats as zero", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(requiresReinstall(manifest(), manifest())).toBe(false);
|
|
});
|
|
|
|
it("manifest with empty events block returns false", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const prev = manifest({
|
|
events: { publishes: [], subscribes: [] },
|
|
});
|
|
const next = manifest({
|
|
events: { publishes: [], subscribes: [] },
|
|
});
|
|
expect(requiresReinstall(prev, next)).toBe(false);
|
|
});
|
|
|
|
it("removing an item is NOT a reinstall trigger", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const prev = manifest({
|
|
scopes: [{ name: "read" }, { name: "write" }],
|
|
});
|
|
const next = manifest({ scopes: [{ name: "read" }] });
|
|
expect(requiresReinstall(prev, next)).toBe(false);
|
|
});
|
|
});
|
|
});
|