import { describe, expect, it } from "@suaveplan/testing/runner"; import { MANIFEST_CODES } from "../error-codes/error-codes.js"; import type { PluginManifest } from "../schema/plugin-manifest.js"; import { MINIMAL_FIXTURE } from "../testing/conformance.js"; import { validateErrorStatusMapShape } from "./validate-error-status-map-shape.js"; import { validateOpenApiRequiresGraphqlOrRoutes } from "./validate-openapi-requires-graphql-or-routes.js"; import { validatePermissionsWhenSandboxed } from "./validate-permissions-when-sandboxed.js"; import { validateSagaOwnership } from "./validate-saga-ownership.js"; describe("validateSagaOwnership", () => { it("rejects step referencing undeclared dependency", { timeout: 5000, }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, sagas: { owns: [ { name: "s1", steps: [ { plugin: "@acme/unknown", action: "a", compensate: "c", }, ], }, ], }, }; const r = validateSagaOwnership(m); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error.code).toBe(MANIFEST_CODES.INVALID_SAGA_OWNERSHIP); } }); it("accepts step naming the manifest itself", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, sagas: { owns: [ { name: "s1", steps: [ { plugin: MINIMAL_FIXTURE.name, action: "a", compensate: "c", }, ], }, ], }, }; expect(validateSagaOwnership(m).ok).toBe(true); }); it("accepts step naming a declared dependency", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, dependsOn: { "@acme/dep": "^1" }, sagas: { owns: [ { name: "s1", steps: [ { plugin: "@acme/dep", action: "a", compensate: "c", }, ], }, ], }, }; expect(validateSagaOwnership(m).ok).toBe(true); }); it("no-op when sagas absent", { timeout: 5000 }, () => { expect(validateSagaOwnership(MINIMAL_FIXTURE).ok).toBe(true); }); it("no-op when owns is empty", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, sagas: { owns: [] }, }; expect(validateSagaOwnership(m).ok).toBe(true); }); }); describe("validatePermissionsWhenSandboxed", () => { it("sandboxed isolation requires permissions.budgets", { timeout: 5000, }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, isolation: "sandboxed", }; const r = validatePermissionsWhenSandboxed(m); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error.code).toBe(MANIFEST_CODES.SANDBOX_REQUIRES_BUDGETS); } }); it("ok when sandboxed with budgets", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, isolation: "sandboxed", permissions: { budgets: { cpuPercent: 25 } }, }; expect(validatePermissionsWhenSandboxed(m).ok).toBe(true); }); it("no-op for non-sandboxed tiers", { timeout: 5000 }, () => { expect(validatePermissionsWhenSandboxed(MINIMAL_FIXTURE).ok).toBe(true); }); }); describe("validateOpenApiRequiresGraphqlOrRoutes", () => { it("ok when not contributing", { timeout: 5000 }, () => { expect(validateOpenApiRequiresGraphqlOrRoutes(MINIMAL_FIXTURE).ok).toBe( true ); }); it("fails when contributing without routes or graphql", { timeout: 5000, }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, openapi: { contributes: true }, }; const r = validateOpenApiRequiresGraphqlOrRoutes(m); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error.code).toBe( MANIFEST_CODES.OPENAPI_REQUIRES_GRAPHQL_OR_ROUTES ); } }); it("ok with routes", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, openapi: { contributes: true }, routes: { prefix: "/x" }, }; expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(true); }); it("ok with graphql.contributes=true", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, openapi: { contributes: true }, graphql: { contributes: true }, }; expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(true); }); it("fails when openapi contributes and graphql.contributes=false", { timeout: 5000, }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, openapi: { contributes: true }, graphql: { contributes: false }, }; expect(validateOpenApiRequiresGraphqlOrRoutes(m).ok).toBe(false); }); }); describe("validateErrorStatusMapShape", () => { it("no-op when statusMap absent", { timeout: 5000 }, () => { expect(validateErrorStatusMapShape(MINIMAL_FIXTURE).ok).toBe(true); }); it("ok when every entry is well-formed", { timeout: 5000 }, () => { const m: PluginManifest = { ...MINIMAL_FIXTURE, errors: { statusMap: { "PLUGIN.A.B": 409 } }, }; expect(validateErrorStatusMapShape(m).ok).toBe(true); }); it("collects code-shape offenders", { timeout: 5000 }, () => { // bypass Zod by passing a manually built object const bad = { ...MINIMAL_FIXTURE, errors: { statusMap: { "lower.case.code": 409, }, }, } as unknown as PluginManifest; const r = validateErrorStatusMapShape(bad); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error.code).toBe(MANIFEST_CODES.INVALID_ERROR_STATUS_MAP); const off = r.error.context?.offenders as unknown[]; expect(off.length).toBeGreaterThan(0); } }); it("collects status-range offenders", { timeout: 5000 }, () => { const bad = { ...MINIMAL_FIXTURE, errors: { statusMap: { "PLUGIN.A.B": 200 }, }, } as unknown as PluginManifest; const r = validateErrorStatusMapShape(bad); expect(r.ok).toBe(false); }); it("rejects non-integer status", { timeout: 5000 }, () => { const bad = { ...MINIMAL_FIXTURE, errors: { statusMap: { "PLUGIN.A.B": 4.5 as unknown as number }, }, } as unknown as PluginManifest; const r = validateErrorStatusMapShape(bad); expect(r.ok).toBe(false); }); });