219 lines
7.8 KiB
TypeScript
219 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import pluginCandidates from "../../../docs/feature-manifests/plugin-candidates.json";
|
|
import { validateFeatureManifestShape } from "../src/index";
|
|
|
|
function manifestWith(overrides: Record<string, unknown>) {
|
|
return structuredClone({ ...pluginCandidates, ...overrides });
|
|
}
|
|
|
|
describe("validateFeatureManifestShape", () => {
|
|
it("accepts the plugin candidates fixture", () => {
|
|
const result = validateFeatureManifestShape(pluginCandidates, { path: "docs/feature-manifests/plugin-candidates.json" });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.manifest?.id).toBe("plugin-candidates");
|
|
expect(result.diagnostics).toEqual([]);
|
|
});
|
|
|
|
it("reports invalid classification enum values with manifest context", () => {
|
|
const result = validateFeatureManifestShape(manifestWith({ classification: "optional" }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_enum",
|
|
manifestId: "plugin-candidates",
|
|
path: "classification",
|
|
message: "classification must be one of: core, plugin-candidate, remove-candidate, mixed.",
|
|
});
|
|
});
|
|
|
|
it("reports missing integration worker sections", () => {
|
|
const integration = structuredClone(pluginCandidates.integration) as Record<string, unknown>;
|
|
delete integration.workers;
|
|
const result = validateFeatureManifestShape(manifestWith({ integration }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "missing_required",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.workers",
|
|
message: "integration.workers is required.",
|
|
});
|
|
});
|
|
|
|
it("reports non-array dependencies", () => {
|
|
const result = validateFeatureManifestShape(manifestWith({ dependencies: "core" }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_type",
|
|
manifestId: "plugin-candidates",
|
|
path: "dependencies",
|
|
message: "dependencies must be an array.",
|
|
});
|
|
});
|
|
|
|
it("reports invalid component ids", () => {
|
|
const components = structuredClone(pluginCandidates.components);
|
|
components[0] = { ...components[0], id: "CRM" };
|
|
const result = validateFeatureManifestShape(manifestWith({ components }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "components.0.id",
|
|
message: "components.0.id has an invalid format.",
|
|
});
|
|
});
|
|
|
|
it("reports invalid optional component collections and notes", () => {
|
|
const components = [false, { ...pluginCandidates.components[0], notes: ["not prose"] }];
|
|
const result = validateFeatureManifestShape(manifestWith({ components }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_item",
|
|
manifestId: "plugin-candidates",
|
|
path: "components.0",
|
|
message: "components.0 must be an object.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_type",
|
|
manifestId: "plugin-candidates",
|
|
path: "components.1.notes",
|
|
message: "components.1.notes must be a string.",
|
|
});
|
|
});
|
|
|
|
it("reports invalid nested integration section types", () => {
|
|
const integration = {
|
|
...pluginCandidates.integration,
|
|
surfaces: [],
|
|
configuration: "env",
|
|
workers: { ...pluginCandidates.integration.workers, required: "yes", queues: ["mail", 7] },
|
|
};
|
|
const result = validateFeatureManifestShape(manifestWith({ integration }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_type",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.surfaces",
|
|
message: "integration.surfaces must be an object.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_type",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.configuration",
|
|
message: "integration.configuration must be an object.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_type",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.workers.required",
|
|
message: "integration.workers.required must be a boolean.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_item",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.workers.queues.1",
|
|
message: "integration.workers.queues.1 must be a string.",
|
|
});
|
|
});
|
|
|
|
it("reports invalid top-level constants, empty strings, and dependency tokens", () => {
|
|
const result = validateFeatureManifestShape(manifestWith({ schemaVersion: "2.0", title: "", dependencies: ["CRM"] }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_enum",
|
|
manifestId: "plugin-candidates",
|
|
path: "schemaVersion",
|
|
message: "schemaVersion must be 1.0.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "title",
|
|
message: "title must not be empty.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "dependencies.0",
|
|
message: "dependencies.0 has an invalid format.",
|
|
});
|
|
});
|
|
|
|
it("uses context manifest ids when the input id is empty", () => {
|
|
const result = validateFeatureManifestShape(manifestWith({ id: "" }), { manifestId: "fixture-id" });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "fixture-id",
|
|
path: "id",
|
|
message: "id has an invalid format.",
|
|
});
|
|
});
|
|
|
|
it("reports invalid lastReviewed dates", () => {
|
|
const validation = { ...pluginCandidates.validation, lastReviewed: "2026-99-05" };
|
|
const result = validateFeatureManifestShape(manifestWith({ validation }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "validation.lastReviewed",
|
|
message: "validation.lastReviewed must be an ISO date in YYYY-MM-DD format.",
|
|
});
|
|
});
|
|
|
|
it("rejects prose in permission and audit-event token arrays", () => {
|
|
const integration = {
|
|
...pluginCandidates.integration,
|
|
permissions: ["admin.email.* when settings are exposed"],
|
|
auditEvents: ["Provider key updated"],
|
|
};
|
|
const result = validateFeatureManifestShape(manifestWith({ integration }));
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.permissions.0",
|
|
message: "integration.permissions.0 has an invalid format.",
|
|
});
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_format",
|
|
manifestId: "plugin-candidates",
|
|
path: "integration.auditEvents.0",
|
|
message: "integration.auditEvents.0 has an invalid format.",
|
|
});
|
|
});
|
|
|
|
it("accepts wildcard and camel-case permission tokens plus dot-separated audit events", () => {
|
|
const integration = {
|
|
...pluginCandidates.integration,
|
|
permissions: ["admin.emails.*", "app.settings.apiKeys.view"],
|
|
auditEvents: ["mail.provider.updated", "admin.ai.provider_key.updated"],
|
|
};
|
|
const result = validateFeatureManifestShape(manifestWith({ integration }));
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("returns diagnostics instead of throwing for non-object input", () => {
|
|
const result = validateFeatureManifestShape(null, { manifestId: "bad", path: "fixture" });
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
manifest: null,
|
|
diagnostics: [{ code: "invalid_type", manifestId: "bad", path: "fixture", message: "fixture must be an object." }],
|
|
});
|
|
});
|
|
});
|