238 lines
7.1 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|