mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
7871730c3e
App Router RSC deployment mismatches and related fallback paths could still write directly to window.location outside the navigation controller. That bypassed the persisted same-target hard-navigation guard, so a canary or rollback mismatch could repeatedly reload the current document instead of stopping after the first hard navigation. Expose the controller's guarded hard-navigation executor and route RSC compatibility fallbacks, server-action deployment fallbacks, and RSC navigation error fallbacks through it. Add skip/cache canary rollback compatibility coverage plus a direct fallback guard regression test.
498 lines
15 KiB
TypeScript
498 lines
15 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
createArtifactCompatibilityEnvelope,
|
|
type ArtifactCompatibilityEnvelope,
|
|
} from "../packages/vinext/src/server/artifact-compatibility.js";
|
|
import {
|
|
createClientReusePayloadHash,
|
|
type ClientReuseManifestEntry,
|
|
} from "../packages/vinext/src/server/client-reuse-manifest.js";
|
|
import {
|
|
buildCacheVariantWithRouteBudget,
|
|
buildRenderObservation,
|
|
buildRenderRequestApiObservations,
|
|
createStaticLayoutArtifactReuseDecision,
|
|
DEFAULT_CACHE_VARIANT_BUDGET,
|
|
type CacheProofOutputScope,
|
|
type StaticLayoutArtifactReuseDecision,
|
|
} from "../packages/vinext/src/server/cache-proof.js";
|
|
import {
|
|
crossCheckClientReuseManifestEntryWithCache,
|
|
type SkipCacheInvalidationProof,
|
|
} from "../packages/vinext/src/server/skip-cache-proof.js";
|
|
|
|
type LayoutOutputScope = Extract<CacheProofOutputScope, { kind: "layout" }>;
|
|
type StaticLayoutReuseDecision = Extract<StaticLayoutArtifactReuseDecision, { kind: "reuse" }>;
|
|
|
|
function createLayoutOutput(): LayoutOutputScope {
|
|
return {
|
|
kind: "layout",
|
|
layoutId: "layout:/dashboard",
|
|
rootBoundaryId: "layout:/",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
}
|
|
|
|
function createCompatibility(
|
|
overrides: Partial<
|
|
Pick<
|
|
ArtifactCompatibilityEnvelope,
|
|
"deploymentVersion" | "graphVersion" | "renderEpoch" | "rootBoundaryId"
|
|
>
|
|
> = {},
|
|
): ArtifactCompatibilityEnvelope {
|
|
return createArtifactCompatibilityEnvelope({
|
|
deploymentVersion: "deploymentVersion" in overrides ? overrides.deploymentVersion : "deploy-a",
|
|
graphVersion: "graphVersion" in overrides ? overrides.graphVersion : "graph-a",
|
|
renderEpoch: "renderEpoch" in overrides ? overrides.renderEpoch : "epoch-a",
|
|
rootBoundaryId: "rootBoundaryId" in overrides ? overrides.rootBoundaryId : "layout:/",
|
|
});
|
|
}
|
|
|
|
function createReuseDecision(
|
|
input: {
|
|
candidateArtifactCompatibility?: ArtifactCompatibilityEnvelope;
|
|
currentArtifactCompatibility?: ArtifactCompatibilityEnvelope;
|
|
output?: LayoutOutputScope;
|
|
} = {},
|
|
): StaticLayoutArtifactReuseDecision {
|
|
const output = input.output ?? createLayoutOutput();
|
|
return createStaticLayoutArtifactReuseDecision({
|
|
currentArtifactCompatibility: input.currentArtifactCompatibility ?? createCompatibility(),
|
|
candidateArtifactCompatibility: input.candidateArtifactCompatibility ?? createCompatibility(),
|
|
candidateObservation: buildRenderObservation({
|
|
boundaryOutcome: { kind: "success" },
|
|
cacheability: "public",
|
|
cacheTags: ["dashboard"],
|
|
completeness: "complete",
|
|
dynamicFetches: [],
|
|
output,
|
|
pathTags: ["/dashboard"],
|
|
requestApis: buildRenderRequestApiObservations({
|
|
completeness: "complete",
|
|
observed: [],
|
|
}),
|
|
}),
|
|
candidateVariant: buildCacheVariantWithRouteBudget({
|
|
budget: DEFAULT_CACHE_VARIANT_BUDGET,
|
|
dimensions: [],
|
|
output,
|
|
routeBudget: {
|
|
routeId: output.routeId,
|
|
variantCacheKeys: [],
|
|
},
|
|
}),
|
|
currentOutput: {
|
|
...output,
|
|
routeId: "route:/dashboard/profile",
|
|
},
|
|
});
|
|
}
|
|
|
|
function expectReuseDecision(
|
|
decision: StaticLayoutArtifactReuseDecision,
|
|
): asserts decision is StaticLayoutReuseDecision {
|
|
expect(decision.kind).toBe("reuse");
|
|
if (decision.kind !== "reuse") {
|
|
throw new Error("Expected fixture cache decision to authorize static layout reuse");
|
|
}
|
|
}
|
|
|
|
function createManifestEntry(input: {
|
|
artifactCompatibility?: ArtifactCompatibilityEnvelope;
|
|
id?: string;
|
|
payloadHash?: string;
|
|
variantCacheKey: string;
|
|
}): ClientReuseManifestEntry {
|
|
return {
|
|
artifactCompatibility: input.artifactCompatibility ?? createCompatibility(),
|
|
id: input.id ?? "layout:/dashboard",
|
|
kind: "layout",
|
|
payloadHash: input.payloadHash ?? createClientReusePayloadHash("dashboard-layout-payload"),
|
|
privacy: "public",
|
|
variantCacheKey: input.variantCacheKey,
|
|
};
|
|
}
|
|
|
|
function createVerifiedFixture(
|
|
options: {
|
|
artifactCompatibility?: ArtifactCompatibilityEnvelope;
|
|
invalidation?: SkipCacheInvalidationProof;
|
|
payloadHash?: string | null;
|
|
} = {},
|
|
) {
|
|
const artifactCompatibility = options.artifactCompatibility ?? createCompatibility();
|
|
const payloadHash = createClientReusePayloadHash("dashboard-layout-payload");
|
|
const decision = createReuseDecision({
|
|
candidateArtifactCompatibility: artifactCompatibility,
|
|
currentArtifactCompatibility: artifactCompatibility,
|
|
});
|
|
expectReuseDecision(decision);
|
|
|
|
return {
|
|
artifact: {
|
|
compatibility: artifactCompatibility,
|
|
invalidation:
|
|
options.invalidation ?? ({ kind: "valid" } satisfies SkipCacheInvalidationProof),
|
|
payloadHash: options.payloadHash === undefined ? payloadHash : options.payloadHash,
|
|
},
|
|
decision,
|
|
entry: createManifestEntry({
|
|
artifactCompatibility,
|
|
payloadHash,
|
|
variantCacheKey: decision.proof.variant.cacheKey,
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe("skip/cache proof cross-checks", () => {
|
|
it("verifies a matching public layout entry without enabling skip transport", () => {
|
|
const fixture = createVerifiedFixture();
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: fixture.entry,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
code: "SKIP_CACHE_CROSS_CHECK_PASSED",
|
|
entryId: "layout:/dashboard",
|
|
kind: "verified",
|
|
skipDisposition: {
|
|
code: "SKIP_MODEL_DISABLED",
|
|
enabled: false,
|
|
mode: "renderAndSend",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects artifact metadata that was not bound to the accepted cache proof", () => {
|
|
const provenCompatibility = createCompatibility({
|
|
deploymentVersion: "deploy-a",
|
|
graphVersion: "graph-a",
|
|
});
|
|
const unprovenCompatibility = createCompatibility({
|
|
deploymentVersion: "deploy-b",
|
|
graphVersion: "graph-b",
|
|
});
|
|
const payloadHash = createClientReusePayloadHash("dashboard-layout-payload");
|
|
const decision = createReuseDecision({
|
|
candidateArtifactCompatibility: provenCompatibility,
|
|
currentArtifactCompatibility: provenCompatibility,
|
|
});
|
|
expectReuseDecision(decision);
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: {
|
|
compatibility: unprovenCompatibility,
|
|
invalidation: { kind: "valid" },
|
|
payloadHash,
|
|
},
|
|
cacheDecision: decision,
|
|
entry: createManifestEntry({
|
|
artifactCompatibility: unprovenCompatibility,
|
|
payloadHash,
|
|
variantCacheKey: decision.proof.variant.cacheKey,
|
|
}),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_ARTIFACT_PROOF_MISMATCH",
|
|
entryId: "layout:/dashboard",
|
|
fields: {
|
|
mismatchedFields: ["graphVersion", "deploymentVersion"],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
for (const [label, artifactCompatibility, reason] of [
|
|
["graph", createCompatibility({ graphVersion: "graph-b" }), "graphVersionMismatch"],
|
|
[
|
|
"deployment",
|
|
createCompatibility({ deploymentVersion: "deploy-b" }),
|
|
"deploymentVersionMismatch",
|
|
],
|
|
[
|
|
"root boundary",
|
|
createCompatibility({ rootBoundaryId: "layout:/other-root" }),
|
|
"rootBoundaryIdMismatch",
|
|
],
|
|
["render epoch", createCompatibility({ renderEpoch: "epoch-b" }), "renderEpochMismatch"],
|
|
] as const) {
|
|
it(`rejects ${label} mismatches between the client hint and cache artifact`, () => {
|
|
const fixture = createVerifiedFixture();
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: {
|
|
...fixture.entry,
|
|
artifactCompatibility,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_ARTIFACT_COMPATIBILITY_INCOMPATIBLE",
|
|
entryId: "layout:/dashboard",
|
|
fields: {
|
|
compatibilityFallback: "renderFresh",
|
|
reason,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
it("rejects unknown graph compatibility between the client hint and cache artifact", () => {
|
|
const fixture = createVerifiedFixture();
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: {
|
|
...fixture.entry,
|
|
artifactCompatibility: createCompatibility({ graphVersion: null }),
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_ARTIFACT_COMPATIBILITY_UNKNOWN",
|
|
entryId: "layout:/dashboard",
|
|
fields: {
|
|
compatibilityFallback: "renderFresh",
|
|
reason: "graphVersionUnknown",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("verifies canary and rollback skip hints when deployments share a compatibility set", () => {
|
|
const rollbackCompatibility = createCompatibility({
|
|
deploymentVersion: "deploy-rollback",
|
|
});
|
|
const fixture = createVerifiedFixture({ artifactCompatibility: rollbackCompatibility });
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
compatibilityMap: {
|
|
deploymentVersions: [["deploy-stable", "deploy-canary", "deploy-rollback"]],
|
|
},
|
|
entry: {
|
|
...fixture.entry,
|
|
artifactCompatibility: createCompatibility({
|
|
deploymentVersion: "deploy-canary",
|
|
}),
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
code: "SKIP_CACHE_CROSS_CHECK_PASSED",
|
|
entryId: "layout:/dashboard",
|
|
kind: "verified",
|
|
skipDisposition: {
|
|
enabled: false,
|
|
mode: "renderAndSend",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects canary and rollback skip hints when the compatibility map is stale", () => {
|
|
const rollbackCompatibility = createCompatibility({
|
|
deploymentVersion: "deploy-rollback",
|
|
});
|
|
const fixture = createVerifiedFixture({ artifactCompatibility: rollbackCompatibility });
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
compatibilityMap: {
|
|
deploymentVersions: [["deploy-stable", "deploy-canary"]],
|
|
},
|
|
entry: {
|
|
...fixture.entry,
|
|
artifactCompatibility: createCompatibility({
|
|
deploymentVersion: "deploy-canary",
|
|
}),
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_ARTIFACT_COMPATIBILITY_INCOMPATIBLE",
|
|
entryId: "layout:/dashboard",
|
|
fields: {
|
|
compatibilityFallback: "renderFresh",
|
|
reason: "deploymentVersionNotDeclaredCompatible",
|
|
},
|
|
},
|
|
skipDisposition: {
|
|
enabled: false,
|
|
mode: "renderAndSend",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects manifest entries whose layout id differs from the accepted cache proof", () => {
|
|
const fixture = createVerifiedFixture();
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: createManifestEntry({
|
|
artifactCompatibility: fixture.artifact.compatibility,
|
|
id: "layout:/billing",
|
|
payloadHash: fixture.entry.payloadHash,
|
|
variantCacheKey: fixture.decision.proof.variant.cacheKey,
|
|
}),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_ENTRY_ID_MISMATCH",
|
|
entryId: "layout:/billing",
|
|
fields: {
|
|
cacheEntryId: "layout:/dashboard",
|
|
manifestEntryId: "layout:/billing",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects variant cache key mismatches", () => {
|
|
const fixture = createVerifiedFixture();
|
|
const staleVariantCacheKey = "cp1:stale-client-variant";
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: {
|
|
...fixture.entry,
|
|
variantCacheKey: staleVariantCacheKey,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_VARIANT_MISMATCH",
|
|
fields: {
|
|
cacheVariantCacheKeyHash: createClientReusePayloadHash(
|
|
fixture.decision.proof.variant.cacheKey,
|
|
),
|
|
entryVariantCacheKeyHash: createClientReusePayloadHash(staleVariantCacheKey),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects payload hash mismatches", () => {
|
|
const fixture = createVerifiedFixture();
|
|
const stalePayloadHash = createClientReusePayloadHash("stale-client-payload");
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: {
|
|
...fixture.entry,
|
|
payloadHash: stalePayloadHash,
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_PAYLOAD_HASH_MISMATCH",
|
|
fields: {
|
|
cachePayloadHash: createClientReusePayloadHash("dashboard-layout-payload"),
|
|
entryPayloadHash: stalePayloadHash,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects missing payload hash proof from the cache artifact", () => {
|
|
const fixture = createVerifiedFixture({ payloadHash: null });
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: fixture.entry,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_PAYLOAD_HASH_MISSING",
|
|
},
|
|
});
|
|
});
|
|
|
|
for (const [invalidation, code] of [
|
|
[{ kind: "unknown" }, "SKIP_CACHE_INVALIDATION_UNKNOWN"],
|
|
[{ kind: "invalidated", invalidationEpoch: "tag-floor-7" }, "SKIP_CACHE_INVALIDATED"],
|
|
] satisfies readonly [SkipCacheInvalidationProof, string][]) {
|
|
it(`rejects ${invalidation.kind} invalidation proof`, () => {
|
|
const fixture = createVerifiedFixture({ invalidation });
|
|
|
|
const result = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: fixture.decision,
|
|
entry: fixture.entry,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code,
|
|
entryId: "layout:/dashboard",
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
it("rejects missing or rejected cache proof instead of treating cache presence as authority", () => {
|
|
const fixture = createVerifiedFixture();
|
|
const rejectedDecision = createReuseDecision({
|
|
candidateArtifactCompatibility: createCompatibility({ deploymentVersion: "deploy-b" }),
|
|
});
|
|
|
|
const missing = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: null,
|
|
entry: fixture.entry,
|
|
});
|
|
const rejected = crossCheckClientReuseManifestEntryWithCache({
|
|
artifact: fixture.artifact,
|
|
cacheDecision: rejectedDecision,
|
|
entry: fixture.entry,
|
|
});
|
|
|
|
expect(missing).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_PROOF_MISSING",
|
|
},
|
|
});
|
|
expect(rejected).toMatchObject({
|
|
kind: "rejected",
|
|
rejection: {
|
|
code: "SKIP_CACHE_PROOF_REJECTED",
|
|
fields: {
|
|
cacheProofCode: "CP_ARTIFACT_COMPATIBILITY_INCOMPATIBLE",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|