371 lines
11 KiB
TypeScript
371 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { verifyRollbackReleaseCoherence } from "../../scripts/drill-runbook.ts";
|
|
import { verifyReleaseArtifactsCoherence } from "../../scripts/verify-release.ts";
|
|
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
|
|
import { computeContractSetDigest } from "../../src/contracts/contract-set-canonical.ts";
|
|
import type {
|
|
ReleaseArtifact,
|
|
RuntimeConfigArtifact,
|
|
} from "../../src/contracts/release-artifacts.ts";
|
|
import {
|
|
compareReleaseToRuntime,
|
|
RELEASE_TOKEN_REGISTRY,
|
|
} from "../../src/contracts/release-tokens.ts";
|
|
|
|
const contractPackages = [
|
|
{
|
|
packageId: "@example/accounts",
|
|
version: "1.2.3",
|
|
digest: `sha256:${"1".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "a".repeat(40),
|
|
},
|
|
{
|
|
packageId: "@example/billing",
|
|
version: "2.3.4",
|
|
digest: `sha256:${"2".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "b".repeat(40),
|
|
},
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
|
|
const releaseV1 = {
|
|
schemaVersion: 1,
|
|
appVersion: "1.0.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
configSchemaVersion: "1",
|
|
apiContractVersion: "1.4.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-08-01T00:00:00.000Z",
|
|
routeChunks: {},
|
|
} as const satisfies ReleaseArtifact;
|
|
|
|
const runtimeV1 = {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
CONFIG_SCHEMA_VERSION: "1",
|
|
API_CONTRACT_VERSION: "1.6.0",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
RELEASE_ID: "release-a",
|
|
} as const satisfies RuntimeConfigArtifact;
|
|
|
|
const runtimeV2 = {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
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",
|
|
},
|
|
} as const satisfies RuntimeConfigArtifact;
|
|
|
|
async function releaseV2With(
|
|
packages: readonly InstalledContractPackageIdentity[],
|
|
setDigest?: `sha256:${string}`,
|
|
): Promise<ReleaseArtifact> {
|
|
return {
|
|
schemaVersion: 2,
|
|
appVersion: "1.0.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
configSchemaVersion: "2.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-08-01T00:00:00.000Z",
|
|
routeChunks: {},
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: setDigest ?? (await computeContractSetDigest(packages)),
|
|
packages: [...packages],
|
|
},
|
|
};
|
|
}
|
|
|
|
function artifactReader(
|
|
entries: Readonly<Record<string, unknown | Error>>,
|
|
reads: string[] = [],
|
|
): (path: string) => Promise<unknown> {
|
|
return async (path) => {
|
|
reads.push(path);
|
|
const value = entries[path];
|
|
if (value === undefined) {
|
|
throw Object.assign(new Error(`missing ${path}`), { code: "ENOENT" });
|
|
}
|
|
if (value instanceof Error) throw value;
|
|
return value;
|
|
};
|
|
}
|
|
|
|
describe("release coherence", () => {
|
|
it("owns all nine release tokens and keeps builtAt diagnostic-only", () => {
|
|
// §5.2 adds contractSetDigest beside the legacy apiContractVersion scalar.
|
|
expect(Object.keys(RELEASE_TOKEN_REGISTRY)).toHaveLength(9);
|
|
expect(RELEASE_TOKEN_REGISTRY.contractSetDigest.compatibilityRole).toContain(
|
|
"multi-package",
|
|
);
|
|
expect(RELEASE_TOKEN_REGISTRY.builtAt.compatibilityRole).toContain(
|
|
"never cache identity",
|
|
);
|
|
});
|
|
|
|
it("compares the runtime config to the release structurally", () => {
|
|
const release = {
|
|
buildId: "build-a",
|
|
configSchemaVersion: "1.0",
|
|
apiContractVersion: "1.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
};
|
|
expect(
|
|
compareReleaseToRuntime(release, {
|
|
BUILD_ID: "build-a",
|
|
CONFIG_SCHEMA_VERSION: "1.2",
|
|
API_CONTRACT_VERSION: "1.1",
|
|
RELEASE_ID: "release-a",
|
|
}),
|
|
).toMatchObject({ compatible: true, mismatches: [] });
|
|
});
|
|
|
|
it("rejects HTML-only rollback against a newer runtime config", () => {
|
|
const oldRelease = {
|
|
buildId: "build-old",
|
|
configSchemaVersion: "1.0",
|
|
apiContractVersion: "1.0",
|
|
assetManifestHash: "assets-old",
|
|
releaseId: "release-old",
|
|
};
|
|
expect(
|
|
compareReleaseToRuntime(oldRelease, {
|
|
BUILD_ID: "build-new",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
API_CONTRACT_VERSION: "2.0",
|
|
RELEASE_ID: "release-new",
|
|
}),
|
|
).toMatchObject({
|
|
compatible: false,
|
|
mismatches: ["buildId", "configSchemaVersion", "apiContractVersion"],
|
|
});
|
|
});
|
|
|
|
it("gives release verification and rollback drills the same V1/V2 tamper verdicts", async () => {
|
|
const packageAdded = [
|
|
...contractPackages,
|
|
{
|
|
packageId: "@example/notifications",
|
|
version: "3.0.0",
|
|
digest: `sha256:${"3".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "c".repeat(40),
|
|
},
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const packageRemoved = contractPackages.slice(0, 1);
|
|
const versionChanged = [
|
|
{ ...contractPackages[0], version: "1.2.4" },
|
|
contractPackages[1],
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const packageDigestChanged = [
|
|
{
|
|
...contractPackages[0],
|
|
digest: `sha256:${"f".repeat(64)}`,
|
|
},
|
|
contractPackages[1],
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const exactV2 = await releaseV2With(contractPackages);
|
|
const matrix = [
|
|
{
|
|
name: "V1 scalar success",
|
|
release: releaseV1,
|
|
runtime: runtimeV1,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V1 scalar mismatch",
|
|
release: releaseV1,
|
|
runtime: { ...runtimeV1, API_CONTRACT_VERSION: "2.0.0" },
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 exact package set",
|
|
release: exactV2,
|
|
runtime: runtimeV2,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V2 exact package set in non-canonical manifest order",
|
|
release: await releaseV2With([...contractPackages].reverse()),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V2 package added",
|
|
release: await releaseV2With(packageAdded),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package removed",
|
|
release: await releaseV2With(packageRemoved),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package version tampered",
|
|
release: await releaseV2With(versionChanged),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package digest tampered",
|
|
release: await releaseV2With(packageDigestChanged),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 set digest tampered",
|
|
release: await releaseV2With(
|
|
contractPackages,
|
|
`sha256:${"0".repeat(64)}`,
|
|
),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
];
|
|
|
|
for (const fixture of matrix) {
|
|
const input = { release: fixture.release, runtime: fixture.runtime };
|
|
const verifierVerdict = (
|
|
await verifyReleaseArtifactsCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": input.release,
|
|
"dist/config.json": input.runtime,
|
|
}),
|
|
contractPackages,
|
|
})
|
|
).coherence.compatible;
|
|
const rollbackDrillVerdict = (
|
|
await verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": fixture.release,
|
|
"dist/config.json": fixture.runtime,
|
|
}),
|
|
contractPackages,
|
|
})
|
|
).coherence.compatible;
|
|
expect(verifierVerdict, `${fixture.name}: verifier`).toBe(
|
|
fixture.expectedCompatible,
|
|
);
|
|
expect(rollbackDrillVerdict, `${fixture.name}: rollback drill`).toBe(
|
|
fixture.expectedCompatible,
|
|
);
|
|
expect(rollbackDrillVerdict, `${fixture.name}: identical verdict`).toBe(
|
|
verifierVerdict,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("falls back to public rollback artifacts only when primary dist is absent", async () => {
|
|
const exactV2 = await releaseV2With(contractPackages);
|
|
const tamperedPrimary = await releaseV2With(contractPackages.slice(0, 1));
|
|
const validPublic = {
|
|
"public/release-manifest.json": exactV2,
|
|
"public/config.json": runtimeV2,
|
|
};
|
|
const unavailablePrimary = [
|
|
new SyntaxError("invalid primary JSON"),
|
|
Object.assign(new Error("unreadable primary"), { code: "EACCES" }),
|
|
{ ...exactV2, unexpected: "tampered" },
|
|
];
|
|
|
|
for (const primaryFailure of unavailablePrimary) {
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": primaryFailure,
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
}),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toBeDefined();
|
|
}
|
|
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": tamperedPrimary,
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
}),
|
|
contractPackages,
|
|
}),
|
|
).resolves.toMatchObject({ coherence: { compatible: false } });
|
|
|
|
const releaseOnlyMissingReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(
|
|
{
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
},
|
|
releaseOnlyMissingReads,
|
|
),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toThrow("primary rollback artifact pair");
|
|
expect(releaseOnlyMissingReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
]);
|
|
|
|
const runtimeOnlyMissingReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(
|
|
{
|
|
"dist/release-manifest.json": exactV2,
|
|
...validPublic,
|
|
},
|
|
runtimeOnlyMissingReads,
|
|
),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toThrow("primary rollback artifact pair");
|
|
expect(runtimeOnlyMissingReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
]);
|
|
|
|
const missingPairReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(validPublic, missingPairReads),
|
|
contractPackages,
|
|
}),
|
|
).resolves.toMatchObject({ coherence: { compatible: true } });
|
|
expect(missingPairReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
"public/release-manifest.json",
|
|
"public/config.json",
|
|
]);
|
|
});
|
|
});
|