fix: unify release runtime coherence verification
This commit is contained in:
+110
-70
@@ -1,4 +1,5 @@
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
|
||||
import { createTelemetryAdapter } from "../src/adapters/telemetry/best-effort-telemetry.ts";
|
||||
@@ -6,8 +7,17 @@ import { verifyCompatibilityTuple } from "../src/application/policies/compatibil
|
||||
import type { StoragePort } from "../src/application/ports/storage-port.ts";
|
||||
import { decideChunkRecovery } from "../src/application/use-cases/decide-chunk-recovery.ts";
|
||||
import { validateRuntimeConfig } from "../src/bootstrap/runtime-config-schema.ts";
|
||||
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.ts";
|
||||
import {
|
||||
parseReleaseArtifact,
|
||||
parseRuntimeConfigArtifact,
|
||||
type ReleaseArtifact,
|
||||
} from "../src/contracts/release-artifacts.ts";
|
||||
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
|
||||
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
|
||||
import {
|
||||
verifyReleaseRuntimeCoherence,
|
||||
type ReleaseRuntimeCoherenceInput,
|
||||
} from "./lib/release-runtime-coherence.ts";
|
||||
|
||||
type RecoveryAssertion = Readonly<{
|
||||
assertion: string;
|
||||
@@ -38,35 +48,15 @@ type RunbookDocument = Readonly<{
|
||||
runbooks: Record<string, RunbookSpecification>;
|
||||
}>;
|
||||
|
||||
type ReleaseManifest = Record<string, unknown> & {
|
||||
buildId: string;
|
||||
configSchemaVersion: string;
|
||||
apiContractVersion: string;
|
||||
assetManifestHash: string;
|
||||
releaseId: string;
|
||||
};
|
||||
|
||||
const runbookId = process.argv
|
||||
.slice(2)
|
||||
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
|
||||
const document = JSON.parse(
|
||||
await readFile("config/runbooks/runbooks.json", "utf8"),
|
||||
) as RunbookDocument;
|
||||
const specification = runbookId ? document.runbooks[runbookId] : undefined;
|
||||
if (!runbookId || !specification) {
|
||||
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
async function releaseManifest(): Promise<ReleaseManifest> {
|
||||
async function releaseManifest(): Promise<ReleaseArtifact> {
|
||||
for (const candidate of [
|
||||
"dist/release-manifest.json",
|
||||
"public/release-manifest.json",
|
||||
]) {
|
||||
try {
|
||||
return JSON.parse(
|
||||
await readFile(candidate, "utf8"),
|
||||
) as ReleaseManifest;
|
||||
return parseReleaseArtifact(
|
||||
JSON.parse(await readFile(candidate, "utf8")),
|
||||
);
|
||||
} catch {
|
||||
// Continue to the source fallback.
|
||||
}
|
||||
@@ -238,15 +228,29 @@ async function drillTelemetry(): Promise<DrillResult> {
|
||||
|
||||
async function drillRollback(): Promise<DrillResult> {
|
||||
const release = await releaseManifest();
|
||||
const runtime = JSON.parse(
|
||||
await readFile(
|
||||
(await access("dist/config.json").then(() => true).catch(() => false))
|
||||
? "dist/config.json"
|
||||
: "public/config.json",
|
||||
"utf8",
|
||||
const runtimeArtifact = parseRuntimeConfigArtifact(
|
||||
JSON.parse(
|
||||
await readFile(
|
||||
(await access("dist/config.json").then(() => true).catch(() => false))
|
||||
? "dist/config.json"
|
||||
: "public/config.json",
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
);
|
||||
const coherent = compareReleaseToRuntime(release, runtime);
|
||||
const runtime = {
|
||||
...runtimeArtifact,
|
||||
BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"),
|
||||
RELEASE_ID: requireIdentity(
|
||||
runtimeArtifact.RELEASE_ID,
|
||||
"runtime RELEASE_ID",
|
||||
),
|
||||
};
|
||||
const coherent = await verifyRollbackReleaseCoherence({
|
||||
release,
|
||||
runtime,
|
||||
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
|
||||
});
|
||||
const mixed = verifyCompatibilityTuple({
|
||||
frontend: {
|
||||
buildId: "build-a",
|
||||
@@ -284,42 +288,78 @@ const drillById: Record<string, () => Promise<DrillResult>> = {
|
||||
"FE-RB-004": drillTelemetry,
|
||||
"FE-RB-005": drillRollback,
|
||||
};
|
||||
const drill = await drillById[runbookId]();
|
||||
const escalationPathAsserted = specification.escalation.length >= 2;
|
||||
const passed =
|
||||
drill.triggerAsserted &&
|
||||
drill.containmentAsserted &&
|
||||
escalationPathAsserted &&
|
||||
drill.recoveryAssertions.every((item) => item.passed) &&
|
||||
drill.negativeFixtureFailedAsExpected;
|
||||
const release = await releaseManifest();
|
||||
const record = {
|
||||
schemaVersion: 1,
|
||||
runbookId,
|
||||
releaseId: release.releaseId,
|
||||
drillTimestamp: new Date().toISOString(),
|
||||
triggerInjected: specification.triggerKinds[0],
|
||||
triggerAsserted: drill.triggerAsserted,
|
||||
containmentAsserted: drill.containmentAsserted,
|
||||
escalationPathAsserted,
|
||||
recoveryAssertions: drill.recoveryAssertions,
|
||||
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
|
||||
windowObservedBucket: specification.window,
|
||||
providerVerificationRequired: drill.providerVerificationRequired,
|
||||
passed,
|
||||
};
|
||||
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
|
||||
await mkdir(artifactDirectory, { recursive: true });
|
||||
await writeFile(
|
||||
`${artifactDirectory}/record.json`,
|
||||
`${JSON.stringify(record, null, 2)}\n`,
|
||||
);
|
||||
if (!passed) {
|
||||
process.stderr.write(`${runbookId} drill failed.\n`);
|
||||
process.exit(1);
|
||||
|
||||
export async function verifyRollbackReleaseCoherence(
|
||||
input: ReleaseRuntimeCoherenceInput,
|
||||
) {
|
||||
return verifyReleaseRuntimeCoherence(input);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const runbookId = process.argv
|
||||
.slice(2)
|
||||
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
|
||||
const document = JSON.parse(
|
||||
await readFile("config/runbooks/runbooks.json", "utf8"),
|
||||
) as RunbookDocument;
|
||||
const specification = runbookId ? document.runbooks[runbookId] : undefined;
|
||||
if (!runbookId || !specification) {
|
||||
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const drill = await drillById[runbookId]();
|
||||
const escalationPathAsserted = specification.escalation.length >= 2;
|
||||
const passed =
|
||||
drill.triggerAsserted &&
|
||||
drill.containmentAsserted &&
|
||||
escalationPathAsserted &&
|
||||
drill.recoveryAssertions.every((item) => item.passed) &&
|
||||
drill.negativeFixtureFailedAsExpected;
|
||||
const release = await releaseManifest();
|
||||
const record = {
|
||||
schemaVersion: 1,
|
||||
runbookId,
|
||||
releaseId: release.releaseId,
|
||||
drillTimestamp: new Date().toISOString(),
|
||||
triggerInjected: specification.triggerKinds[0],
|
||||
triggerAsserted: drill.triggerAsserted,
|
||||
containmentAsserted: drill.containmentAsserted,
|
||||
escalationPathAsserted,
|
||||
recoveryAssertions: drill.recoveryAssertions,
|
||||
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
|
||||
windowObservedBucket: specification.window,
|
||||
providerVerificationRequired: drill.providerVerificationRequired,
|
||||
passed,
|
||||
};
|
||||
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
|
||||
await mkdir(artifactDirectory, { recursive: true });
|
||||
await writeFile(
|
||||
`${artifactDirectory}/record.json`,
|
||||
`${JSON.stringify(record, null, 2)}\n`,
|
||||
);
|
||||
if (!passed) {
|
||||
process.stderr.write(`${runbookId} drill failed.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
|
||||
drill.providerVerificationRequired ? "still required" : "not required"
|
||||
})\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function requireIdentity(value: string | undefined, label: string): string {
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new TypeError(`${label} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1];
|
||||
if (
|
||||
invokedPath !== undefined &&
|
||||
import.meta.url === pathToFileURL(invokedPath).href
|
||||
) {
|
||||
await main();
|
||||
}
|
||||
process.stdout.write(
|
||||
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
|
||||
drill.providerVerificationRequired ? "still required" : "not required"
|
||||
})\n`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { isVersionCompatible } from "../../src/application/policies/compatibility.ts";
|
||||
import { verifyContractSet } from "../../src/contracts/contract-set.ts";
|
||||
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
|
||||
import type { ReleaseArtifact } from "../../src/contracts/release-artifacts.ts";
|
||||
import { compareReleaseToRuntime } from "../../src/contracts/release-tokens.ts";
|
||||
|
||||
export type ReleaseRuntimeCoherenceInput = Readonly<{
|
||||
release: ReleaseArtifact;
|
||||
runtime: Readonly<{
|
||||
BUILD_ID: string;
|
||||
CONFIG_SCHEMA_VERSION: string;
|
||||
API_CONTRACT_VERSION?: string;
|
||||
RELEASE_ID: string;
|
||||
}>;
|
||||
contractPackages: readonly InstalledContractPackageIdentity[];
|
||||
}>;
|
||||
|
||||
export type ReleaseRuntimeCoherence = Readonly<{
|
||||
compatible: boolean;
|
||||
mismatches: readonly string[];
|
||||
warnings: readonly string[];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Verifies the runtime identity using the release schema's own contract model.
|
||||
* V1 retains the scalar compatibility policy. V2 has no scalar projection: its
|
||||
* identity is the exact compiled package tuple set and canonical set digest.
|
||||
*/
|
||||
export async function verifyReleaseRuntimeCoherence(
|
||||
input: ReleaseRuntimeCoherenceInput,
|
||||
): Promise<ReleaseRuntimeCoherence> {
|
||||
if (input.release.schemaVersion === 1) {
|
||||
if (input.runtime.API_CONTRACT_VERSION === undefined) {
|
||||
return compareWithoutContractScalar(input, ["apiContractVersion"]);
|
||||
}
|
||||
return compareReleaseToRuntime(input.release, {
|
||||
...input.runtime,
|
||||
API_CONTRACT_VERSION: input.runtime.API_CONTRACT_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
const comparison = compareWithoutContractScalar(input);
|
||||
const contractSet = await verifyContractSet({
|
||||
expected: input.contractPackages,
|
||||
manifest: input.release.contractSet,
|
||||
});
|
||||
if (contractSet.ok) return comparison;
|
||||
|
||||
const mismatches = Object.freeze([
|
||||
...comparison.mismatches,
|
||||
contractSet.code,
|
||||
]);
|
||||
return Object.freeze({
|
||||
compatible: false,
|
||||
mismatches,
|
||||
warnings: comparison.warnings,
|
||||
});
|
||||
}
|
||||
|
||||
function compareWithoutContractScalar(
|
||||
input: Pick<ReleaseRuntimeCoherenceInput, "release" | "runtime">,
|
||||
initialMismatches: readonly string[] = [],
|
||||
): ReleaseRuntimeCoherence {
|
||||
const mismatches = [...initialMismatches];
|
||||
if (input.release.buildId !== input.runtime.BUILD_ID) {
|
||||
mismatches.push("buildId");
|
||||
}
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
input.release.configSchemaVersion,
|
||||
input.runtime.CONFIG_SCHEMA_VERSION,
|
||||
)
|
||||
) {
|
||||
mismatches.push("configSchemaVersion");
|
||||
}
|
||||
const warnings =
|
||||
input.release.releaseId === input.runtime.RELEASE_ID
|
||||
? []
|
||||
: ["releaseId"];
|
||||
return Object.freeze({
|
||||
compatible: mismatches.length === 0,
|
||||
mismatches: Object.freeze(mismatches),
|
||||
warnings: Object.freeze(warnings),
|
||||
});
|
||||
}
|
||||
@@ -5,9 +5,6 @@ import {
|
||||
verifyCompatibilityTuple,
|
||||
type CompatibilityTuple,
|
||||
} from "../src/application/policies/compatibility.ts";
|
||||
import {
|
||||
compareReleaseToRuntime,
|
||||
} from "../src/contracts/release-tokens.ts";
|
||||
import {
|
||||
parseBuildManifestArtifact,
|
||||
parseReleaseArtifact,
|
||||
@@ -17,13 +14,13 @@ import {
|
||||
type ReleaseArtifact,
|
||||
type RuntimeConfigArtifact,
|
||||
} from "../src/contracts/release-artifacts.ts";
|
||||
import { verifyContractSet } from "../src/contracts/contract-set.ts";
|
||||
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
|
||||
import {
|
||||
ROUTE_REGISTRY,
|
||||
ROUTE_RUNTIME_CONTRACT,
|
||||
} from "../src/features/installed-feature-contracts.ts";
|
||||
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts";
|
||||
|
||||
type CoherenceFixture = Readonly<{
|
||||
name: string;
|
||||
@@ -71,22 +68,17 @@ const actualAssetManifestHash = createHash("sha256")
|
||||
.update(viteManifest)
|
||||
.digest("hex");
|
||||
|
||||
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
|
||||
const artifactComparison = await verifyReleaseRuntimeCoherence({
|
||||
release,
|
||||
runtime: runtimeConfig,
|
||||
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
|
||||
});
|
||||
const artifactMismatches: string[] = [...artifactComparison.mismatches];
|
||||
for (const [token, value] of Object.entries(projectReleaseTokens(release))) {
|
||||
if (token !== "schemaVersion" && (typeof value !== "string" || value.length === 0)) {
|
||||
artifactMismatches.push(`releaseToken:${token}`);
|
||||
}
|
||||
}
|
||||
if (release.schemaVersion === 2) {
|
||||
const contractSetVerification = await verifyContractSet({
|
||||
expected: EXPECTED_CONTRACT_SET_PACKAGES,
|
||||
manifest: release.contractSet,
|
||||
});
|
||||
if (!contractSetVerification.ok) {
|
||||
artifactMismatches.push(contractSetVerification.code);
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof release.builtAt !== "string" ||
|
||||
!Number.isFinite(Date.parse(release.builtAt))
|
||||
|
||||
@@ -42,33 +42,23 @@ export function compareReleaseToRuntime(
|
||||
release: Readonly<{
|
||||
buildId: string;
|
||||
configSchemaVersion: string;
|
||||
apiContractVersion?: string;
|
||||
apiContractVersion: string;
|
||||
assetManifestHash: string;
|
||||
releaseId: string;
|
||||
}>,
|
||||
runtimeConfig: Readonly<{
|
||||
BUILD_ID: string;
|
||||
CONFIG_SCHEMA_VERSION: string;
|
||||
/**
|
||||
* §5.1. Removed from Runtime Config V2. When neither side declares it there
|
||||
* is nothing to disagree about: contract identity is verified by the
|
||||
* Release Manifest V2 `contractSet` check instead.
|
||||
*/
|
||||
API_CONTRACT_VERSION?: string;
|
||||
API_CONTRACT_VERSION: string;
|
||||
RELEASE_ID: string;
|
||||
}>,
|
||||
) {
|
||||
const declaredContractVersion =
|
||||
runtimeConfig.API_CONTRACT_VERSION ?? release.apiContractVersion ?? "0";
|
||||
return verifyCompatibilityTuple({
|
||||
frontend: {
|
||||
...release,
|
||||
apiContractVersion: release.apiContractVersion ?? declaredContractVersion,
|
||||
},
|
||||
frontend: release,
|
||||
runtime: {
|
||||
buildId: runtimeConfig.BUILD_ID,
|
||||
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
|
||||
apiContractVersion: declaredContractVersion,
|
||||
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
|
||||
assetManifestHash: release.assetManifestHash,
|
||||
releaseId: runtimeConfig.RELEASE_ID,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,103 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { verifyRollbackReleaseCoherence } from "../../scripts/drill-runbook.ts";
|
||||
import { verifyReleaseRuntimeCoherence } from "../../scripts/lib/release-runtime-coherence.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],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("release coherence", () => {
|
||||
it("owns all nine release tokens and keeps builtAt diagnostic-only", () => {
|
||||
// §5.2 adds contractSetDigest beside the legacy apiContractVersion scalar.
|
||||
@@ -55,4 +148,112 @@ describe("release coherence", () => {
|
||||
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,
|
||||
contractPackages,
|
||||
};
|
||||
const verifierVerdict = (
|
||||
await verifyReleaseRuntimeCoherence(input)
|
||||
).compatible;
|
||||
const rollbackDrillVerdict = (
|
||||
await verifyRollbackReleaseCoherence(input)
|
||||
).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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user