mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
fbf77b17dc
* fix(router): refresh retained parallel routes * fix(router): harden supplemental parallel refreshes * fix(router): preserve refreshed nested source routes * fix(router): skip retained descendant refreshes * fix(router): canonicalize supplemental refresh paths * fix(app-router): preserve superseded flight streams
1397 lines
47 KiB
TypeScript
1397 lines
47 KiB
TypeScript
import React from "react";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vite-plus/test";
|
|
import { UNMATCHED_SLOT } from "../packages/vinext/src/shims/slot.js";
|
|
import {
|
|
APP_ARTIFACT_COMPATIBILITY_KEY,
|
|
APP_CACHE_ENTRY_REUSE_PROOF_KEY,
|
|
APP_DYNAMIC_STALE_TIME_KEY,
|
|
AppElementsWire,
|
|
APP_INTERCEPTION_KEY,
|
|
APP_INTERCEPTION_CONTEXT_KEY,
|
|
APP_LAYOUT_IDS_KEY,
|
|
APP_LAYOUT_FLAGS_KEY,
|
|
APP_ROOT_LAYOUT_KEY,
|
|
APP_ROUTE_KEY,
|
|
APP_BFCACHE_SEGMENT_IDENTITIES_KEY,
|
|
APP_SKIPPED_LAYOUT_IDS_KEY,
|
|
APP_SOURCE_PAGE_SEGMENTS_KEY,
|
|
APP_SLOT_BINDINGS_KEY,
|
|
APP_UNMATCHED_SLOT_WIRE_VALUE,
|
|
buildOutgoingAppPayload,
|
|
isAppElementsRecord,
|
|
normalizeAppElements,
|
|
readAppElementsMetadata,
|
|
resolveVisitedResponseInterceptionContext,
|
|
withLayoutFlags,
|
|
} from "../packages/vinext/src/server/app-elements.js";
|
|
import {
|
|
APP_ELEMENTS_SCHEMA_VERSION,
|
|
ARTIFACT_COMPATIBILITY_SCHEMA_VERSION,
|
|
createArtifactCompatibilityEnvelope,
|
|
evaluateArtifactCompatibility,
|
|
RSC_PAYLOAD_SCHEMA_VERSION,
|
|
} from "../packages/vinext/src/server/artifact-compatibility.js";
|
|
import { createCacheEntryReuseProof } from "../packages/vinext/src/server/cache-proof.js";
|
|
|
|
describe("AppElementsWire", () => {
|
|
it("exposes stable metadata keys through the codec boundary", () => {
|
|
expect(AppElementsWire.keys.bfcacheSegmentIdentities).toBe(APP_BFCACHE_SEGMENT_IDENTITIES_KEY);
|
|
});
|
|
|
|
it("encodes outgoing record payloads without mutating caller-owned records", () => {
|
|
const element = {
|
|
"layout:/": "root-layout",
|
|
"page:/blog": "blog-page",
|
|
};
|
|
|
|
const encoded = AppElementsWire.encodeOutgoingPayload({
|
|
element,
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
|
|
expect(encoded).not.toBe(element);
|
|
expect(isAppElementsRecord(encoded)).toBe(true);
|
|
if (isAppElementsRecord(encoded)) {
|
|
expect(encoded).toEqual({
|
|
"layout:/": "root-layout",
|
|
"page:/blog": "blog-page",
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: createArtifactCompatibilityEnvelope(),
|
|
[APP_LAYOUT_FLAGS_KEY]: { "layout:/": "s" },
|
|
});
|
|
}
|
|
expect(element).toEqual({
|
|
"layout:/": "root-layout",
|
|
"page:/blog": "blog-page",
|
|
});
|
|
});
|
|
|
|
it("decodes wire payloads and reads metadata through one codec boundary", () => {
|
|
const decoded = AppElementsWire.decode({
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: "/feed",
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: AppElementsWire.encodeRouteId("/photos/42", "/feed"),
|
|
"slot:modal:/": AppElementsWire.unmatchedSlotValue,
|
|
});
|
|
|
|
expect(decoded["slot:modal:/"]).toBe(UNMATCHED_SLOT);
|
|
expect(AppElementsWire.readMetadata(decoded)).toEqual({
|
|
artifactCompatibility: createArtifactCompatibilityEnvelope(),
|
|
interception: null,
|
|
interceptionContext: "/feed",
|
|
layoutIds: [],
|
|
layoutFlags: {},
|
|
rootLayoutTreePath: "/",
|
|
routeId: "route:/photos/42\0/feed",
|
|
bfcacheSegmentIdentities: {},
|
|
skippedLayoutIds: [],
|
|
slotBindings: [],
|
|
sourcePage: null,
|
|
});
|
|
});
|
|
|
|
it("degrades malformed optional source-page metadata to null", () => {
|
|
const decoded = AppElementsWire.decode({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
[APP_SOURCE_PAGE_SEGMENTS_KEY]: ["dashboard/page"],
|
|
});
|
|
|
|
expect(AppElementsWire.readMetadata(decoded).sourcePage).toBeNull();
|
|
});
|
|
|
|
it("reads legacy source-page strings from cached payloads", () => {
|
|
const decoded = AppElementsWire.decode({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
__sourcePage: "/(dashboard)/page",
|
|
});
|
|
|
|
expect(AppElementsWire.readMetadata(decoded).sourcePage).toBe("/(dashboard)/page");
|
|
});
|
|
|
|
it.each(["/", "//page", "dashboard/page"])(
|
|
"omits malformed outgoing source-page metadata: %s",
|
|
(sourcePage) => {
|
|
const metadata = AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
sourcePage,
|
|
});
|
|
|
|
expect(metadata).not.toHaveProperty(APP_SOURCE_PAGE_SEGMENTS_KEY);
|
|
},
|
|
);
|
|
|
|
it("creates the canonical metadata entries for outgoing AppElements records", () => {
|
|
const metadata = AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
layoutIds: ["layout:/(dashboard)"],
|
|
rootLayoutTreePath: "/(dashboard)",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
bfcacheSegmentIdentities: {
|
|
[AppElementsWire.encodeLayoutId("/(dashboard)")]: "/dashboard",
|
|
[AppElementsWire.encodePageId("/dashboard", null)]: "/dashboard",
|
|
},
|
|
sourcePage: "/(dashboard)/page",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: null,
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/(dashboard)"],
|
|
[APP_ROOT_LAYOUT_KEY]: "/(dashboard)",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
|
|
"layout:/(dashboard)": "/dashboard",
|
|
"page:/dashboard": "/dashboard",
|
|
},
|
|
[APP_SOURCE_PAGE_SEGMENTS_KEY]: ["(dashboard)", "page"],
|
|
});
|
|
});
|
|
|
|
it("round-trips explicit interception proof metadata through the codec", () => {
|
|
const interception = {
|
|
sourceMatchedUrl: "/feed",
|
|
sourceRouteId: AppElementsWire.encodeRouteId("/feed", null),
|
|
slotId: AppElementsWire.encodeSlotId("modal", "/feed"),
|
|
targetMatchedUrl: "/photos/42",
|
|
targetRouteId: AppElementsWire.encodeRouteId("/photos/42", null),
|
|
};
|
|
const metadata = AppElementsWire.createMetadataEntries({
|
|
interception,
|
|
interceptionContext: "/feed",
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/photos/42", "/feed"),
|
|
});
|
|
|
|
expect(metadata[APP_INTERCEPTION_KEY]).toEqual(interception);
|
|
expect(AppElementsWire.readMetadata(metadata).interception).toEqual(interception);
|
|
});
|
|
|
|
it("rejects malformed path URLs in explicit interception proof metadata", () => {
|
|
const validInterception = {
|
|
sourceMatchedUrl: "/feed",
|
|
sourceRouteId: AppElementsWire.encodeRouteId("/feed", null),
|
|
slotId: AppElementsWire.encodeSlotId("modal", "/feed"),
|
|
targetMatchedUrl: "/photos/42",
|
|
targetRouteId: AppElementsWire.encodeRouteId("/photos/42", null),
|
|
};
|
|
const malformedMatchedUrls = [
|
|
"//example.test/feed",
|
|
"/feed?tab=latest",
|
|
"/feed#modal",
|
|
"/fe\0ed",
|
|
];
|
|
|
|
for (const sourceMatchedUrl of malformedMatchedUrls) {
|
|
expect(() =>
|
|
readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_INTERCEPTION_KEY]: {
|
|
...validInterception,
|
|
sourceMatchedUrl,
|
|
},
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: AppElementsWire.encodeRouteId("/photos/42", "/feed"),
|
|
}),
|
|
),
|
|
).toThrow("[vinext] Invalid __interception in App Router payload: expected path URLs");
|
|
}
|
|
});
|
|
|
|
it("normalizes slot binding metadata at the wire boundary", () => {
|
|
const metadata = AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
layoutIds: ["layout:/dashboard"],
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
slotBindings: [
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:analytics:/dashboard",
|
|
state: "default",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(metadata[APP_SLOT_BINDINGS_KEY]).toEqual([
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:analytics:/dashboard",
|
|
state: "default",
|
|
},
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "duplicate slot id",
|
|
layoutIds: ["layout:/dashboard"],
|
|
slotBindings: [
|
|
{ ownerLayoutId: "layout:/dashboard", slotId: "slot:team:/dashboard", state: "active" },
|
|
{ ownerLayoutId: "layout:/dashboard", slotId: "slot:team:/dashboard", state: "default" },
|
|
],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: duplicate slot id",
|
|
},
|
|
{
|
|
label: "owner layout not present in layoutIds",
|
|
layoutIds: ["layout:/dashboard"],
|
|
slotBindings: [
|
|
{ ownerLayoutId: "layout:/stale", slotId: "slot:team:/dashboard", state: "active" },
|
|
],
|
|
message:
|
|
"[vinext] Invalid __slotBindings in App Router payload: owner layout id missing from __layoutIds",
|
|
},
|
|
] as const)(
|
|
"rejects invalid slot binding metadata while creating payload entries: $label",
|
|
({ layoutIds, message, slotBindings }) => {
|
|
expect(() =>
|
|
AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
layoutIds,
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
slotBindings,
|
|
}),
|
|
).toThrow(message);
|
|
},
|
|
);
|
|
|
|
it("constructs and parses canonical element wire keys through the codec", () => {
|
|
const keys = [
|
|
AppElementsWire.encodeRouteId("/blog/[slug]", null),
|
|
AppElementsWire.encodeRouteId("/photos/42", "/feed"),
|
|
AppElementsWire.encodePageId("/blog/[slug]", null),
|
|
AppElementsWire.encodeLayoutId("/(marketing)/blog/[slug]"),
|
|
AppElementsWire.encodeTemplateId("/(marketing)/blog/[slug]"),
|
|
AppElementsWire.encodeSlotId("modal", "/feed"),
|
|
];
|
|
|
|
expect(keys).toEqual([
|
|
"route:/blog/[slug]",
|
|
"route:/photos/42\0/feed",
|
|
"page:/blog/[slug]",
|
|
"layout:/(marketing)/blog/[slug]",
|
|
"template:/(marketing)/blog/[slug]",
|
|
"slot:modal:/feed",
|
|
]);
|
|
|
|
expect(AppElementsWire.parseElementKey(keys[0])).toEqual({
|
|
interceptionContext: null,
|
|
kind: "route",
|
|
path: "/blog/[slug]",
|
|
});
|
|
expect(AppElementsWire.parseElementKey(keys[1])).toEqual({
|
|
interceptionContext: "/feed",
|
|
kind: "route",
|
|
path: "/photos/42",
|
|
});
|
|
expect(AppElementsWire.parseElementKey(keys[2])).toEqual({
|
|
interceptionContext: null,
|
|
kind: "page",
|
|
path: "/blog/[slug]",
|
|
});
|
|
expect(AppElementsWire.parseElementKey(keys[3])).toEqual({
|
|
kind: "layout",
|
|
treePath: "/(marketing)/blog/[slug]",
|
|
});
|
|
expect(AppElementsWire.parseElementKey(keys[4])).toEqual({
|
|
kind: "template",
|
|
treePath: "/(marketing)/blog/[slug]",
|
|
});
|
|
expect(AppElementsWire.parseElementKey(keys[5])).toEqual({
|
|
kind: "slot",
|
|
name: "modal",
|
|
treePath: "/feed",
|
|
});
|
|
expect(AppElementsWire.isSlotId(keys[5])).toBe(true);
|
|
expect(AppElementsWire.parseElementKey("__route")).toBeNull();
|
|
expect(AppElementsWire.parseElementKey("slot:modal")).toBeNull();
|
|
});
|
|
|
|
it("round-trips legacy-compatible payload metadata through the codec", () => {
|
|
const payload = AppElementsWire.encodeOutgoingPayload({
|
|
element: {
|
|
...AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
}),
|
|
[AppElementsWire.encodeLayoutId("/")]: "layout",
|
|
[AppElementsWire.encodePageId("/dashboard", null)]: "page",
|
|
},
|
|
layoutFlags: {
|
|
[AppElementsWire.encodeLayoutId("/")]: "s",
|
|
},
|
|
});
|
|
|
|
expect(isAppElementsRecord(payload)).toBe(true);
|
|
if (!isAppElementsRecord(payload)) return;
|
|
|
|
expect(AppElementsWire.readMetadata(payload)).toEqual({
|
|
artifactCompatibility: createArtifactCompatibilityEnvelope(),
|
|
interception: null,
|
|
interceptionContext: null,
|
|
layoutIds: [],
|
|
layoutFlags: { [AppElementsWire.encodeLayoutId("/")]: "s" },
|
|
rootLayoutTreePath: "/",
|
|
routeId: "route:/dashboard",
|
|
bfcacheSegmentIdentities: {},
|
|
skippedLayoutIds: [],
|
|
slotBindings: [],
|
|
sourcePage: null,
|
|
});
|
|
});
|
|
|
|
it("round-trips per-page dynamic stale time through payload metadata", () => {
|
|
const payload = AppElementsWire.encodeOutgoingPayload({
|
|
dynamicStaleTimeSeconds: 30,
|
|
element: {
|
|
...AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
rootLayoutTreePath: "/",
|
|
routeId: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
}),
|
|
[AppElementsWire.encodePageId("/dashboard", null)]: "page",
|
|
},
|
|
layoutFlags: {},
|
|
});
|
|
|
|
expect(isAppElementsRecord(payload)).toBe(true);
|
|
if (!isAppElementsRecord(payload)) return;
|
|
|
|
expect(payload[APP_DYNAMIC_STALE_TIME_KEY]).toBe(30);
|
|
expect(AppElementsWire.readMetadata(payload).dynamicStaleTimeSeconds).toBe(30);
|
|
});
|
|
|
|
it("keeps legacy unmatched-slot markers compatible while parsing slot keys", () => {
|
|
const slotId = AppElementsWire.encodeSlotId("modal", "/");
|
|
const decoded = AppElementsWire.decode({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: AppElementsWire.encodeRouteId("/dashboard", null),
|
|
[slotId]: AppElementsWire.unmatchedSlotValue,
|
|
});
|
|
|
|
expect(decoded[slotId]).toBe(UNMATCHED_SLOT);
|
|
expect(AppElementsWire.parseElementKey(slotId)).toEqual({
|
|
kind: "slot",
|
|
name: "modal",
|
|
treePath: "/",
|
|
});
|
|
});
|
|
|
|
it("keeps raw AppElements wire-key construction inside the codec boundary", () => {
|
|
const root = path.resolve(import.meta.dirname, "..");
|
|
const sourceRoot = path.join(root, "packages/vinext/src");
|
|
const allowed = new Set([
|
|
path.join(sourceRoot, "routing/app-route-graph.ts"),
|
|
path.join(sourceRoot, "server/app-elements-wire.ts"),
|
|
]);
|
|
const rawWireConstruction =
|
|
/`(?:route|page|layout|template):\$\{|`slot:\$\{|["'](?:route|page|layout|template):["']\s*\+|["']slot:["']\s*\+|\.startsWith\(["'](?:slot|layout|page|route|template):["']\)/;
|
|
|
|
expect(rawWireConstruction.test('"slot:" + name + ":" + treePath')).toBe(true);
|
|
expect(rawWireConstruction.test('"layout:" + treePath')).toBe(true);
|
|
expect(rawWireConstruction.test("key.startsWith('slot:')")).toBe(true);
|
|
|
|
const violations: string[] = [];
|
|
const visit = (dir: string): void => {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
visit(fullPath);
|
|
continue;
|
|
}
|
|
if (!entry.isFile() || !/\.(?:ts|tsx)$/.test(entry.name)) continue;
|
|
if (allowed.has(fullPath)) continue;
|
|
|
|
const source = fs.readFileSync(fullPath, "utf8");
|
|
if (rawWireConstruction.test(source)) {
|
|
violations.push(path.relative(root, fullPath));
|
|
}
|
|
}
|
|
};
|
|
|
|
visit(sourceRoot);
|
|
|
|
expect(violations).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("app elements payload helpers", () => {
|
|
it("normalizes the unmatched-slot wire marker to UNMATCHED_SLOT for slot entries", () => {
|
|
const normalized = normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"page:/dashboard": React.createElement("main", null, "dashboard"),
|
|
"slot:modal:/": APP_UNMATCHED_SLOT_WIRE_VALUE,
|
|
});
|
|
|
|
expect(normalized["slot:modal:/"]).toBe(UNMATCHED_SLOT);
|
|
expect(normalized["page:/dashboard"]).not.toBe(UNMATCHED_SLOT);
|
|
});
|
|
|
|
it("does not rewrite the unmatched-slot wire marker for non-slot entries", () => {
|
|
const normalized = normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"page:/dashboard": APP_UNMATCHED_SLOT_WIRE_VALUE,
|
|
});
|
|
|
|
expect(normalized["page:/dashboard"]).toBe(APP_UNMATCHED_SLOT_WIRE_VALUE);
|
|
});
|
|
|
|
it("reads route metadata from the normalized payload", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: "/feed",
|
|
[APP_ROOT_LAYOUT_KEY]: "/(dashboard)",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"route:/dashboard": React.createElement("div", null, "route"),
|
|
}),
|
|
);
|
|
|
|
expect(metadata.routeId).toBe("route:/dashboard");
|
|
expect(metadata.interceptionContext).toBe("/feed");
|
|
expect(metadata.rootLayoutTreePath).toBe("/(dashboard)");
|
|
});
|
|
|
|
it("defaults missing interception context metadata to null", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"route:/dashboard": React.createElement("div", null, "route"),
|
|
}),
|
|
);
|
|
|
|
expect(metadata.interceptionContext).toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "non-string binding",
|
|
bfcacheSegmentIdentities: {
|
|
"page:/dashboard": "/dashboard",
|
|
"layout:/": 123,
|
|
},
|
|
},
|
|
{
|
|
label: "non-segment element id",
|
|
bfcacheSegmentIdentities: {
|
|
"page:/dashboard": "/dashboard",
|
|
"route:/dashboard": "/dashboard",
|
|
},
|
|
},
|
|
])("rejects the BFCache identity map atomically: $label", ({ bfcacheSegmentIdentities }) => {
|
|
const metadata = readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: bfcacheSegmentIdentities,
|
|
});
|
|
|
|
expect(metadata.bfcacheSegmentIdentities).toEqual({});
|
|
});
|
|
|
|
it("encodes intercepted route ids and cache keys with a NUL separator", () => {
|
|
expect(AppElementsWire.encodeRouteId("/photos/42", null)).toBe("route:/photos/42");
|
|
expect(AppElementsWire.encodeRouteId("/photos/42", "/feed")).toBe("route:/photos/42\0/feed");
|
|
expect(AppElementsWire.encodeCacheKey("/photos/42.rsc", null)).toBe("/photos/42.rsc");
|
|
expect(AppElementsWire.encodeCacheKey("/photos/42.rsc", "/feed")).toBe("/photos/42.rsc\0/feed");
|
|
});
|
|
|
|
it("preserves the request cache context when a direct-route payload omits it", () => {
|
|
expect(resolveVisitedResponseInterceptionContext("/feed", null)).toBe("/feed");
|
|
expect(resolveVisitedResponseInterceptionContext("/feed", "/feed")).toBe("/feed");
|
|
expect(resolveVisitedResponseInterceptionContext("/feed", "/gallery")).toBe("/gallery");
|
|
expect(resolveVisitedResponseInterceptionContext(null, null)).toBeNull();
|
|
});
|
|
|
|
it("rejects payloads with a missing __route key", () => {
|
|
expect(() =>
|
|
readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
}),
|
|
),
|
|
).toThrow("[vinext] Missing __route string in App Router payload");
|
|
});
|
|
|
|
it("rejects payloads with an invalid __rootLayout value", () => {
|
|
expect(() =>
|
|
readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: 123,
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
),
|
|
).toThrow("[vinext] Invalid __rootLayout in App Router payload: expected string or null");
|
|
});
|
|
|
|
it("rejects payloads with a missing __rootLayout key", () => {
|
|
expect(() =>
|
|
readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
),
|
|
).toThrow("[vinext] Missing __rootLayout key in App Router payload");
|
|
});
|
|
|
|
it("rejects payloads with an invalid __interceptionContext value", () => {
|
|
expect(() =>
|
|
readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: 123,
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
),
|
|
).toThrow("[vinext] Invalid __interceptionContext in App Router payload");
|
|
});
|
|
|
|
it("reads layoutFlags from payload metadata", () => {
|
|
// Layout flags are set directly on the elements object (not via
|
|
// normalizeAppElements which expects AppWireElementValue types).
|
|
const elements = {
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/blog",
|
|
"page:/blog": React.createElement("div", null, "blog"),
|
|
}),
|
|
[APP_LAYOUT_FLAGS_KEY]: { "layout:/": "s", "layout:/blog": "d" },
|
|
};
|
|
const metadata = readAppElementsMetadata(elements);
|
|
|
|
expect(metadata.layoutIds).toEqual([]);
|
|
expect(metadata.layoutFlags).toEqual({ "layout:/": "s", "layout:/blog": "d" });
|
|
});
|
|
|
|
it("defaults missing layoutFlags to empty object (backward compat)", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"route:/dashboard": React.createElement("div", null, "route"),
|
|
}),
|
|
);
|
|
|
|
expect(metadata.layoutIds).toEqual([]);
|
|
expect(metadata.layoutFlags).toEqual({});
|
|
});
|
|
|
|
it("reads layoutIds from payload metadata", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/", "layout:/dashboard"],
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard/settings",
|
|
"route:/dashboard/settings": React.createElement("div", null, "route"),
|
|
}),
|
|
);
|
|
|
|
expect(metadata.layoutIds).toEqual(["layout:/", "layout:/dashboard"]);
|
|
});
|
|
|
|
it("reads validated slot bindings from payload metadata", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/", "layout:/dashboard"],
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard/settings",
|
|
[APP_SLOT_BINDINGS_KEY]: [
|
|
{
|
|
activeRouteId: "route:/dashboard/settings",
|
|
interceptionId: "interception:slot:team:/dashboard:/dashboard->/dashboard/settings",
|
|
interceptionSourceMatchedUrl: "/dashboard",
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:analytics:/dashboard",
|
|
state: "unmatched",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
expect(metadata.slotBindings).toEqual([
|
|
{
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:analytics:/dashboard",
|
|
state: "unmatched",
|
|
},
|
|
{
|
|
activeRouteId: "route:/dashboard/settings",
|
|
interceptionId: "interception:slot:team:/dashboard:/dashboard->/dashboard/settings",
|
|
interceptionSourceMatchedUrl: "/dashboard",
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects invalid layoutIds metadata", () => {
|
|
expect(() =>
|
|
readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/", 1],
|
|
}),
|
|
).toThrow("[vinext] Invalid __layoutIds in App Router payload: expected layout id string[]");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "non-array",
|
|
value: "layout:/dashboard",
|
|
message:
|
|
"[vinext] Invalid __skippedLayoutIds in App Router payload: expected layout id string[]",
|
|
},
|
|
{
|
|
label: "non-string",
|
|
value: ["layout:/", 1],
|
|
message:
|
|
"[vinext] Invalid __skippedLayoutIds in App Router payload: expected layout id string[]",
|
|
},
|
|
{
|
|
label: "non-layout id",
|
|
value: ["page:/dashboard"],
|
|
message: "[vinext] Invalid __skippedLayoutIds in App Router payload: expected layout ids",
|
|
},
|
|
])("rejects invalid skipped layout metadata: $label", ({ message, value }) => {
|
|
expect(() =>
|
|
readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_SKIPPED_LAYOUT_IDS_KEY]: value,
|
|
}),
|
|
).toThrow(message);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "non-array",
|
|
value: "slot:team:/dashboard",
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected array",
|
|
},
|
|
{
|
|
label: "non-object",
|
|
value: ["slot:team:/dashboard"],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected objects",
|
|
},
|
|
{
|
|
label: "non-slot id",
|
|
value: [{ ownerLayoutId: "layout:/dashboard", slotId: "page:/dashboard", state: "active" }],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected slot ids",
|
|
},
|
|
{
|
|
label: "non-layout owner",
|
|
value: [
|
|
{ ownerLayoutId: "slot:team:/dashboard", slotId: "slot:team:/dashboard", state: "active" },
|
|
],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected owner layout ids",
|
|
},
|
|
{
|
|
label: "invalid state",
|
|
value: [
|
|
{ ownerLayoutId: "layout:/dashboard", slotId: "slot:team:/dashboard", state: "stale" },
|
|
],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected state",
|
|
},
|
|
{
|
|
label: "invalid interception id",
|
|
value: [
|
|
{
|
|
interceptionId: 1,
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: expected interception ids",
|
|
},
|
|
{
|
|
label: "invalid interception source URL",
|
|
value: [
|
|
{
|
|
interceptionSourceMatchedUrl: "https://example.test/dashboard",
|
|
ownerLayoutId: "layout:/dashboard",
|
|
slotId: "slot:team:/dashboard",
|
|
state: "active",
|
|
},
|
|
],
|
|
message: "[vinext] Invalid __interception in App Router payload: expected path URLs",
|
|
},
|
|
{
|
|
label: "duplicate slot id",
|
|
value: [
|
|
{ ownerLayoutId: "layout:/dashboard", slotId: "slot:team:/dashboard", state: "active" },
|
|
{ ownerLayoutId: "layout:/dashboard", slotId: "slot:team:/dashboard", state: "default" },
|
|
],
|
|
message: "[vinext] Invalid __slotBindings in App Router payload: duplicate slot id",
|
|
},
|
|
{
|
|
label: "owner layout not present in layoutIds",
|
|
value: [{ ownerLayoutId: "layout:/stale", slotId: "slot:team:/dashboard", state: "active" }],
|
|
message:
|
|
"[vinext] Invalid __slotBindings in App Router payload: owner layout id missing from __layoutIds",
|
|
},
|
|
])("rejects invalid slotBindings metadata: $label", ({ value, message }) => {
|
|
expect(() =>
|
|
readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/dashboard"],
|
|
[APP_SLOT_BINDINGS_KEY]: value,
|
|
}),
|
|
).toThrow(message);
|
|
});
|
|
|
|
it.each([
|
|
["page id", "page:/dashboard"],
|
|
["slot id", "slot:modal:/dashboard"],
|
|
["malformed layout id", "layout:dashboard"],
|
|
])("rejects %s in layoutIds metadata", (_, layoutId) => {
|
|
expect(() =>
|
|
readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_LAYOUT_IDS_KEY]: ["layout:/", layoutId],
|
|
}),
|
|
).toThrow("[vinext] Invalid __layoutIds in App Router payload: expected layout ids");
|
|
});
|
|
|
|
it("reads artifact compatibility envelope metadata", () => {
|
|
const envelope = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
});
|
|
const metadata = readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"route:/dashboard": React.createElement("div", null, "route"),
|
|
}),
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: envelope,
|
|
});
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(envelope);
|
|
});
|
|
|
|
it("defaults missing artifact compatibility to unknown proof for legacy payloads", () => {
|
|
const metadata = readAppElementsMetadata(
|
|
normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
"route:/dashboard": React.createElement("div", null, "route"),
|
|
}),
|
|
);
|
|
|
|
expect(metadata.artifactCompatibility).toEqual({
|
|
schemaVersion: ARTIFACT_COMPATIBILITY_SCHEMA_VERSION,
|
|
graphVersion: null,
|
|
deploymentVersion: null,
|
|
appElementsSchemaVersion: APP_ELEMENTS_SCHEMA_VERSION,
|
|
rscPayloadSchemaVersion: RSC_PAYLOAD_SCHEMA_VERSION,
|
|
rootBoundaryId: null,
|
|
renderEpoch: null,
|
|
});
|
|
});
|
|
|
|
it("defaults malformed artifact compatibility metadata to unknown proof", () => {
|
|
const metadata = readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: {
|
|
schemaVersion: ARTIFACT_COMPATIBILITY_SCHEMA_VERSION,
|
|
graphVersion: 123,
|
|
deploymentVersion: null,
|
|
appElementsSchemaVersion: APP_ELEMENTS_SCHEMA_VERSION,
|
|
rscPayloadSchemaVersion: RSC_PAYLOAD_SCHEMA_VERSION,
|
|
rootBoundaryId: null,
|
|
renderEpoch: null,
|
|
},
|
|
});
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(createArtifactCompatibilityEnvelope());
|
|
});
|
|
|
|
it("defaults artifact compatibility with an unrecognized schema version to unknown proof", () => {
|
|
const metadata = readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: {
|
|
schemaVersion: 99,
|
|
graphVersion: null,
|
|
deploymentVersion: null,
|
|
appElementsSchemaVersion: APP_ELEMENTS_SCHEMA_VERSION,
|
|
rscPayloadSchemaVersion: RSC_PAYLOAD_SCHEMA_VERSION,
|
|
rootBoundaryId: null,
|
|
renderEpoch: null,
|
|
},
|
|
});
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(createArtifactCompatibilityEnvelope());
|
|
});
|
|
|
|
it("defaults non-object artifact compatibility metadata to unknown proof", () => {
|
|
const metadata = readAppElementsMetadata({
|
|
...normalizeAppElements({
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
}),
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: "garbage",
|
|
});
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(createArtifactCompatibilityEnvelope());
|
|
});
|
|
});
|
|
|
|
describe("isAppElementsRecord", () => {
|
|
it("returns true for a plain record", () => {
|
|
expect(isAppElementsRecord({ "page:/": "x" })).toBe(true);
|
|
});
|
|
|
|
it("returns false for a React element", () => {
|
|
expect(isAppElementsRecord(React.createElement("div", null, "x"))).toBe(false);
|
|
});
|
|
|
|
it("returns false for null", () => {
|
|
expect(isAppElementsRecord(null)).toBe(false);
|
|
});
|
|
|
|
it("returns false for an array", () => {
|
|
expect(isAppElementsRecord([])).toBe(false);
|
|
});
|
|
|
|
it("returns false for a string", () => {
|
|
expect(isAppElementsRecord("string")).toBe(false);
|
|
});
|
|
|
|
it("returns false for undefined", () => {
|
|
expect(isAppElementsRecord(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("withLayoutFlags", () => {
|
|
it("attaches the __layoutFlags key with the supplied value", () => {
|
|
const input = { "page:/": "page" };
|
|
const result = withLayoutFlags(input, { "layout:/": "s" });
|
|
expect(result[APP_LAYOUT_FLAGS_KEY]).toEqual({ "layout:/": "s" });
|
|
});
|
|
|
|
it("does not mutate the input", () => {
|
|
const input: Record<string, unknown> = { "page:/": "page", "layout:/": "layout" };
|
|
const snapshot = structuredClone(input);
|
|
const result = withLayoutFlags(input, { "layout:/": "d" });
|
|
expect(result).not.toBe(input);
|
|
expect(input).toEqual(snapshot);
|
|
expect(Object.keys(input)).toEqual(Object.keys(snapshot));
|
|
expect(APP_LAYOUT_FLAGS_KEY in input).toBe(false);
|
|
});
|
|
|
|
it("preserves other keys on the returned object", () => {
|
|
const input = { "page:/blog": "page", "layout:/": "layout" };
|
|
const result = withLayoutFlags(input, { "layout:/": "s" });
|
|
expect(result["page:/blog"]).toBe("page");
|
|
expect(result["layout:/"]).toBe("layout");
|
|
});
|
|
|
|
it("returns a new object with a different identity", () => {
|
|
const input = { "page:/": "page" };
|
|
const result = withLayoutFlags(input, {});
|
|
expect(result).not.toBe(input);
|
|
});
|
|
});
|
|
|
|
describe("buildOutgoingAppPayload", () => {
|
|
it("returns a non-record element unchanged (same identity)", () => {
|
|
const element = React.createElement("div", null, "page");
|
|
const result = buildOutgoingAppPayload({
|
|
element,
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
expect(result).toBe(element);
|
|
});
|
|
|
|
it("returns a new object for a record element (different identity)", () => {
|
|
const element = { "page:/": "page" };
|
|
const result = buildOutgoingAppPayload({
|
|
element,
|
|
layoutFlags: {},
|
|
});
|
|
expect(result).not.toBe(element);
|
|
});
|
|
|
|
it("does not mutate the input record", () => {
|
|
const element: Record<string, React.ReactNode> = {
|
|
"layout:/": "root-layout",
|
|
"layout:/blog": "blog-layout",
|
|
"page:/blog": "blog-page",
|
|
};
|
|
const snapshot = structuredClone(element);
|
|
const result = buildOutgoingAppPayload({
|
|
element,
|
|
layoutFlags: { "layout:/": "s", "layout:/blog": "d" },
|
|
});
|
|
expect(result).not.toBe(element);
|
|
expect(element).toEqual(snapshot);
|
|
expect(Object.keys(element)).toEqual(Object.keys(snapshot));
|
|
expect(APP_LAYOUT_FLAGS_KEY in element).toBe(false);
|
|
expect(APP_ARTIFACT_COMPATIBILITY_KEY in element).toBe(false);
|
|
});
|
|
|
|
it("attaches __layoutFlags on the returned record", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: { "page:/": "page" },
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result[APP_LAYOUT_FLAGS_KEY]).toEqual({ "layout:/": "s" });
|
|
}
|
|
});
|
|
|
|
it("attaches __artifactCompatibility on the returned record", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: { "page:/": "page" },
|
|
layoutFlags: { "layout:/": "s" },
|
|
artifactCompatibility: createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
}),
|
|
});
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result[APP_ARTIFACT_COMPATIBILITY_KEY]).toEqual({
|
|
schemaVersion: ARTIFACT_COMPATIBILITY_SCHEMA_VERSION,
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
appElementsSchemaVersion: APP_ELEMENTS_SCHEMA_VERSION,
|
|
rscPayloadSchemaVersion: RSC_PAYLOAD_SCHEMA_VERSION,
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: null,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("carries planner-visible cache entry reuse proof as metadata only", () => {
|
|
const cacheEntryReuseProof = createCacheEntryReuseProof(null);
|
|
const result = buildOutgoingAppPayload({
|
|
element: {
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
"page:/dashboard": "dashboard-page",
|
|
},
|
|
cacheEntryReuseProof,
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result[APP_CACHE_ENTRY_REUSE_PROOF_KEY]).toEqual(cacheEntryReuseProof);
|
|
expect(AppElementsWire.readMetadata(result).cacheEntryReuseProof).toEqual(
|
|
cacheEntryReuseProof,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("carries source pages as segments on the wire and reconstructs the browser value", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: {
|
|
...AppElementsWire.createMetadataEntries({
|
|
interceptionContext: null,
|
|
rootLayoutTreePath: "/",
|
|
routeId: "route:/pricing",
|
|
sourcePage: "/(marketing)/pricing/page",
|
|
}),
|
|
"page:/pricing": "pricing-page",
|
|
},
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result[APP_SOURCE_PAGE_SEGMENTS_KEY]).toEqual(["(marketing)", "pricing", "page"]);
|
|
expect(AppElementsWire.readMetadata(result).sourcePage).toBe("/(marketing)/pricing/page");
|
|
expect(JSON.stringify(result)).not.toContain("__sourcePage");
|
|
expect(JSON.stringify(result)).not.toContain("/(marketing)/pricing/page");
|
|
}
|
|
});
|
|
|
|
it("returns canonical record keys when no skip disposition is supplied", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: { "layout:/": "root-layout", "page:/": "page" },
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result["layout:/"]).toBe("root-layout");
|
|
expect(result["page:/"]).toBe("page");
|
|
}
|
|
});
|
|
|
|
it("omits only proven layout entries when static-layout skip transport is enabled", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: {
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
"layout:/": "root-layout",
|
|
"layout:/dashboard": "dashboard-layout",
|
|
"page:/dashboard": "dashboard-page",
|
|
},
|
|
layoutFlags: { "layout:/": "s", "layout:/dashboard": "s" },
|
|
skipDisposition: {
|
|
code: "SKIP_STATIC_LAYOUT_VERIFIED",
|
|
enabled: true,
|
|
mode: "skipStaticLayout",
|
|
skippedEntryIds: ["layout:/dashboard", "page:/dashboard"],
|
|
},
|
|
});
|
|
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result["layout:/"]).toBe("root-layout");
|
|
expect(Object.hasOwn(result, "layout:/dashboard")).toBe(false);
|
|
expect(result["page:/dashboard"]).toBe("dashboard-page");
|
|
expect(result[APP_LAYOUT_FLAGS_KEY]).toEqual({
|
|
"layout:/": "s",
|
|
"layout:/dashboard": "s",
|
|
});
|
|
expect(result[APP_ROUTE_KEY]).toBe("route:/dashboard");
|
|
expect(result[APP_ROOT_LAYOUT_KEY]).toBe("/");
|
|
expect(result[APP_SKIPPED_LAYOUT_IDS_KEY]).toEqual(["layout:/dashboard"]);
|
|
expect(AppElementsWire.readMetadata(result).skippedLayoutIds).toEqual(["layout:/dashboard"]);
|
|
}
|
|
});
|
|
|
|
it("preserves non-layout metadata keys", () => {
|
|
const result = buildOutgoingAppPayload({
|
|
element: {
|
|
[APP_ROUTE_KEY]: "route:/blog",
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: null,
|
|
"layout:/": "root-layout",
|
|
"page:/blog": "blog-page",
|
|
},
|
|
layoutFlags: { "layout:/": "s" },
|
|
});
|
|
expect(isAppElementsRecord(result)).toBe(true);
|
|
if (isAppElementsRecord(result)) {
|
|
expect(result[APP_ROUTE_KEY]).toBe("route:/blog");
|
|
expect(result[APP_ROOT_LAYOUT_KEY]).toBe("/");
|
|
expect(result[APP_INTERCEPTION_CONTEXT_KEY]).toBeNull();
|
|
expect(result["page:/blog"]).toBe("blog-page");
|
|
expect(result["layout:/"]).toBe("root-layout");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("artifact compatibility proof evaluation", () => {
|
|
it("returns compatible only when every current proof field is known and equal", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, current)).toEqual({
|
|
kind: "compatible",
|
|
});
|
|
});
|
|
|
|
it("falls back to renderFresh when graph compatibility is unknown", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, candidate)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "graphVersionUnknown",
|
|
});
|
|
});
|
|
|
|
it("falls back to renderFresh when deployment compatibility is unknown", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, candidate)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "deploymentVersionUnknown",
|
|
});
|
|
});
|
|
|
|
it("falls back to renderFresh when root boundary compatibility is unknown", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, candidate)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "rootBoundaryIdUnknown",
|
|
});
|
|
});
|
|
|
|
it("does not promote matching graph and deployment metadata to reuse proof when renderEpoch is unknown", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, candidate)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "renderEpochUnknown",
|
|
});
|
|
});
|
|
|
|
it("rejects a known deployment mismatch instead of using the unknown fallback", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-b",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(evaluateArtifactCompatibility(current, candidate)).toEqual({
|
|
kind: "incompatible",
|
|
fallback: "renderFresh",
|
|
reason: "deploymentVersionMismatch",
|
|
});
|
|
});
|
|
|
|
it("accepts rolling deploy payloads when compatibility is explicitly declared", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-next",
|
|
deploymentVersion: "deploy-canary",
|
|
rootBoundaryId: "root-next",
|
|
renderEpoch: "epoch-next",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-current",
|
|
deploymentVersion: "deploy-stable",
|
|
rootBoundaryId: "root-current",
|
|
renderEpoch: "epoch-current",
|
|
});
|
|
|
|
expect(
|
|
evaluateArtifactCompatibility(current, candidate, {
|
|
compatibilityMap: {
|
|
graphVersions: [["graph-current", "graph-next"]],
|
|
deploymentVersions: [["deploy-stable", "deploy-canary"]],
|
|
rootBoundaryIds: [["root-current", "root-next"]],
|
|
renderEpochs: [["epoch-current", "epoch-next"]],
|
|
},
|
|
}),
|
|
).toEqual({ kind: "compatible" });
|
|
});
|
|
|
|
it("supports canary and rollback only when they share a declared compatibility set", () => {
|
|
const rollback = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-rollback",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const canary = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-canary",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(
|
|
evaluateArtifactCompatibility(rollback, canary, {
|
|
compatibilityMap: {
|
|
deploymentVersions: [["deploy-stable", "deploy-canary", "deploy-rollback"]],
|
|
},
|
|
}),
|
|
).toEqual({ kind: "compatible" });
|
|
});
|
|
|
|
it("does not infer transitive deployment compatibility across overlapping pairs", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-c",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const candidate = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-a",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(
|
|
evaluateArtifactCompatibility(current, candidate, {
|
|
compatibilityMap: {
|
|
deploymentVersions: [
|
|
["deploy-a", "deploy-b"],
|
|
["deploy-b", "deploy-c"],
|
|
],
|
|
},
|
|
}),
|
|
).toEqual({
|
|
kind: "incompatible",
|
|
fallback: "renderFresh",
|
|
reason: "deploymentVersionNotDeclaredCompatible",
|
|
});
|
|
});
|
|
|
|
it("falls back to fresh render when a stale compatibility map lacks the rollback deploy", () => {
|
|
const rollback = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-rollback",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
const staleCanary = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-a",
|
|
deploymentVersion: "deploy-canary",
|
|
rootBoundaryId: "root-a",
|
|
renderEpoch: "epoch-a",
|
|
});
|
|
|
|
expect(
|
|
evaluateArtifactCompatibility(rollback, staleCanary, {
|
|
compatibilityMap: {
|
|
deploymentVersions: [["deploy-stable", "deploy-canary"]],
|
|
},
|
|
}),
|
|
).toEqual({
|
|
kind: "incompatible",
|
|
fallback: "renderFresh",
|
|
reason: "deploymentVersionNotDeclaredCompatible",
|
|
});
|
|
});
|
|
|
|
it("treats old-client/new-server future compatibility metadata as unknown proof", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-current",
|
|
deploymentVersion: "deploy-current",
|
|
rootBoundaryId: "root-current",
|
|
renderEpoch: "epoch-current",
|
|
});
|
|
const payload = {
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: null,
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
[APP_ARTIFACT_COMPATIBILITY_KEY]: {
|
|
schemaVersion: ARTIFACT_COMPATIBILITY_SCHEMA_VERSION + 1,
|
|
graphVersion: "graph-next",
|
|
deploymentVersion: "deploy-next",
|
|
appElementsSchemaVersion: APP_ELEMENTS_SCHEMA_VERSION,
|
|
rscPayloadSchemaVersion: RSC_PAYLOAD_SCHEMA_VERSION,
|
|
rootBoundaryId: "root-next",
|
|
renderEpoch: "epoch-next",
|
|
},
|
|
} satisfies Readonly<Record<string, unknown>>;
|
|
|
|
const metadata = AppElementsWire.readMetadata(payload);
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(createArtifactCompatibilityEnvelope());
|
|
expect(evaluateArtifactCompatibility(current, metadata.artifactCompatibility)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "graphVersionUnknown",
|
|
});
|
|
});
|
|
|
|
it("treats new-client/old-server legacy payload metadata as unknown proof", () => {
|
|
const current = createArtifactCompatibilityEnvelope({
|
|
graphVersion: "graph-current",
|
|
deploymentVersion: "deploy-current",
|
|
rootBoundaryId: "root-current",
|
|
renderEpoch: "epoch-current",
|
|
});
|
|
const payload = {
|
|
[APP_ROUTE_KEY]: "route:/dashboard",
|
|
[APP_INTERCEPTION_CONTEXT_KEY]: null,
|
|
[APP_ROOT_LAYOUT_KEY]: "/",
|
|
} satisfies Readonly<Record<string, unknown>>;
|
|
|
|
const metadata = AppElementsWire.readMetadata(payload);
|
|
|
|
expect(metadata.artifactCompatibility).toEqual(createArtifactCompatibilityEnvelope());
|
|
expect(evaluateArtifactCompatibility(current, metadata.artifactCompatibility)).toEqual({
|
|
kind: "unknown",
|
|
fallback: "renderFresh",
|
|
reason: "graphVersionUnknown",
|
|
});
|
|
});
|
|
});
|