/*
RFC-1120: unit tests for feature disclosure — inventory union across system.md frontmatter, system-config deployment, content-tree markers, dist markers, and opportunistic entitlements; verdict mapping; feature-notes.md rendering.
- Do not test export wiring — covered by tests-handoff integration tests.
- RFC-1120: initial feature-notes tests.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stringify as stringifyYaml } from "yaml";
import { buildFeatureInventory, renderFeatureNotes } from "../feature-notes.ts";
let siteDir: string;
let distDir: string;
async function writeSystemMd(frontmatter: Record): Promise {
const dir = join(siteDir, "src", "content");
await mkdir(dir, { recursive: true });
await writeFile(join(dir, "system.md"), `---\n${stringifyYaml(frontmatter)}---\n\n`, "utf8");
}
async function inventory(systemConfig: Record = {}) {
return buildFeatureInventory({ siteDir, distDir, systemConfig });
}
beforeEach(async () => {
const root = await mkdtemp(join(tmpdir(), "na-features-"));
siteDir = join(root, "site");
distDir = join(root, "dist");
await mkdir(join(siteDir, "src", "content"), { recursive: true });
await mkdir(distDir, { recursive: true });
});
afterEach(async () => {
await rm(join(siteDir, ".."), { recursive: true, force: true });
});
test("detects entitlementsOverride features from system.md frontmatter", async () => {
await writeSystemMd({ entitlementsOverride: ["portal", "nachweis"] });
const features = await inventory();
const ids = features.map((f) => f.feature);
expect(ids).toContain("portal");
expect(ids).toContain("nachweis");
// unknown ids default to needs-backend (fail-honest)
expect(features.find((f) => f.feature === "nachweis")?.verdict).toBe("needs-backend");
});
test("detects integrations from system.md frontmatter as needs-backend", async () => {
await writeSystemMd({ integrations: { matomo: { enabled: true }, uchat: {} } });
const features = await inventory();
expect(features.find((f) => f.feature === "integrations.matomo")?.verdict).toBe("needs-backend");
expect(features.find((f) => f.feature === "integrations.uchat")?.verdict).toBe("needs-backend");
});
test("detects worker-serving from deployment.adapter in system-config", async () => {
await writeSystemMd({});
const features = await inventory({ deployment: { adapter: "cloudflare-workers" } });
expect(features.find((f) => f.feature === "worker-serving")?.verdict).toBe("needs-backend");
});
test("detects worker-serving from dist/_worker.js marker", async () => {
await writeSystemMd({});
await writeFile(join(distDir, "_worker.js"), "export default {}\n", "utf8");
const features = await inventory();
expect(features.find((f) => f.feature === "worker-serving")?.verdict).toBe("needs-backend");
});
test("detects portal-routes from src/content/portal tree marker", async () => {
await writeSystemMd({});
await mkdir(join(siteDir, "src", "content", "portal"), { recursive: true });
const features = await inventory();
expect(features.find((f) => f.feature === "portal-routes")?.verdict).toBe("needs-backend");
});
test("maps currency-auto-refresh to frozen-at-export", async () => {
await writeSystemMd({ entitlementsOverride: ["currency-auto-refresh"] });
const features = await inventory();
expect(features.find((f) => f.feature === "currency-auto-refresh")?.verdict).toBe(
"frozen-at-export",
);
});
test("reads entitlements.generated.yaml opportunistically when present", async () => {
await writeSystemMd({});
await writeFile(
join(siteDir, "src", "entitlements.generated.yaml"),
stringifyYaml({ features: ["portal"] }) + "\n",
"utf8",
);
const features = await inventory();
expect(features.map((f) => f.feature)).toContain("portal");
});
test("returns empty inventory for a fully static site", async () => {
await writeSystemMd({});
const features = await inventory();
expect(features).toEqual([]);
});
test("renderFeatureNotes is non-empty with zero features", async () => {
const md = renderFeatureNotes({
systemId: "sys",
releaseId: "sys-r1",
exportedAt: "2026-09-21T00:00:00Z",
features: [],
});
expect(md.trim().length).toBeGreaterThan(0);
expect(md).toContain("fully static");
});
test("renderFeatureNotes emits a table row per feature", async () => {
const md = renderFeatureNotes({
systemId: "sys",
releaseId: "sys-r1",
exportedAt: "2026-09-21T00:00:00Z",
features: [
{ feature: "worker-serving", verdict: "needs-backend", detail: "Worker serving." },
{ feature: "currency-auto-refresh", verdict: "frozen-at-export", detail: "Frozen." },
],
});
expect(md).toContain("| `worker-serving` | needs-backend |");
expect(md).toContain("| `currency-auto-refresh` | frozen-at-export |");
});