Files

238 lines
7.1 KiB
TypeScript

import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { generateArtifactSchemas } from "../../scripts/generate-artifact-schemas.ts";
import { assertMatchesJsonSchema } from "../../scripts/lib/json-schema.ts";
const temporaryDirectories: string[] = [];
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(path.join(tmpdir(), "artifact-schemas-"));
temporaryDirectories.push(directory);
return directory;
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { force: true, recursive: true }),
),
);
});
async function json(path: string): Promise<unknown> {
return JSON.parse(await readFile(path, "utf8")) as unknown;
}
describe("checked-in JSON Schema execution", () => {
it("accepts a build manifest and rejects undeclared output fields", async () => {
const schema = await json("schemas/artifacts/build-manifest.schema.json");
const manifest = {
schemaVersion: 1,
buildId: "build-1",
commitSha: "commit-1",
releaseId: "release-1",
moduleInventoryHash: "inventory-hash",
generatedAt: "2026-08-01T00:00:00.000Z",
buildContext: {
nodeVersion: "v24.11.0",
packageManagerVersion: "11.17.0",
runnerImage: "test-runner",
sourceDateEpoch: null,
},
outputs: {
directory: "dist",
viteManifest: "dist/.vite/manifest.json",
moduleInventory: "artifacts/quality/vite-module-inventory.json",
routeChunks: { home: "assets/home.js" },
runtimeConfigSchema: "dist/runtime-config.schema.json",
},
};
expect(() =>
assertMatchesJsonSchema(schema, manifest, "build manifest"),
).not.toThrow();
expect(() =>
assertMatchesJsonSchema(
schema,
{ ...manifest, undocumented: true },
"build manifest",
),
).toThrow(/checked-in JSON Schema/u);
});
it("resolves local schema definitions in the recipe catalog", async () => {
const [schema, catalog] = await Promise.all([
json("schemas/config/frontend-capability-recipes.schema.json"),
json("config/recipes/frontend-capability-recipes.json"),
]);
expect(() =>
assertMatchesJsonSchema(schema, catalog, "optional recipe catalog"),
).not.toThrow();
});
it("generates deterministic draft 2020-12 artifact schemas and detects drift", async () => {
const root = await temporaryDirectory();
await generateArtifactSchemas({ root });
const generatedPath = path.join(
root,
"schemas/artifacts/dependency-inventory.schema.json",
);
const generated = await readFile(generatedPath, "utf8");
expect(generated.endsWith("\n")).toBe(true);
expect(JSON.parse(generated)).toMatchObject({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
additionalProperties: false,
});
await generateArtifactSchemas({ root, check: true });
await writeFile(generatedPath, `${generated.trimEnd()} \n`, "utf8");
await expect(
generateArtifactSchemas({ root, check: true }),
).rejects.toThrow(/dependency-inventory\.schema\.json/u);
});
it.each([
{
name: "dependency inventory",
path: "schemas/artifacts/dependency-inventory.schema.json",
value: {
schemaVersion: 2,
packageManager: "pnpm@11.17.0",
lockfileSha256: "a".repeat(64),
dependencyCount: 1,
directDependencyCount: 1,
dependencies: [
{
name: "zod",
version: "4.4.3",
direct: true,
scope: "production",
optional: false,
license: "MIT",
integrity: `sha512-${"a".repeat(86)}`,
dependencies: [],
},
],
},
},
{
name: "registry snapshot",
path: "schemas/artifacts/registry-snapshot.schema.json",
value: {
schemaVersion: 2,
generatedAt: "2026-08-01T00:00:00.000Z",
baselineDigest: "b".repeat(64),
currentDigest: "c".repeat(64),
compatibility: { impact: "none", changes: [] },
failures: [],
registries: Array.from({ length: 11 }, (_, index) => ({
registryId: `registry-${index}`,
owner: "platform",
source: `src/registry-${index}.ts`,
rowCount: 0,
contract: {},
rows: {},
})),
},
},
{
name: "supply-chain verification",
path: "schemas/artifacts/supply-chain-verification.schema.json",
value: {
schemaVersion: 1,
localStatus: "PASS",
promotionStatus: "FAIL_UNVERIFIED",
lockfileSha256: "d".repeat(64),
sourceSetSha256: "e".repeat(64),
distSha256: "f".repeat(64),
sbomSha256: "0".repeat(64),
dependencyDiff: {
added: [],
removed: [],
changed: [],
upgrades: [],
},
highRiskReview: [],
vulnerabilityStatus: "FAIL_UNVERIFIED",
provenanceAttestationStatus: "FAIL_UNVERIFIED",
failures: [],
},
},
])("accepts $name and rejects undeclared top-level fields", async (fixture) => {
const schema = await json(fixture.path);
expect(() =>
assertMatchesJsonSchema(schema, fixture.value, fixture.name),
).not.toThrow();
expect(() =>
assertMatchesJsonSchema(
schema,
{ ...fixture.value, undocumented: true },
fixture.name,
),
).toThrow(/checked-in JSON Schema/u);
if (fixture.name === "dependency inventory") {
const dependency = fixture.value.dependencies?.[0];
if (dependency === undefined) {
throw new TypeError("dependency inventory fixture is incomplete");
}
expect(() =>
assertMatchesJsonSchema(
schema,
{
...fixture.value,
dependencies: [
{
...dependency,
undocumented: true,
},
],
},
fixture.name,
),
).toThrow(/checked-in JSON Schema/u);
expect(() =>
assertMatchesJsonSchema(
schema,
{
...fixture.value,
dependencyCount: 0,
directDependencyCount: 0,
dependencies: [],
},
fixture.name,
),
).toThrow(/checked-in JSON Schema/u);
}
if (fixture.name === "registry snapshot") {
expect(() =>
assertMatchesJsonSchema(
schema,
{ ...fixture.value, failures: [], registries: [] },
fixture.name,
),
).toThrow(/checked-in JSON Schema/u);
expect(() =>
assertMatchesJsonSchema(
schema,
{
...fixture.value,
baselineDigest: null,
compatibility: { impact: "not-evaluated", changes: [] },
failures: ["missing registry source"],
registries: [],
},
fixture.name,
),
).not.toThrow();
}
});
});