601 lines
19 KiB
TypeScript
601 lines
19 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { verifyBuildManifestOutputs } from "../../scripts/lib/build-manifest-outputs.ts";
|
|
|
|
import {
|
|
dependencyInventoryArtifactSchema,
|
|
fieldWebVitalsArtifactSchema,
|
|
jsonSchemaDocumentArtifactSchema,
|
|
labPerformanceArtifactSchema,
|
|
parseBuildManifestArtifact,
|
|
parseReleaseArtifact,
|
|
parseRuntimeConfigArtifact,
|
|
projectReleaseTokens,
|
|
registrySnapshotArtifactSchema,
|
|
supplyChainVerificationArtifactSchema,
|
|
} from "../../scripts/contracts/release-artifacts.ts";
|
|
|
|
const EMPTY_CONTRACT_SET_DIGEST =
|
|
"sha256:ad6aab71fea6a9ff87cbd170b984b339965afc90d85bb57f87801c9e0c020da2";
|
|
|
|
const releaseV2 = {
|
|
schemaVersion: 2,
|
|
appVersion: "0.1.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
configSchemaVersion: "2.0",
|
|
assetManifestHash: "asset-hash",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-08-01T00:00:00.000Z",
|
|
routeChunks: { "route-home": "assets/home.js" },
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: EMPTY_CONTRACT_SET_DIGEST,
|
|
packages: [],
|
|
},
|
|
} as const;
|
|
|
|
const buildManifest = {
|
|
schemaVersion: 1,
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
releaseId: "release-a",
|
|
moduleInventoryHash: "inventory-hash",
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
buildContext: {
|
|
nodeVersion: "v24.14.0",
|
|
packageManagerVersion: "11.17.0",
|
|
runnerImage: "linux-x64",
|
|
sourceDateEpoch: null,
|
|
},
|
|
outputs: {
|
|
directory: "dist",
|
|
viteManifest: "dist/.vite/manifest.json",
|
|
moduleInventory: "artifacts/quality/vite-module-inventory.json",
|
|
routeChunks: { "route-home": "assets/home.js" },
|
|
runtimeConfigSchema: "dist/runtime-config.schema.json",
|
|
},
|
|
} as const;
|
|
|
|
describe("release artifact contracts", () => {
|
|
it("verifies confined build outputs and the raw module inventory bytes", async () => {
|
|
const moduleInventoryBytes = Buffer.from(
|
|
'{"schemaVersion":1,"chunks":[]}\n',
|
|
);
|
|
const files = new Map<string, Buffer>([
|
|
["/repo/dist/.vite/manifest.json", Buffer.from("{}\n")],
|
|
["/repo/artifacts/quality/vite-module-inventory.json", moduleInventoryBytes],
|
|
["/repo/dist/runtime-config.schema.json", Buffer.from("{}\n")],
|
|
["/repo/dist/assets/home.js", Buffer.from("chunk\n")],
|
|
]);
|
|
const manifest = {
|
|
...buildManifest,
|
|
moduleInventoryHash: createHash("sha256")
|
|
.update(moduleInventoryBytes)
|
|
.digest("hex"),
|
|
};
|
|
|
|
await expect(
|
|
verifyBuildManifestOutputs(manifest, {
|
|
repositoryRoot: "/repo",
|
|
readBytes: async (target) => files.get(target) ?? Promise.reject(Object.assign(new Error("missing"), { code: "ENOENT" })),
|
|
realpathPath: async (target) => target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
}),
|
|
).resolves.toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["missing", undefined, /moduleInventory:missing/u],
|
|
[
|
|
"tampered",
|
|
Buffer.from('{"schemaVersion":1,"chunks":[{"fileName":"other.js","modules":[]}]}\n'),
|
|
/moduleInventoryHash/u,
|
|
],
|
|
] as const)("rejects a %s module inventory", async (_name, bytes, expected) => {
|
|
const files = new Map<string, Buffer>([
|
|
["/repo/dist/.vite/manifest.json", Buffer.from("{}\n")],
|
|
["/repo/dist/runtime-config.schema.json", Buffer.from("{}\n")],
|
|
["/repo/dist/assets/home.js", Buffer.from("chunk\n")],
|
|
...(bytes ? [["/repo/artifacts/quality/vite-module-inventory.json", bytes] as const] : []),
|
|
]);
|
|
const mismatches = await verifyBuildManifestOutputs(buildManifest, {
|
|
repositoryRoot: "/repo",
|
|
readBytes: async (target) => files.get(target) ?? Promise.reject(Object.assign(new Error("missing"), { code: "ENOENT" })),
|
|
realpathPath: async (target) => target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
});
|
|
expect(mismatches.join("\n")).toMatch(expected);
|
|
});
|
|
|
|
it.each(["../outside", "/absolute", "dist\\escape"])(
|
|
"rejects unsafe build-manifest output path %s",
|
|
async (unsafePath) => {
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
outputs: { ...buildManifest.outputs, moduleInventory: unsafePath },
|
|
},
|
|
{
|
|
repositoryRoot: "/repo",
|
|
realpathPath: async (target) => target,
|
|
},
|
|
);
|
|
expect(mismatches).toContain("buildManifest:moduleInventory:path");
|
|
},
|
|
);
|
|
|
|
it("requires the canonical Vite manifest declared by the build writer", async () => {
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
outputs: {
|
|
...buildManifest.outputs,
|
|
viteManifest: "dist/.vite/tampered-manifest.json",
|
|
},
|
|
},
|
|
{ repositoryRoot: process.cwd() },
|
|
);
|
|
expect(mismatches).toContain("buildManifest:viteManifest:path");
|
|
});
|
|
|
|
it("rejects a realpath escape from a declared build output", async () => {
|
|
const mismatches = await verifyBuildManifestOutputs(buildManifest, {
|
|
repositoryRoot: "/repo",
|
|
realpathPath: async (target) =>
|
|
target.endsWith("vite-module-inventory.json") ? "/outside/inventory.json" : target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
readBytes: async () => Buffer.from("inventory\n"),
|
|
});
|
|
expect(mismatches).toContain("buildManifest:moduleInventory:path");
|
|
});
|
|
|
|
it("rejects a nested symlink that resolves inside the repository but outside dist", async () => {
|
|
const moduleInventoryBytes = Buffer.from(
|
|
'{"schemaVersion":1,"chunks":[]}\n',
|
|
);
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
moduleInventoryHash: createHash("sha256")
|
|
.update(moduleInventoryBytes)
|
|
.digest("hex"),
|
|
},
|
|
{
|
|
repositoryRoot: "/repo",
|
|
realpathPath: async (target) =>
|
|
target === "/repo/dist/assets/home.js"
|
|
? "/repo/src/home.js"
|
|
: target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
readBytes: async () => moduleInventoryBytes,
|
|
},
|
|
);
|
|
expect(mismatches).toContain("buildManifest:routeChunk:route-home:path");
|
|
});
|
|
|
|
it("allows a legal dist child whose name begins with two dots", async () => {
|
|
const moduleInventoryBytes = Buffer.from(
|
|
'{"schemaVersion":1,"chunks":[]}\n',
|
|
);
|
|
const files = new Map<string, Buffer>([
|
|
["/repo/dist/.vite/manifest.json", Buffer.from("{}\n")],
|
|
["/repo/artifacts/quality/vite-module-inventory.json", moduleInventoryBytes],
|
|
["/repo/dist/runtime-config.schema.json", Buffer.from("{}\n")],
|
|
["/repo/dist/..assets/home.js", Buffer.from("chunk\n")],
|
|
]);
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
moduleInventoryHash: createHash("sha256")
|
|
.update(moduleInventoryBytes)
|
|
.digest("hex"),
|
|
outputs: {
|
|
...buildManifest.outputs,
|
|
routeChunks: { "route-home": "..assets/home.js" },
|
|
},
|
|
},
|
|
{
|
|
repositoryRoot: "/repo",
|
|
readBytes: async (target) =>
|
|
files.get(target) ?? Promise.reject(new Error("missing")),
|
|
realpathPath: async (target) => target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
},
|
|
);
|
|
expect(mismatches).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["viteManifest", "package.json"],
|
|
["runtimeConfigSchema", "schemas/artifacts/build-manifest.schema.json"],
|
|
["moduleInventory", "package.json"],
|
|
] as const)("rejects %s outside its approved output root", async (field, value) => {
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
outputs: { ...buildManifest.outputs, [field]: value },
|
|
},
|
|
{ repositoryRoot: process.cwd() },
|
|
);
|
|
expect(mismatches).toContain(`buildManifest:${field}:path`);
|
|
});
|
|
|
|
it("rejects a parse-invalid module inventory even when its raw hash matches", async () => {
|
|
const bytes = Buffer.from("{}\n");
|
|
const mismatches = await verifyBuildManifestOutputs(
|
|
{
|
|
...buildManifest,
|
|
moduleInventoryHash: createHash("sha256").update(bytes).digest("hex"),
|
|
},
|
|
{
|
|
repositoryRoot: "/repo",
|
|
readBytes: async () => bytes,
|
|
realpathPath: async (target) => target,
|
|
assertRegularFile: async () => undefined,
|
|
assertDirectory: async () => undefined,
|
|
},
|
|
);
|
|
expect(mismatches).toContain("buildManifest:moduleInventory:invalid");
|
|
});
|
|
|
|
it("projects the nested V2 contract-set digest without a legacy scalar", () => {
|
|
const release = parseReleaseArtifact(releaseV2);
|
|
|
|
expect(projectReleaseTokens(release)).toMatchObject({
|
|
schemaVersion: 2,
|
|
buildId: "build-a",
|
|
contractSetDigest: EMPTY_CONTRACT_SET_DIGEST,
|
|
});
|
|
expect(projectReleaseTokens(release)).not.toHaveProperty(
|
|
"apiContractVersion",
|
|
);
|
|
});
|
|
|
|
it("projects the legacy scalar only for V1", () => {
|
|
const { contractSet: _contractSet, ...releaseWithoutContractSet } = releaseV2;
|
|
void _contractSet;
|
|
const release = parseReleaseArtifact({
|
|
...releaseWithoutContractSet,
|
|
schemaVersion: 1,
|
|
configSchemaVersion: "1",
|
|
apiContractVersion: "1.4.0",
|
|
});
|
|
|
|
expect(projectReleaseTokens(release)).toMatchObject({
|
|
schemaVersion: 1,
|
|
apiContractVersion: "1.4.0",
|
|
});
|
|
expect(projectReleaseTokens(release)).not.toHaveProperty(
|
|
"contractSetDigest",
|
|
);
|
|
});
|
|
|
|
it("rejects a V2 release carrying the removed scalar", () => {
|
|
expect(() =>
|
|
parseReleaseArtifact({
|
|
...releaseV2,
|
|
apiContractVersion: "1",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("accepts the exact build manifest emitted by the generator", () => {
|
|
expect(parseBuildManifestArtifact(buildManifest)).toEqual(buildManifest);
|
|
});
|
|
|
|
it("rejects unknown build manifest output fields", () => {
|
|
expect(() =>
|
|
parseBuildManifestArtifact({
|
|
...buildManifest,
|
|
outputs: { ...buildManifest.outputs, unexpected: "value" },
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("accepts Runtime Config V2 without a legacy API scalar", () => {
|
|
expect(
|
|
parseRuntimeConfigArtifact({
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "demo",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
RELEASE_ID: "release-a",
|
|
CAPABILITY_OVERRIDES: {
|
|
REALTIME: "DEFAULT",
|
|
WEB_WORKER: "DEFAULT",
|
|
SERVICE_WORKER: "DEFAULT",
|
|
OFFLINE_COMMANDS: "DEFAULT",
|
|
},
|
|
}),
|
|
).not.toHaveProperty("API_CONTRACT_VERSION");
|
|
});
|
|
|
|
it("enforces dependency inventory counts at the executable writer boundary", () => {
|
|
const inventory = {
|
|
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: [],
|
|
},
|
|
],
|
|
} as const;
|
|
|
|
expect(dependencyInventoryArtifactSchema.parse(inventory)).toEqual(
|
|
inventory,
|
|
);
|
|
expect(() =>
|
|
dependencyInventoryArtifactSchema.parse({
|
|
...inventory,
|
|
dependencyCount: 2,
|
|
}),
|
|
).toThrow();
|
|
expect(() =>
|
|
dependencyInventoryArtifactSchema.parse({
|
|
...inventory,
|
|
dependencyCount: 0,
|
|
directDependencyCount: 0,
|
|
dependencies: [],
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects undeclared supply-chain verification evidence", () => {
|
|
const verification = {
|
|
schemaVersion: 1,
|
|
localStatus: "PASS",
|
|
promotionStatus: "FAIL_UNVERIFIED",
|
|
lockfileSha256: "a".repeat(64),
|
|
sourceSetSha256: "b".repeat(64),
|
|
distSha256: "c".repeat(64),
|
|
sbomSha256: "d".repeat(64),
|
|
dependencyDiff: {
|
|
added: [],
|
|
removed: [],
|
|
changed: [],
|
|
upgrades: [],
|
|
},
|
|
highRiskReview: [],
|
|
vulnerabilityStatus: "FAIL_UNVERIFIED",
|
|
provenanceAttestationStatus: "FAIL_UNVERIFIED",
|
|
failures: [],
|
|
} as const;
|
|
|
|
expect(supplyChainVerificationArtifactSchema.parse(verification)).toEqual(
|
|
verification,
|
|
);
|
|
expect(() =>
|
|
supplyChainVerificationArtifactSchema.parse({
|
|
...verification,
|
|
undocumented: true,
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("preserves no-baseline and failure registry evidence", () => {
|
|
const failureEvidence = {
|
|
schemaVersion: 2,
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
baselineDigest: null,
|
|
currentDigest: "a".repeat(64),
|
|
compatibility: { impact: "not-evaluated", changes: [] },
|
|
failures: ["missing registry source"],
|
|
registries: [],
|
|
} as const;
|
|
|
|
expect(registrySnapshotArtifactSchema.parse(failureEvidence)).toEqual(
|
|
failureEvidence,
|
|
);
|
|
});
|
|
|
|
it("requires the complete repository registry set for success evidence", () => {
|
|
const successfulEvidence = {
|
|
schemaVersion: 2,
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
baselineDigest: null,
|
|
currentDigest: "a".repeat(64),
|
|
compatibility: { impact: "not-evaluated", changes: [] },
|
|
failures: [],
|
|
registries: Array.from({ length: 11 }, (_, index) => ({
|
|
registryId: `registry-${index}`,
|
|
owner: "platform",
|
|
source: `src/registry-${index}.ts`,
|
|
rowCount: 0,
|
|
contract: {},
|
|
rows: {},
|
|
})),
|
|
} as const;
|
|
|
|
expect(registrySnapshotArtifactSchema.parse(successfulEvidence)).toEqual(
|
|
successfulEvidence,
|
|
);
|
|
expect(() =>
|
|
registrySnapshotArtifactSchema.parse({
|
|
...successfulEvidence,
|
|
registries: [],
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("preserves unverified field evidence when no samples are eligible", () => {
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
window: {
|
|
days: 28,
|
|
start: "2026-07-04T00:00:00.000Z",
|
|
end: "2026-08-01T00:00:00.000Z",
|
|
},
|
|
context: {
|
|
source: "config/performance/field-input.example.json",
|
|
sourceSystem: null,
|
|
exportId: null,
|
|
network: "production-real-user",
|
|
routeAggregation: "route-id-only",
|
|
releaseId: null,
|
|
privacyApprovalRef: null,
|
|
thresholdDecisionRef: null,
|
|
validationFailures: ["input: invalid evidence"],
|
|
},
|
|
metrics: { p75LcpMs: null, p75Cls: null, p75InpMs: null },
|
|
thresholds: {
|
|
p75LcpMs: 2_500,
|
|
p75Cls: 0.1,
|
|
p75InpMs: 200,
|
|
minimumEligibleSamples: null,
|
|
},
|
|
eligibility: {
|
|
consentRequired: true,
|
|
totalSamples: 0,
|
|
eligibleSamples: 0,
|
|
minimumEligibleSamples: null,
|
|
routeSamples: {},
|
|
},
|
|
status: "FAIL_UNVERIFIED",
|
|
passed: false,
|
|
} as const;
|
|
|
|
expect(fieldWebVitalsArtifactSchema.parse(report)).toEqual(report);
|
|
});
|
|
|
|
it("preserves verified field evidence that fails an approved threshold", () => {
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
window: {
|
|
days: 28,
|
|
start: "2026-07-04T00:00:00.000Z",
|
|
end: "2026-08-01T00:00:00.000Z",
|
|
},
|
|
context: {
|
|
source: "provider.json",
|
|
sourceSystem: "provider",
|
|
exportId: "export-1",
|
|
network: "production-real-user",
|
|
routeAggregation: "route-id-only",
|
|
releaseId: "release-1",
|
|
privacyApprovalRef: "privacy-1",
|
|
thresholdDecisionRef: "decision-1",
|
|
validationFailures: [],
|
|
},
|
|
metrics: { p75LcpMs: 2_501, p75Cls: 0.1, p75InpMs: 200 },
|
|
thresholds: {
|
|
p75LcpMs: 2_500,
|
|
p75Cls: 0.1,
|
|
p75InpMs: 200,
|
|
minimumEligibleSamples: 1,
|
|
},
|
|
eligibility: {
|
|
consentRequired: true,
|
|
totalSamples: 1,
|
|
eligibleSamples: 1,
|
|
minimumEligibleSamples: 1,
|
|
routeSamples: { APP_HOME: 1 },
|
|
},
|
|
status: "FAIL_THRESHOLD",
|
|
passed: false,
|
|
} as const;
|
|
|
|
expect(fieldWebVitalsArtifactSchema.parse(report)).toEqual(report);
|
|
});
|
|
|
|
it("accepts the lab writer's nested browser context as JSON evidence", () => {
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: "2026-08-01T00:00:00.000Z",
|
|
context: {
|
|
runner: { platform: "linux", architecture: "x64", nodeVersion: "v24" },
|
|
browser: { name: "chromium", version: "140" },
|
|
viewport: { width: 1280, height: 720 },
|
|
network: {
|
|
profile: "contract-fast-4g",
|
|
latencyMs: 40,
|
|
downloadBytesPerSecond: 200_000,
|
|
uploadBytesPerSecond: 93_750,
|
|
},
|
|
cpu: { throttlingRate: 4 },
|
|
cache: { state: "cold", isolation: "new-browser-context" },
|
|
build: { buildId: "build-1", releaseId: "release-1" },
|
|
},
|
|
metrics: { lcpMs: 1_000, cls: 0.01, namedInteractionMs: 100 },
|
|
thresholds: { lcpMs: 2_500, cls: 0.1, namedInteractionMs: 200 },
|
|
fixtures: [
|
|
{ name: "missing-context", passed: true },
|
|
{ name: "lcp-over-threshold", passed: true },
|
|
],
|
|
passed: true,
|
|
} as const;
|
|
|
|
expect(labPerformanceArtifactSchema.parse(report)).toEqual(report);
|
|
});
|
|
|
|
it("rejects non-JSON values in generated schema documents", () => {
|
|
expect(() =>
|
|
jsonSchemaDocumentArtifactSchema.parse({
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
invalid: () => undefined,
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("keeps machine-readable evidence publishers on the validated writer", async () => {
|
|
const writerFiles = [
|
|
"scripts/generate-build-manifest.ts",
|
|
"scripts/generate-supply-chain.ts",
|
|
"scripts/collect-web-vitals-evidence.ts",
|
|
"scripts/test-performance.ts",
|
|
"scripts/verify-release.ts",
|
|
"scripts/drill-runbook.ts",
|
|
"scripts/check-registries.ts",
|
|
] as const;
|
|
const sources = await Promise.all(
|
|
writerFiles.map(async (file) => ({
|
|
file,
|
|
source: await readFile(file, "utf8"),
|
|
})),
|
|
);
|
|
|
|
for (const { file, source } of sources) {
|
|
const directWrites = source.match(/\bwriteFile\s*\(/gu) ?? [];
|
|
if (file === "scripts/generate-supply-chain.ts") {
|
|
expect(directWrites, file).toHaveLength(1);
|
|
expect(source, file).toMatch(
|
|
/writeFile\(\s*["']artifacts\/release\/checksums\.txt["']/u,
|
|
);
|
|
} else {
|
|
expect(directWrites, file).toHaveLength(0);
|
|
}
|
|
}
|
|
|
|
expect(
|
|
sources.flatMap(({ source }) =>
|
|
source.match(/\bwriteValidatedJsonArtifact\s*\(/gu) ?? [],
|
|
),
|
|
).toHaveLength(19);
|
|
});
|
|
});
|