mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
5748be3ba5
* feat(cache): gate planner commits on explicit reuse proof Runtime cache entries could enter browser commit planning as ordinary payloads even when the entry had no planner-visible reuse proof. That violated the #726 invariant that a cache hit is not semantic authority by itself. The planner now receives a compact cache-entry proof decision through AppElements metadata and rejects missing or incompatible cache entries before route-topology commit approval. Cache-restored browser payloads synthesize the missing-proof rejection when metadata is absent, while fresh payloads keep the existing path. Targeted tests cover proof projection, metadata transport, planner decisions, and the cached-payload lifecycle path. * fix(cache): skip unproofed visited RSC cache restores Visited App Router RSC snapshots were still eligible for replay even though production payloads do not yet carry cache-entry reuse proof. That let the new planner gate reject the cache path and turn soft restores into hard navigations. Require decoded proof metadata before a payload is stored as a visited-response restore candidate, and update the real app-router navigation test to prove unproofed responses refetch without reloading. Also make cache-proof rejection-code parsing exhaustive at the type boundary. * fix(cache): require explicit App Router payload origins App Router navigation commits still carried cache-restore semantics as an optional boolean. A fresh default could hide a future restore path that forgot to request reuse proof, reopening the cache authority bug for visited responses. The violated invariant is that cache origin is part of the payload contract, not a caller memory check. Replace the boolean with a discriminated AppNavigationPayloadOrigin and thread it through browser entry, navigation controller, visible commit classification, and tests. Fresh payloads remain proof-optional; visited-cache payloads synthesize a missing-proof rejection and fail closed. * docs(cache): document static layout proof parsing boundary Runtime cache proof parsing currently accepts one proven reuse class: static layout. Future proof classes must update the parser before visited-cache responses can safely restore as commit-capable payloads. The review also noted that the cache-restorable metadata predicate behaves as a narrowing boundary. Make that contract explicit as a TypeScript type guard without changing runtime behavior. * fix(cache): keep accepted proof in rejection traces Cache-proofed payloads that failed a later interception or root-boundary gate produced correct hard-navigation decisions, but the rejection trace lost the accepted proof fields. That made cache-proof diagnostics incomplete for payloads that passed proof validation and failed a later topology gate. The root cause was that later rejection branches still used the pre-cache-proof trace fields after commitTraceFields had been computed. Thread the enriched trace fields into those rejection decisions, document cache proof parser three-way semantics, narrow the cache-restorable metadata type guard precisely, and read the pending commit proof from the canonical top-level field.
1915 lines
64 KiB
TypeScript
1915 lines
64 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
NAVIGATION_TRACE_SCHEMA_VERSION,
|
|
NavigationTraceReasonCodes,
|
|
} from "../packages/vinext/src/server/navigation-trace.js";
|
|
import {
|
|
navigationPlanner,
|
|
type FlightResultV0,
|
|
type MountedParallelSlotSnapshotV0,
|
|
type NavigationDecisionV0,
|
|
type NavigationEvent,
|
|
type NavigationPlannerInput,
|
|
type NavigationPlannerStateV0,
|
|
type OperationToken,
|
|
type ParallelSlotBindingSnapshotV0,
|
|
type RefreshScope,
|
|
type RouteSnapshotV0,
|
|
type InterceptionSnapshotV0,
|
|
type RootBoundaryTransition,
|
|
} from "../packages/vinext/src/server/navigation-planner.js";
|
|
import type {
|
|
GraphVersion,
|
|
RootBoundaryId,
|
|
RouteManifest,
|
|
RouteManifestInterception,
|
|
RouteManifestRoute,
|
|
RouteManifestRootBoundary,
|
|
RouteManifestSlotBinding,
|
|
StaticSegmentGraph,
|
|
} from "../packages/vinext/src/routing/app-route-graph.js";
|
|
import {
|
|
createCacheEntryReuseProof,
|
|
type CacheEntryReuseProof,
|
|
} from "../packages/vinext/src/server/cache-proof.js";
|
|
|
|
type TestManifestRoute = {
|
|
id?: string;
|
|
layoutIds: readonly string[];
|
|
pattern: string;
|
|
patternParts?: readonly string[];
|
|
rootBoundaryId: string | null;
|
|
slotBindings?: readonly ParallelSlotBindingSnapshotV0[];
|
|
interceptions?: readonly TestManifestInterception[];
|
|
};
|
|
|
|
type TestManifestInterception = {
|
|
sourcePattern: string;
|
|
targetPattern: string;
|
|
slotId: string;
|
|
ownerLayoutId: string | null;
|
|
};
|
|
|
|
function createRouteSnapshot(
|
|
rootBoundaryId: string | null,
|
|
layoutIds: readonly string[] = rootBoundaryId === null ? [] : [`layout:${rootBoundaryId}`],
|
|
mountedParallelSlots: readonly MountedParallelSlotSnapshotV0[] = [],
|
|
slotBindings: readonly ParallelSlotBindingSnapshotV0[] = [],
|
|
): RouteSnapshotV0 {
|
|
return {
|
|
displayUrl: "https://example.com/dashboard",
|
|
interception: null,
|
|
interceptionContext: null,
|
|
layoutIds,
|
|
matchedUrl: "/dashboard",
|
|
mountedParallelSlots,
|
|
rootBoundaryId,
|
|
routeId: "route:/dashboard",
|
|
slotBindings,
|
|
};
|
|
}
|
|
|
|
function createInterceptionSnapshot(
|
|
overrides: Partial<InterceptionSnapshotV0> = {},
|
|
): InterceptionSnapshotV0 {
|
|
return {
|
|
sourceMatchedUrl: "/feed",
|
|
sourceRouteId: "route:/feed",
|
|
slotId: "slot:modal:/feed",
|
|
targetMatchedUrl: "/photos/42",
|
|
targetRouteId: "route:/photos/42",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createAcceptedStaticLayoutCacheEntryReuseProof(): CacheEntryReuseProof {
|
|
return {
|
|
kind: "runtime-cache-entry",
|
|
decision: {
|
|
canReuse: true,
|
|
code: "CP_STATIC_LAYOUT_REUSE_PROVEN",
|
|
kind: "reuse",
|
|
reuseClass: "static-layout",
|
|
},
|
|
};
|
|
}
|
|
|
|
function createSlotBinding(
|
|
slotId: string,
|
|
ownerLayoutId: string,
|
|
state: ParallelSlotBindingSnapshotV0["state"] = "active",
|
|
): ParallelSlotBindingSnapshotV0 {
|
|
return { ownerLayoutId, slotId, state };
|
|
}
|
|
|
|
function createTestRouteManifest(routes: readonly TestManifestRoute[]): RouteManifest {
|
|
const manifestRoutes = new Map<string, RouteManifestRoute>();
|
|
const slotBindings = new Map<string, RouteManifestSlotBinding>();
|
|
const interceptions = new Map<string, RouteManifestInterception>();
|
|
const interceptionsBySlotId = new Map<string, RouteManifestInterception[]>();
|
|
const rootBoundaries = new Map<RootBoundaryId, RouteManifestRootBoundary>();
|
|
const routeIdByPattern = new Map(
|
|
routes.map((route) => [route.pattern, route.id ?? `route:${route.pattern}`]),
|
|
);
|
|
|
|
for (const route of routes) {
|
|
const routeId = routeIdByPattern.get(route.pattern) ?? `route:${route.pattern}`;
|
|
const rootBoundaryId =
|
|
route.rootBoundaryId === null ? null : (route.rootBoundaryId as RootBoundaryId);
|
|
const routeSlotBindings = route.slotBindings ?? [];
|
|
const slotIds = routeSlotBindings.map((binding) => binding.slotId).sort();
|
|
const patternParts =
|
|
route.patternParts ?? route.pattern.split("/").filter((segment) => segment.length > 0);
|
|
manifestRoutes.set(routeId, {
|
|
id: routeId,
|
|
isDynamic: patternParts.some((part) => part.startsWith(":")),
|
|
layoutIds: route.layoutIds,
|
|
pageId: null,
|
|
paramNames: patternParts
|
|
.filter((part) => part.startsWith(":"))
|
|
.map((part) => part.replace(/^:/, "").replace(/[+*]$/, "")),
|
|
pattern: route.pattern,
|
|
patternParts,
|
|
rootBoundaryId,
|
|
rootParamNames: [],
|
|
routeHandlerId: null,
|
|
slotIds,
|
|
templateIds: [],
|
|
});
|
|
|
|
for (const binding of routeSlotBindings) {
|
|
slotBindings.set(`${routeId}::${binding.slotId}`, {
|
|
defaultId: binding.state === "default" ? `default:${binding.slotId}` : null,
|
|
id: `${routeId}::${binding.slotId}`,
|
|
ownerLayoutId: binding.ownerLayoutId,
|
|
routeId,
|
|
routeSegments: null,
|
|
slotId: binding.slotId,
|
|
state: binding.state,
|
|
});
|
|
}
|
|
|
|
for (const interception of route.interceptions ?? []) {
|
|
const targetPatternParts = interception.targetPattern
|
|
.split("/")
|
|
.filter((segment) => segment.length > 0);
|
|
const id = `interception:${interception.slotId}:${interception.sourcePattern}->${interception.targetPattern}`;
|
|
const manifestInterception = {
|
|
id,
|
|
interceptingRouteId: routeIdByPattern.get(interception.sourcePattern) ?? null,
|
|
ownerLayoutId: interception.ownerLayoutId,
|
|
slotId: interception.slotId,
|
|
sourcePattern: interception.sourcePattern,
|
|
sourcePatternParts: interception.sourcePattern
|
|
.split("/")
|
|
.filter((segment) => segment.length > 0),
|
|
targetPattern: interception.targetPattern,
|
|
targetPatternParts,
|
|
targetRouteId: routeIdByPattern.get(interception.targetPattern) ?? null,
|
|
};
|
|
interceptions.set(id, manifestInterception);
|
|
const slotInterceptions = interceptionsBySlotId.get(interception.slotId);
|
|
if (slotInterceptions) {
|
|
slotInterceptions.push(manifestInterception);
|
|
} else {
|
|
interceptionsBySlotId.set(interception.slotId, [manifestInterception]);
|
|
}
|
|
}
|
|
|
|
const rootLayoutId = route.layoutIds[0];
|
|
if (rootBoundaryId !== null && rootLayoutId !== undefined) {
|
|
rootBoundaries.set(rootBoundaryId, {
|
|
id: rootBoundaryId,
|
|
layoutId: rootLayoutId,
|
|
treePath: route.rootBoundaryId?.replace(/^root-boundary:/, "") ?? "/",
|
|
});
|
|
}
|
|
}
|
|
|
|
const segmentGraph: StaticSegmentGraph = {
|
|
boundaries: new Map(),
|
|
defaults: new Map(),
|
|
interceptions,
|
|
interceptionsBySlotId,
|
|
layouts: new Map(),
|
|
pages: new Map(),
|
|
rootBoundaries,
|
|
routeHandlers: new Map(),
|
|
routes: manifestRoutes,
|
|
slotBindings,
|
|
slots: new Map(),
|
|
templates: new Map(),
|
|
};
|
|
|
|
return {
|
|
graphVersion: "graph:test" as GraphVersion,
|
|
segmentGraph,
|
|
};
|
|
}
|
|
|
|
function rootBoundaryIdForManifest(rootBoundaryId: string | null): string | null {
|
|
return rootBoundaryId === null ? null : `root-boundary:${rootBoundaryId}`;
|
|
}
|
|
|
|
function createRouteManifestForSnapshots(
|
|
currentSnapshot: RouteSnapshotV0,
|
|
targetSnapshot: RouteSnapshotV0,
|
|
): RouteManifest {
|
|
if (targetSnapshot.interception !== null) {
|
|
const proof = targetSnapshot.interception;
|
|
const routes: TestManifestRoute[] = [
|
|
{
|
|
id: currentSnapshot.routeId,
|
|
layoutIds: currentSnapshot.layoutIds,
|
|
pattern: currentSnapshot.matchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(currentSnapshot.rootBoundaryId),
|
|
},
|
|
];
|
|
if (
|
|
proof.sourceRouteId !== currentSnapshot.routeId ||
|
|
proof.sourceMatchedUrl !== currentSnapshot.matchedUrl
|
|
) {
|
|
routes.push({
|
|
id: proof.sourceRouteId,
|
|
layoutIds: targetSnapshot.layoutIds,
|
|
pattern: proof.sourceMatchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(targetSnapshot.rootBoundaryId),
|
|
});
|
|
}
|
|
routes.push(
|
|
{
|
|
id: proof.sourceRouteId,
|
|
interceptions: [
|
|
{
|
|
ownerLayoutId:
|
|
targetSnapshot.slotBindings.find((binding) => binding.slotId === proof.slotId)
|
|
?.ownerLayoutId ?? null,
|
|
slotId: proof.slotId,
|
|
sourcePattern: proof.sourceMatchedUrl,
|
|
targetPattern: proof.targetMatchedUrl,
|
|
},
|
|
],
|
|
layoutIds: currentSnapshot.layoutIds,
|
|
pattern: proof.sourceMatchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(currentSnapshot.rootBoundaryId),
|
|
slotBindings: targetSnapshot.slotBindings,
|
|
},
|
|
{
|
|
id: proof.targetRouteId,
|
|
layoutIds: targetSnapshot.layoutIds,
|
|
pattern: proof.targetMatchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(targetSnapshot.rootBoundaryId),
|
|
},
|
|
);
|
|
return createTestRouteManifest(routes);
|
|
}
|
|
|
|
return createTestRouteManifest([
|
|
{
|
|
id: currentSnapshot.routeId,
|
|
layoutIds: currentSnapshot.layoutIds,
|
|
pattern: currentSnapshot.matchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(currentSnapshot.rootBoundaryId),
|
|
slotBindings: currentSnapshot.slotBindings,
|
|
},
|
|
{
|
|
id: targetSnapshot.routeId,
|
|
layoutIds: targetSnapshot.layoutIds,
|
|
pattern: targetSnapshot.matchedUrl,
|
|
rootBoundaryId: rootBoundaryIdForManifest(targetSnapshot.rootBoundaryId),
|
|
slotBindings: targetSnapshot.slotBindings,
|
|
},
|
|
]);
|
|
}
|
|
|
|
function createOperationToken(overrides: Partial<OperationToken> = {}): OperationToken {
|
|
return {
|
|
baseVisibleCommitVersion: 2,
|
|
deploymentVersion: null,
|
|
graphVersion: null,
|
|
lane: "navigation",
|
|
operationId: 7,
|
|
targetSnapshotFingerprint: "route:/dashboard|root:/",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createRejectedCacheEntryReuseProof(
|
|
code: "CP_ARTIFACT_COMPATIBILITY_INCOMPATIBLE" | "CP_ROUTE_VARIANT_CEILING_EXCEEDED",
|
|
): CacheEntryReuseProof {
|
|
return {
|
|
kind: "runtime-cache-entry",
|
|
decision: {
|
|
canReuse: false,
|
|
code,
|
|
kind: "reject",
|
|
mode: code === "CP_ROUTE_VARIANT_CEILING_EXCEEDED" ? "privateUncacheable" : "renderFresh",
|
|
scope: code === "CP_ROUTE_VARIANT_CEILING_EXCEEDED" ? "route" : "affectedOutput",
|
|
},
|
|
};
|
|
}
|
|
|
|
function planFlightResponse(rootBoundaryId: string | null): NavigationDecisionV0 {
|
|
const token = createOperationToken({
|
|
targetSnapshotFingerprint: `route:/dashboard|root:${rootBoundaryId ?? "unknown"}`,
|
|
});
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/"],
|
|
pattern: "/current",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
{
|
|
layoutIds: rootBoundaryId === null ? [] : [`layout:${rootBoundaryId}`],
|
|
pattern: "/dashboard",
|
|
rootBoundaryId: rootBoundaryId === null ? null : `root-boundary:${rootBoundaryId}`,
|
|
},
|
|
]);
|
|
const result: FlightResultV0 = {
|
|
href: "https://example.com/dashboard",
|
|
targetSnapshot: {
|
|
...createRouteSnapshot(rootBoundaryId),
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
},
|
|
};
|
|
const state: NavigationPlannerStateV0 = {
|
|
nextOperationToken: token,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: rootBoundaryId,
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/current",
|
|
matchedUrl: "/current",
|
|
routeId: "route:/current",
|
|
},
|
|
};
|
|
const event: NavigationEvent = {
|
|
kind: "flightResponseArrived",
|
|
result,
|
|
token,
|
|
};
|
|
const input: NavigationPlannerInput = {
|
|
event,
|
|
routeManifest,
|
|
state,
|
|
};
|
|
|
|
return navigationPlanner.plan(input);
|
|
}
|
|
|
|
function planFlightResponseFromRootBoundaries(options: {
|
|
currentRootBoundaryId: string | null;
|
|
nextRootBoundaryId: string | null;
|
|
}): NavigationDecisionV0 {
|
|
const token = createOperationToken({
|
|
targetSnapshotFingerprint: `route:/dashboard|root:${options.nextRootBoundaryId ?? "unknown"}`,
|
|
});
|
|
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds:
|
|
options.currentRootBoundaryId === null ? [] : [`layout:${options.currentRootBoundaryId}`],
|
|
pattern: "/current",
|
|
rootBoundaryId:
|
|
options.currentRootBoundaryId === null
|
|
? null
|
|
: `root-boundary:${options.currentRootBoundaryId}`,
|
|
},
|
|
{
|
|
layoutIds:
|
|
options.nextRootBoundaryId === null ? [] : [`layout:${options.nextRootBoundaryId}`],
|
|
pattern: "/dashboard",
|
|
rootBoundaryId:
|
|
options.nextRootBoundaryId === null ? null : `root-boundary:${options.nextRootBoundaryId}`,
|
|
},
|
|
]);
|
|
|
|
return navigationPlanner.plan({
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
href: "https://example.com/dashboard",
|
|
targetSnapshot: {
|
|
...createRouteSnapshot(options.nextRootBoundaryId),
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
},
|
|
},
|
|
token,
|
|
},
|
|
routeManifest,
|
|
state: {
|
|
nextOperationToken: token,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: options.currentRootBoundaryId,
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: options.nextRootBoundaryId,
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: {
|
|
...createRouteSnapshot(options.currentRootBoundaryId),
|
|
displayUrl: "https://example.com/current",
|
|
matchedUrl: "/current",
|
|
routeId: "route:/current",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
function planFlightResponseFromSnapshots(options: {
|
|
cacheEntryReuseProof?: CacheEntryReuseProof;
|
|
currentSnapshot: RouteSnapshotV0;
|
|
lane?: OperationToken["lane"];
|
|
routeManifest?: RouteManifest | null;
|
|
targetSnapshot: RouteSnapshotV0;
|
|
traceFields?: NavigationPlannerStateV0["traceFields"];
|
|
}): NavigationDecisionV0 {
|
|
const token = createOperationToken({
|
|
lane: options.lane ?? "navigation",
|
|
targetSnapshotFingerprint: `${options.targetSnapshot.routeId}|root:${
|
|
options.targetSnapshot.rootBoundaryId ?? "unknown"
|
|
}`,
|
|
});
|
|
|
|
return navigationPlanner.plan({
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
...(options.cacheEntryReuseProof
|
|
? { cacheEntryReuseProof: options.cacheEntryReuseProof }
|
|
: {}),
|
|
href: options.targetSnapshot.displayUrl,
|
|
targetSnapshot: options.targetSnapshot,
|
|
},
|
|
token,
|
|
},
|
|
routeManifest:
|
|
options.routeManifest === undefined
|
|
? createRouteManifestForSnapshots(options.currentSnapshot, options.targetSnapshot)
|
|
: options.routeManifest,
|
|
state: {
|
|
nextOperationToken: token,
|
|
traceFields: options.traceFields,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: options.currentSnapshot,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("navigationPlanner root-boundary decisions", () => {
|
|
// Root-layout MPA semantics match Next.js coverage:
|
|
// .nextjs-ref/test/e2e/app-dir/root-layout/root-layout.test.ts
|
|
// .nextjs-ref/test/e2e/app-dir/segment-cache/mpa-navigations/mpa-navigations.test.ts
|
|
it("proposes a visible commit for same-root flight responses", () => {
|
|
const decision = planFlightResponse("/");
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.targetSnapshot.rootBoundaryId).toBe("/");
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
expect(decision.trace).toEqual({
|
|
schemaVersion: NAVIGATION_TRACE_SCHEMA_VERSION,
|
|
entries: [
|
|
{
|
|
code: NavigationTraceReasonCodes.commitCurrent,
|
|
fields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("rejects runtime cache entries when the cache proof is missing", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/profile",
|
|
matchedUrl: "/dashboard/profile",
|
|
routeId: "route:/dashboard/profile",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
cacheEntryReuseProof: createCacheEntryReuseProof(null),
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected cache proof rejection to hard navigate");
|
|
}
|
|
expect(decision.reason).toBe("cacheProofRejected");
|
|
expect(decision.url).toBe("https://example.com/dashboard/settings");
|
|
expect(decision.trace.entries).toEqual([
|
|
{
|
|
code: NavigationTraceReasonCodes.cacheProofRejected,
|
|
fields: {
|
|
cacheProofCode: "CP_CACHE_ENTRY_PROOF_MISSING",
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("rejects incompatible runtime cache entries before route-topology commit approval", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/profile",
|
|
matchedUrl: "/dashboard/profile",
|
|
routeId: "route:/dashboard/profile",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
cacheEntryReuseProof: createRejectedCacheEntryReuseProof(
|
|
"CP_ARTIFACT_COMPATIBILITY_INCOMPATIBLE",
|
|
),
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected incompatible cache entry to hard navigate");
|
|
}
|
|
expect(decision.reason).toBe("cacheProofRejected");
|
|
expect(decision.trace.entries[0]).toEqual({
|
|
code: NavigationTraceReasonCodes.cacheProofRejected,
|
|
fields: {
|
|
cacheProofCode: "CP_ARTIFACT_COMPATIBILITY_INCOMPATIBLE",
|
|
cacheProofMode: "renderFresh",
|
|
cacheProofScope: "affectedOutput",
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps accepted runtime cache proof visible on the commit proposal", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/profile",
|
|
matchedUrl: "/dashboard/profile",
|
|
routeId: "route:/dashboard/profile",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
const cacheEntryReuseProof = createAcceptedStaticLayoutCacheEntryReuseProof();
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
cacheEntryReuseProof,
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proven cache entry to remain committable");
|
|
}
|
|
expect(decision.proposal.cacheEntryReuseDecision).toEqual(cacheEntryReuseProof.decision);
|
|
expect(decision.trace.entries[0]).toEqual({
|
|
code: NavigationTraceReasonCodes.commitCurrent,
|
|
fields: {
|
|
cacheProofCode: "CP_STATIC_LAYOUT_REUSE_PROVEN",
|
|
cacheProofReuseClass: "static-layout",
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("hard-navigates cross-root flight responses", () => {
|
|
const transition: RootBoundaryTransition = navigationPlanner.classifyRootBoundaryTransition(
|
|
"/",
|
|
"/(dashboard)",
|
|
);
|
|
const decision = planFlightResponse("/(dashboard)");
|
|
|
|
expect(transition).toBe("rootBoundaryChanged");
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("rootBoundaryChanged");
|
|
expect(decision.url).toBe("https://example.com/dashboard");
|
|
expect(decision.trace.entries).toEqual([
|
|
{
|
|
code: NavigationTraceReasonCodes.rootBoundaryChanged,
|
|
fields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/(dashboard)",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps accepted runtime cache proof fields on later root-boundary rejections", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/"),
|
|
displayUrl: "https://example.com/current",
|
|
matchedUrl: "/current",
|
|
routeId: "route:/current",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/(dashboard)"),
|
|
displayUrl: "https://example.com/dashboard",
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
cacheEntryReuseProof: createAcceptedStaticLayoutCacheEntryReuseProof(),
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("rootBoundaryChanged");
|
|
expect(decision.trace.entries[0]).toEqual({
|
|
code: NavigationTraceReasonCodes.rootBoundaryChanged,
|
|
fields: {
|
|
cacheProofCode: "CP_STATIC_LAYOUT_REUSE_PROVEN",
|
|
cacheProofReuseClass: "static-layout",
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/(dashboard)",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("uses an unproven payload fallback when the target root identity is unknown", () => {
|
|
const decision = planFlightResponse(null);
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("unprovenTopologyFallback");
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.preserveElementIds).toEqual([]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.rootBoundaryUnknown);
|
|
});
|
|
|
|
it("uses an unproven payload fallback when the visible root identity is unknown", () => {
|
|
const transition = navigationPlanner.classifyRootBoundaryTransition(null, "/");
|
|
const decision = planFlightResponseFromRootBoundaries({
|
|
currentRootBoundaryId: null,
|
|
nextRootBoundaryId: "/",
|
|
});
|
|
|
|
expect(transition).toBe("rootBoundaryUnknown");
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("unprovenTopologyFallback");
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.preserveElementIds).toEqual([]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.rootBoundaryUnknown);
|
|
});
|
|
|
|
it("preserves only the common same-root layout ancestor prefix", () => {
|
|
const currentSnapshot = createRouteSnapshot("/", [
|
|
"layout:/",
|
|
"layout:/dashboard",
|
|
"layout:/dashboard/settings",
|
|
]);
|
|
const targetSnapshot = createRouteSnapshot("/", [
|
|
"layout:/",
|
|
"layout:/dashboard",
|
|
"layout:/dashboard/profile",
|
|
]);
|
|
|
|
expect(
|
|
navigationPlanner.resolveSameLayoutAncestorPersistence(currentSnapshot, targetSnapshot),
|
|
).toEqual(["layout:/", "layout:/dashboard"]);
|
|
});
|
|
|
|
it("preserves mounted parallel slots owned by approved same-layout ancestors", () => {
|
|
// Mirrors Next.js mounted parallel route preservation covered by:
|
|
// .nextjs-ref/test/e2e/app-dir/parallel-routes-and-interception-catchall/parallel-routes-and-interception-catchall.test.ts
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[
|
|
{ ownerLayoutId: "layout:/feed", slotId: "slot:modal:/feed" },
|
|
{ ownerLayoutId: "layout:/other", slotId: "slot:stale:/other" },
|
|
],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot("/", [
|
|
"layout:/",
|
|
"layout:/feed",
|
|
"layout:/feed/comments",
|
|
]);
|
|
|
|
expect(
|
|
navigationPlanner.resolveMountedParallelSlotPersistence(currentSnapshot, targetSnapshot),
|
|
).toEqual(["slot:modal:/feed"]);
|
|
expect(
|
|
navigationPlanner.resolveCurrentRootBoundaryElementPersistence(
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
),
|
|
).toEqual(["layout:/", "layout:/feed", "slot:modal:/feed"]);
|
|
});
|
|
|
|
it("does not preserve mounted parallel slots when their owner layout is not retained", () => {
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[{ ownerLayoutId: "layout:/feed", slotId: "slot:modal:/feed" }],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot("/", ["layout:/", "layout:/settings"]);
|
|
|
|
expect(
|
|
navigationPlanner.resolveMountedParallelSlotPersistence(currentSnapshot, targetSnapshot),
|
|
).toEqual([]);
|
|
expect(
|
|
navigationPlanner.resolveCurrentRootBoundaryElementPersistence(
|
|
currentSnapshot,
|
|
targetSnapshot,
|
|
),
|
|
).toEqual(["layout:/"]);
|
|
});
|
|
|
|
it("does not approve mounted parallel slot preservation for traverse commits", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[{ ownerLayoutId: "layout:/feed", slotId: "slot:modal:/feed" }],
|
|
),
|
|
displayUrl: "https://example.com/feed",
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed", "layout:/feed/comments"]),
|
|
displayUrl: "https://example.com/feed/comments",
|
|
matchedUrl: "/feed/comments",
|
|
routeId: "route:/feed/comments",
|
|
};
|
|
const token = createOperationToken({
|
|
lane: "traverse",
|
|
targetSnapshotFingerprint: "route:/feed/comments|root:/",
|
|
});
|
|
|
|
const decision = navigationPlanner.plan({
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
href: "https://example.com/feed/comments",
|
|
targetSnapshot,
|
|
},
|
|
token,
|
|
},
|
|
routeManifest: createRouteManifestForSnapshots(currentSnapshot, targetSnapshot),
|
|
state: {
|
|
nextOperationToken: token,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: currentSnapshot,
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
});
|
|
|
|
it("preserves previous slot ids for default and unmatched target bindings", () => {
|
|
// Mirrors Next.js default-slot reuse semantics:
|
|
// .nextjs-ref/packages/next/src/client/components/router-reducer/ppr-navigations.ts
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "active"),
|
|
createSlotBinding("slot:analytics:/dashboard", "layout:/dashboard", "default"),
|
|
createSlotBinding("slot:reports:/dashboard", "layout:/dashboard", "active"),
|
|
],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "default"),
|
|
createSlotBinding("slot:analytics:/dashboard", "layout:/dashboard", "unmatched"),
|
|
createSlotBinding("slot:reports:/dashboard", "layout:/dashboard", "active"),
|
|
],
|
|
);
|
|
const targetSettingsSnapshot: RouteSnapshotV0 = {
|
|
...targetSnapshot,
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
const token = createOperationToken({
|
|
targetSnapshotFingerprint: "route:/dashboard/settings|root:/",
|
|
});
|
|
|
|
const decision = navigationPlanner.plan({
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
href: "https://example.com/dashboard/settings",
|
|
targetSnapshot: targetSettingsSnapshot,
|
|
},
|
|
token,
|
|
},
|
|
routeManifest: createRouteManifestForSnapshots(currentSnapshot, targetSettingsSnapshot),
|
|
state: {
|
|
nextOperationToken: token,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: currentSnapshot,
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/dashboard"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([
|
|
"slot:analytics:/dashboard",
|
|
"slot:team:/dashboard",
|
|
]);
|
|
});
|
|
|
|
it("does not preserve default or unmatched target slots without visible route-state proof", () => {
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard"],
|
|
[],
|
|
[createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "active")],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "default"),
|
|
createSlotBinding("slot:analytics:/dashboard", "layout:/dashboard", "unmatched"),
|
|
],
|
|
);
|
|
const targetSettingsSnapshot: RouteSnapshotV0 = {
|
|
...targetSnapshot,
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
const token = createOperationToken({
|
|
targetSnapshotFingerprint: "route:/dashboard/settings|root:/",
|
|
});
|
|
|
|
const decision = navigationPlanner.plan({
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
href: "https://example.com/dashboard/settings",
|
|
targetSnapshot: targetSettingsSnapshot,
|
|
},
|
|
token,
|
|
},
|
|
routeManifest: createRouteManifestForSnapshots(currentSnapshot, targetSettingsSnapshot),
|
|
state: {
|
|
nextOperationToken: token,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: currentSnapshot,
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual(["slot:team:/dashboard"]);
|
|
});
|
|
|
|
it("does not preserve default target slots when their owner layout is not retained", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/dashboard", "layout:/dashboard", "active")],
|
|
),
|
|
displayUrl: "https://example.com/feed",
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/dashboard", "layout:/dashboard", "default")],
|
|
);
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
});
|
|
|
|
it("does not preserve target slots when the current binding is unmatched", () => {
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard"],
|
|
[],
|
|
[createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "unmatched")],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
[],
|
|
[createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "default")],
|
|
);
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
});
|
|
|
|
it("does not preserve active target slot bindings", () => {
|
|
const currentSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard"],
|
|
[],
|
|
[createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "active")],
|
|
);
|
|
const targetSnapshot = createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
[],
|
|
[createSlotBinding("slot:team:/dashboard", "layout:/dashboard", "active")],
|
|
);
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
});
|
|
|
|
it("uses RouteManifest root boundaries instead of stale snapshot roots", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/app"],
|
|
pattern: "/app",
|
|
rootBoundaryId: "root-boundary:/app",
|
|
},
|
|
{
|
|
layoutIds: ["layout:/marketing"],
|
|
pattern: "/marketing",
|
|
rootBoundaryId: "root-boundary:/marketing",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/stale-app"]),
|
|
matchedUrl: "/app",
|
|
routeId: "route:/app",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/stale-app", "layout:/stale-marketing"]),
|
|
displayUrl: "https://example.com/marketing",
|
|
matchedUrl: "/marketing",
|
|
routeId: "route:/marketing",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("rootBoundaryChanged");
|
|
expect(decision.trace.entries[0]?.fields.currentRootLayoutTreePath).toBe("/app");
|
|
expect(decision.trace.entries[0]?.fields.nextRootLayoutTreePath).toBe("/marketing");
|
|
});
|
|
|
|
it("uses RouteManifest topology to commit when snapshot root identities are missing", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard"],
|
|
pattern: "/dashboard",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
pattern: "/dashboard/settings",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("currentRootBoundary");
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/dashboard"]);
|
|
});
|
|
|
|
it("does not let stale manifest route IDs override the matched URL", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/app"],
|
|
pattern: "/app",
|
|
rootBoundaryId: "root-boundary:/app",
|
|
},
|
|
{
|
|
layoutIds: ["layout:/marketing"],
|
|
pattern: "/marketing",
|
|
rootBoundaryId: "root-boundary:/marketing",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
displayUrl: "https://example.com/app",
|
|
matchedUrl: "/app",
|
|
routeId: "route:/app",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
displayUrl: "https://example.com/app",
|
|
matchedUrl: "/app",
|
|
routeId: "route:/marketing",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("currentRootBoundary");
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/app"]);
|
|
});
|
|
|
|
it("matches concrete dynamic URLs to manifest route patterns", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/blog"],
|
|
pattern: "/blog/:slug",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
displayUrl: "https://example.com/blog/hello",
|
|
matchedUrl: "/blog/hello",
|
|
routeId: "route:/blog/hello",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
displayUrl: "https://example.com/blog/world",
|
|
matchedUrl: "/blog/world",
|
|
routeId: "route:/blog/world",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("currentRootBoundary");
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/blog"]);
|
|
});
|
|
|
|
it("uses manifest target slot bindings while keeping visible snapshot as content proof", () => {
|
|
const modalSlot = "slot:modal:/dashboard";
|
|
const staleSlot = "slot:stale:/dashboard";
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard"],
|
|
pattern: "/dashboard",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
pattern: "/dashboard/settings",
|
|
rootBoundaryId: "root-boundary:/",
|
|
slotBindings: [createSlotBinding(modalSlot, "layout:/dashboard", "default")],
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[
|
|
createSlotBinding(modalSlot, "layout:/dashboard", "active"),
|
|
createSlotBinding(staleSlot, "layout:/dashboard", "active"),
|
|
],
|
|
),
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[createSlotBinding(staleSlot, "layout:/dashboard", "default")],
|
|
),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([modalSlot]);
|
|
});
|
|
|
|
it("does not use manifest current slots as visible content proof", () => {
|
|
const modalSlot = "slot:modal:/dashboard";
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard"],
|
|
pattern: "/dashboard",
|
|
rootBoundaryId: "root-boundary:/",
|
|
slotBindings: [createSlotBinding(modalSlot, "layout:/dashboard", "active")],
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
|
|
pattern: "/dashboard/settings",
|
|
rootBoundaryId: "root-boundary:/",
|
|
slotBindings: [createSlotBinding(modalSlot, "layout:/dashboard", "default")],
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, [], [], []),
|
|
matchedUrl: "/dashboard",
|
|
routeId: "route:/dashboard",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, [], [], []),
|
|
displayUrl: "https://example.com/dashboard/settings",
|
|
matchedUrl: "/dashboard/settings",
|
|
routeId: "route:/dashboard/settings",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
});
|
|
|
|
it("uses the manifest source route topology for intercepted payloads", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/feed"],
|
|
pattern: "/feed",
|
|
rootBoundaryId: "root-boundary:/",
|
|
interceptions: [
|
|
{
|
|
ownerLayoutId: "layout:/feed",
|
|
slotId: "slot:modal:/feed",
|
|
sourcePattern: "/feed",
|
|
targetPattern: "/photos/:id",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
layoutIds: ["layout:/photos", "layout:/photos/photo"],
|
|
pattern: "/photos/:id",
|
|
rootBoundaryId: "root-boundary:/photos",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("interceptedCurrentRootBoundary");
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
|
|
});
|
|
|
|
it("approves manifest-declared dynamic interception topology with concrete wire route ids", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/[locale]", "layout:/[locale]/feed"],
|
|
pattern: "/:locale/feed",
|
|
rootBoundaryId: "root-boundary:/",
|
|
interceptions: [
|
|
{
|
|
ownerLayoutId: "layout:/[locale]/feed",
|
|
slotId: "slot:modal:/[locale]/feed",
|
|
sourcePattern: "/:locale/feed",
|
|
targetPattern: "/:locale/photos/:id",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/[locale]", "layout:/[locale]/photos"],
|
|
pattern: "/:locale/photos/:id",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
matchedUrl: "/en/feed",
|
|
routeId: "route:/en/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[createSlotBinding("slot:modal:/[locale]/feed", "layout:/[locale]/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/en/photos/42",
|
|
interception: createInterceptionSnapshot({
|
|
sourceMatchedUrl: "/en/feed",
|
|
sourceRouteId: "route:/en/feed",
|
|
slotId: "slot:modal:/[locale]/feed",
|
|
targetMatchedUrl: "/en/photos/42",
|
|
targetRouteId: "route:/en/photos/42",
|
|
}),
|
|
interceptionContext: "/en/feed",
|
|
matchedUrl: "/en/photos/42",
|
|
routeId: "route:/en/photos/42\u0000/en/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("interceptedCurrentRootBoundary");
|
|
expect(decision.proposal.preserveElementIds).toEqual([
|
|
"layout:/",
|
|
"layout:/[locale]",
|
|
"layout:/[locale]/feed",
|
|
]);
|
|
});
|
|
|
|
it("rejects intercepted preservation when RouteManifest does not declare the topology", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/feed"],
|
|
pattern: "/feed",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/photos"],
|
|
pattern: "/photos/:id",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
cacheEntryReuseProof: createAcceptedStaticLayoutCacheEntryReuseProof(),
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedUndeclaredTopology,
|
|
);
|
|
expect(decision.trace.entries[0]?.fields).toMatchObject({
|
|
cacheProofCode: "CP_STATIC_LAYOUT_REUSE_PROVEN",
|
|
cacheProofReuseClass: "static-layout",
|
|
});
|
|
});
|
|
|
|
it("rejects intercepted preservation when RouteManifest declares a different slot owner", () => {
|
|
const routeManifest = createTestRouteManifest([
|
|
{
|
|
layoutIds: ["layout:/", "layout:/feed", "layout:/other"],
|
|
pattern: "/feed",
|
|
rootBoundaryId: "root-boundary:/",
|
|
interceptions: [
|
|
{
|
|
ownerLayoutId: "layout:/feed",
|
|
slotId: "slot:modal:/feed",
|
|
sourcePattern: "/feed",
|
|
targetPattern: "/photos/:id",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
layoutIds: ["layout:/", "layout:/photos"],
|
|
pattern: "/photos/:id",
|
|
rootBoundaryId: "root-boundary:/",
|
|
},
|
|
]);
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(null, []),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
null,
|
|
[],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/other", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
routeManifest,
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedUndeclaredTopology,
|
|
);
|
|
});
|
|
|
|
it("approves intercepted preservation only from explicit source and slot proof", () => {
|
|
// Core-15 oracle, porting the visible behavior from Next.js:
|
|
// test/e2e/app-dir/parallel-routes-and-interception-catchall/parallel-routes-and-interception-catchall.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-and-interception-catchall/parallel-routes-and-interception-catchall.test.ts
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:modal:/feed", "layout:/feed", "default"),
|
|
createSlotBinding("slot:activity:/feed", "layout:/feed", "active"),
|
|
],
|
|
),
|
|
displayUrl: "https://example.com/feed",
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:activity:/feed", "layout:/feed", "default"),
|
|
createSlotBinding("slot:modal:/feed", "layout:/feed", "active"),
|
|
],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("interceptedCurrentRootBoundary");
|
|
expect(decision.proposal.preserveAbsentSlots).toBe(false);
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual(["slot:activity:/feed"]);
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedCommitCurrent,
|
|
);
|
|
});
|
|
|
|
it("does not treat legacy context-only payloads as intercepted preservation proof", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed"]),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed"]),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("currentRootBoundary");
|
|
expect(decision.proposal.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual([]);
|
|
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.commitCurrent);
|
|
});
|
|
|
|
it("rejects intercepted preservation when the visible source route is stale", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/gallery"]),
|
|
matchedUrl: "/gallery",
|
|
routeId: "route:/gallery",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedUnknownSource,
|
|
);
|
|
});
|
|
|
|
it("rejects intercepted preservation when proof target does not match the rendered route", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed"]),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot({ targetMatchedUrl: "/photos/99" }),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedTargetMismatch,
|
|
);
|
|
});
|
|
|
|
it("does not use intercepted snapshot root topology as preservation authority", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed"]),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/marketing",
|
|
["layout:/marketing"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/marketing", "active")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedMissingSlotProof,
|
|
);
|
|
});
|
|
|
|
it("rejects intercepted preservation when the target slot is not proven active", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot("/", ["layout:/", "layout:/feed"]),
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[createSlotBinding("slot:modal:/feed", "layout:/feed", "default")],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({ currentSnapshot, targetSnapshot });
|
|
|
|
expect(decision.kind).toBe("hardNavigate");
|
|
if (decision.kind !== "hardNavigate") {
|
|
throw new Error("Expected hardNavigate decision");
|
|
}
|
|
expect(decision.reason).toBe("interceptionProofRejected");
|
|
expect(decision.trace.entries[0]?.code).toBe(
|
|
NavigationTraceReasonCodes.interceptedRejectedMissingSlotProof,
|
|
);
|
|
});
|
|
|
|
it("allows traverse to restore an intercepted visible world only with proof", () => {
|
|
const currentSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[createSlotBinding("slot:activity:/feed", "layout:/feed", "active")],
|
|
),
|
|
displayUrl: "https://example.com/feed",
|
|
matchedUrl: "/feed",
|
|
routeId: "route:/feed",
|
|
};
|
|
const targetSnapshot: RouteSnapshotV0 = {
|
|
...createRouteSnapshot(
|
|
"/",
|
|
["layout:/", "layout:/feed"],
|
|
[],
|
|
[
|
|
createSlotBinding("slot:activity:/feed", "layout:/feed", "default"),
|
|
createSlotBinding("slot:modal:/feed", "layout:/feed", "active"),
|
|
],
|
|
),
|
|
displayUrl: "https://example.com/photos/42",
|
|
interception: createInterceptionSnapshot(),
|
|
interceptionContext: "/feed",
|
|
matchedUrl: "/photos/42",
|
|
routeId: "route:/photos/42\u0000/feed",
|
|
};
|
|
|
|
const decision = planFlightResponseFromSnapshots({
|
|
currentSnapshot,
|
|
lane: "traverse",
|
|
targetSnapshot,
|
|
});
|
|
|
|
expect(decision.kind).toBe("proposeCommit");
|
|
if (decision.kind !== "proposeCommit") {
|
|
throw new Error("Expected proposeCommit decision");
|
|
}
|
|
expect(decision.proposal.reason).toBe("interceptedCurrentRootBoundary");
|
|
expect(decision.proposal.preservePreviousSlotIds).toEqual(["slot:activity:/feed"]);
|
|
});
|
|
|
|
it("does not preserve layouts across root-boundary uncertainty", () => {
|
|
const currentSnapshot = createRouteSnapshot("/", ["layout:/"]);
|
|
const targetSnapshot = createRouteSnapshot(null, ["layout:/"]);
|
|
|
|
expect(
|
|
navigationPlanner.resolveSameLayoutAncestorPersistence(currentSnapshot, targetSnapshot),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("never hard-navigates prefetch flight responses", () => {
|
|
const token = createOperationToken({
|
|
lane: "prefetch",
|
|
targetSnapshotFingerprint: "route:/dashboard|root:/(dashboard)",
|
|
});
|
|
const decision = navigationPlanner.plan({
|
|
routeManifest: null,
|
|
state: {
|
|
nextOperationToken: token,
|
|
traceFields: {
|
|
currentRootLayoutTreePath: "/",
|
|
currentVisibleCommitVersion: 2,
|
|
nextRootLayoutTreePath: "/(dashboard)",
|
|
startedVisibleCommitVersion: 2,
|
|
},
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: createRouteSnapshot("/"),
|
|
},
|
|
event: {
|
|
kind: "flightResponseArrived",
|
|
result: {
|
|
href: "https://example.com/dashboard",
|
|
targetSnapshot: createRouteSnapshot("/(dashboard)"),
|
|
},
|
|
token,
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("noCommit");
|
|
if (decision.kind !== "noCommit") {
|
|
throw new Error("Expected noCommit decision");
|
|
}
|
|
expect(decision.reason).toBe("prefetchOnly");
|
|
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.prefetchOnly);
|
|
});
|
|
|
|
it("returns requestWork for initial navigation intent events", () => {
|
|
const token = createOperationToken();
|
|
const input: NavigationPlannerInput = {
|
|
routeManifest: null,
|
|
state: {
|
|
nextOperationToken: token,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: createRouteSnapshot("/"),
|
|
},
|
|
event: {
|
|
href: "https://example.com/dashboard",
|
|
kind: "navigate",
|
|
mode: "push",
|
|
},
|
|
};
|
|
const decision = navigationPlanner.plan(input);
|
|
|
|
expect(decision.kind).toBe("requestWork");
|
|
if (decision.kind !== "requestWork") {
|
|
throw new Error("Expected requestWork decision");
|
|
}
|
|
expect(decision.token).toBe(token);
|
|
expect(decision.work).toEqual({
|
|
href: "https://example.com/dashboard",
|
|
kind: "flight",
|
|
mode: "push",
|
|
});
|
|
});
|
|
|
|
it("returns requestWork for refresh intent events", () => {
|
|
const token = createOperationToken({
|
|
targetSnapshotFingerprint: "route:/dashboard|root:/|refresh",
|
|
});
|
|
const scope: RefreshScope = "visible";
|
|
const event: NavigationEvent = { kind: "refresh", scope };
|
|
const decision = navigationPlanner.plan({
|
|
event,
|
|
routeManifest: null,
|
|
state: {
|
|
nextOperationToken: token,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: createRouteSnapshot("/"),
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("requestWork");
|
|
if (decision.kind !== "requestWork") {
|
|
throw new Error("Expected requestWork decision");
|
|
}
|
|
expect(decision.work).toEqual({
|
|
href: "https://example.com/dashboard",
|
|
kind: "flight",
|
|
mode: "refresh",
|
|
});
|
|
});
|
|
|
|
it("does not invent a target href for traverse intent events", () => {
|
|
const token = createOperationToken();
|
|
const historyState = { key: "previous-entry" };
|
|
const decision = navigationPlanner.plan({
|
|
routeManifest: null,
|
|
state: {
|
|
nextOperationToken: token,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: createRouteSnapshot("/"),
|
|
},
|
|
event: {
|
|
direction: "back",
|
|
historyState,
|
|
kind: "traverse",
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("requestWork");
|
|
if (decision.kind !== "requestWork") {
|
|
throw new Error("Expected requestWork decision");
|
|
}
|
|
expect(decision.work).toEqual({
|
|
direction: "back",
|
|
historyState,
|
|
kind: "traverseFlight",
|
|
});
|
|
expect(decision.trace.entries[0]?.fields.targetHref).toBeNull();
|
|
expect(decision.trace.entries[0]?.fields.traverseDirection).toBe("back");
|
|
});
|
|
|
|
it("keeps unknown traversal direction explicit instead of guessing", () => {
|
|
const token = createOperationToken();
|
|
const historyState = { key: "external-entry" };
|
|
const decision = navigationPlanner.plan({
|
|
routeManifest: null,
|
|
state: {
|
|
nextOperationToken: token,
|
|
visibleCommitVersion: 2,
|
|
visibleSnapshot: createRouteSnapshot("/"),
|
|
},
|
|
event: {
|
|
direction: "unknown",
|
|
historyState,
|
|
kind: "traverse",
|
|
},
|
|
});
|
|
|
|
expect(decision.kind).toBe("requestWork");
|
|
if (decision.kind !== "requestWork") {
|
|
throw new Error("Expected requestWork decision");
|
|
}
|
|
expect(decision.work).toEqual({
|
|
direction: "unknown",
|
|
historyState,
|
|
kind: "traverseFlight",
|
|
});
|
|
expect(decision.trace.entries[0]?.fields.traverseDirection).toBe("unknown");
|
|
});
|
|
});
|