149 lines
6.9 KiB
TypeScript
149 lines
6.9 KiB
TypeScript
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import pluginCandidates from "../../../docs/feature-manifests/plugin-candidates.json";
|
|
import { loadFeatureManifests } from "../src/manifest-loader";
|
|
|
|
function manifestWith(overrides: Record<string, unknown>) {
|
|
return structuredClone({ ...pluginCandidates, ...overrides });
|
|
}
|
|
|
|
async function makeFixtureDirectory() {
|
|
const rootDir = await mkdtemp(path.join(os.tmpdir(), "feature-manifests-"));
|
|
const manifestDir = path.join(rootDir, "docs", "feature-manifests");
|
|
await mkdir(manifestDir, { recursive: true });
|
|
return { rootDir, manifestDir };
|
|
}
|
|
|
|
async function writeManifest(manifestDir: string, fileName: string, overrides: Record<string, unknown> = {}) {
|
|
const manifest = manifestWith(overrides);
|
|
await writeFile(path.join(manifestDir, fileName), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
return manifest;
|
|
}
|
|
|
|
describe("loadFeatureManifests", () => {
|
|
it("loads valid docs manifests in deterministic order and skips the schema by default", async () => {
|
|
const result = await loadFeatureManifests();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.diagnostics).toEqual([]);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toContain("plugin-candidates");
|
|
expect(result.manifestFiles.map((file) => file.fileName)).toEqual([...result.manifestFiles.map((file) => file.fileName)].sort((a, b) => a.localeCompare(b)));
|
|
expect(result.manifestFiles.some((file) => file.fileName === "schema.json")).toBe(false);
|
|
});
|
|
|
|
it("reports a missing manifest directory", async () => {
|
|
const rootDir = await mkdtemp(path.join(os.tmpdir(), "feature-manifests-missing-"));
|
|
const result = await loadFeatureManifests({ rootDir });
|
|
|
|
expect(result).toMatchObject({ ok: false, manifests: [], manifestFiles: [] });
|
|
expect(result.diagnostics[0]).toMatchObject({ code: "missing_directory", manifestId: null, path: path.join(rootDir, "docs", "feature-manifests") });
|
|
});
|
|
|
|
it("preserves sibling valid manifests when one file contains malformed JSON", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "plugin-candidates.json", { id: "plugin-candidates", dependencies: [] });
|
|
await writeFile(path.join(manifestDir, "broken.json"), "{ not json", "utf8");
|
|
|
|
const result = await loadFeatureManifests({ rootDir });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toEqual(["plugin-candidates"]);
|
|
expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "invalid_json", manifestId: null, path: path.join(manifestDir, "broken.json") }));
|
|
});
|
|
|
|
it("loads from an absolute manifest directory", async () => {
|
|
const { manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "plugin-candidates.json", { id: "plugin-candidates", dependencies: [] });
|
|
|
|
const result = await loadFeatureManifests({ rootDir: "/ignored/root", manifestDir });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toEqual(["plugin-candidates"]);
|
|
expect(result.manifestFiles[0]).toMatchObject({
|
|
fileName: "plugin-candidates.json",
|
|
filePath: path.join(manifestDir, "plugin-candidates.json"),
|
|
manifestId: "plugin-candidates",
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it("can include schema-like json files when requested", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "schema.json", { id: "schema-file", dependencies: [] });
|
|
await writeManifest(manifestDir, "plugin-candidates.json", { id: "plugin-candidates", dependencies: [] });
|
|
|
|
const result = await loadFeatureManifests({ rootDir, includeSchema: true });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.manifestFiles.map((file) => file.fileName)).toEqual(["plugin-candidates.json", "schema.json"]);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toEqual(["plugin-candidates", "schema-file"]);
|
|
});
|
|
|
|
it("uses custom schema file names for the default skip rule", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "manifest.schema.json", { id: "custom-schema", dependencies: [] });
|
|
await writeManifest(manifestDir, "schema.json", { id: "ordinary-schema-name", dependencies: [] });
|
|
|
|
const result = await loadFeatureManifests({ rootDir, schemaFileName: "manifest.schema.json" });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.manifestFiles.map((file) => file.fileName)).toEqual(["schema.json"]);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toEqual(["ordinary-schema-name"]);
|
|
});
|
|
|
|
it("keeps manifest ids null for non-object json payloads", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
const filePath = path.join(manifestDir, "array.json");
|
|
await writeFile(filePath, "[]\n", "utf8");
|
|
|
|
const result = await loadFeatureManifests({ rootDir });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.manifests).toEqual([]);
|
|
expect(result.manifestFiles).toEqual([
|
|
{
|
|
fileName: "array.json",
|
|
filePath,
|
|
manifestId: null,
|
|
manifest: null,
|
|
diagnostics: [{ code: "invalid_type", manifestId: null, path: `${filePath}#${filePath}`, message: `${filePath} must be an object.` }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("reports invalid manifest shape diagnostics with source file metadata", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "invalid-shape.json", { id: "invalid-shape", classification: "optional", dependencies: [] });
|
|
|
|
const result = await loadFeatureManifests({ rootDir });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.manifests).toEqual([]);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "invalid_enum",
|
|
manifestId: "invalid-shape",
|
|
path: `${path.join(manifestDir, "invalid-shape.json")}#classification`,
|
|
message: "classification must be one of: core, plugin-candidate, remove-candidate, mixed.",
|
|
});
|
|
});
|
|
|
|
it("reports missing dependency references after loading valid manifests", async () => {
|
|
const { rootDir, manifestDir } = await makeFixtureDirectory();
|
|
await writeManifest(manifestDir, "plugin-candidates.json", { id: "plugin-candidates", dependencies: ["missing-feature"] });
|
|
|
|
const result = await loadFeatureManifests({ rootDir });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.manifests.map((manifest) => manifest.id)).toEqual(["plugin-candidates"]);
|
|
expect(result.diagnostics).toContainEqual({
|
|
code: "missing_dependency",
|
|
manifestId: "plugin-candidates",
|
|
path: `${path.join(manifestDir, "plugin-candidates.json")}#dependencies`,
|
|
message: "plugin-candidates depends on missing feature manifest missing-feature.",
|
|
});
|
|
});
|
|
});
|