Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/modules/server/module-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import {
clearReactVersionCache,
getDependencyPinningSnapshot,
} from "#veryfront/transforms/esm/package-registry.ts";
import {
DEPENDENCY_PINNING_PROJECTS_ENV,
DEPENDENCY_PINNING_ROLLOUT_PERCENT_ENV,
} from "#veryfront/transforms/esm/dependency-pinning-cohort.ts";
import { buildImportMapJson, clearImportMapCache } from "../../html/utils.ts";
import { hashString } from "#veryfront/cache/hash.ts";
import { register, tryResolve, unregister } from "#veryfront/extensions/contracts.ts";
Expand Down Expand Up @@ -95,6 +99,11 @@ describe({ name: "serveModule", sanitizeResources: false, sanitizeOps: false },
deleteEnv(RELEASE_ASSET_MANIFEST_ENV_FLAG);
deleteEnv(RELEASE_ASSET_DEPENDENCY_IMPORT_MAP_ENV_FLAG);
deleteEnv(DEPENDENCY_PINNING_ENV_FLAG);
// Paired with the flag above: the flag arms the rollout and this decides
// who is in it, so a test that sets one and leaks the other leaves a later
// test on a cohort it never chose.
deleteEnv(DEPENDENCY_PINNING_ROLLOUT_PERCENT_ENV);
deleteEnv(DEPENDENCY_PINNING_PROJECTS_ENV);
deleteEnv("VERYFRONT_ENABLE_SERVER_TIMING");
deleteEnv("VERYFRONT_CACHE_DIR");
clearReleaseAssetManifestCache();
Expand Down Expand Up @@ -2868,6 +2877,85 @@ describe({ name: "serveModule", sanitizeResources: false, sanitizeOps: false },
}
});

it("serves a client boundary for a project outside the pinning cohort", async () => {
// The flag arms the rollout; the cohort decides who is in it. An armed flag
// at 0% must stay inert. It did not: the early snapshot resolve was gated on
// the flag alone, so an out-of-cohort project skipped it, then carried no
// requested key -- because its document correctly emits none -- and every
// client module answered 409 instead of being served unpinned.
const projectDir = await Deno.makeTempDir({ prefix: "vf-module-cohort-out-" });
try {
setEnv(DEPENDENCY_PINNING_ENV_FLAG, "1");
setEnv(DEPENDENCY_PINNING_ROLLOUT_PERCENT_ENV, "0");
await Deno.writeTextFile(
`${projectDir}/package.json`,
JSON.stringify({ dependencies: { react: "19.2.4" } }),
);
await Deno.mkdir(`${projectDir}/components`, { recursive: true });
await Deno.writeTextFile(
`${projectDir}/components/Widget.tsx`,
['"use client";', "export default function Widget() { return null; }"].join("\n"),
);

const url = new URL("http://localhost:3000/_vf_modules/components/Widget.js");
const response = await serve(new Request(url), projectDir);
assertEquals(response.status, 200);
assertEquals(response.headers.get("content-type")?.includes("javascript"), true);
} finally {
clearReactVersionCache();
clearImportMapCache();
await Deno.remove(projectDir, { recursive: true });
}
});

it("conflicts an allowlisted project that carries no pin key", async () => {
// Covers the allowlist arm of the cohort, which the rollout-percent tests
// do not reach: named explicitly, the project is in the cohort, and a
// request with no pin key is a conflict.
//
// This does NOT prove which identity the cohort check reads. Where the UUID
// and the path id disagree, other guards answer 409 with the identical body
// for their own reasons, so the response cannot distinguish them. That the
// check must read effectiveProjectId -- the same identity handed to
// createDependencyPinningSource below it -- is established by reading the
// code, not by this test.
const { serveModule } = await import("./module-server.ts");
const projectDir = await Deno.makeTempDir({ prefix: "vf-module-cohort-id-" });
try {
setEnv(DEPENDENCY_PINNING_ENV_FLAG, "1");
setEnv(DEPENDENCY_PINNING_PROJECTS_ENV, "pinned-uuid");
await Deno.writeTextFile(
`${projectDir}/package.json`,
JSON.stringify({ dependencies: { react: "19.2.4" } }),
);
await Deno.mkdir(`${projectDir}/components`, { recursive: true });
await Deno.writeTextFile(
`${projectDir}/components/Widget.tsx`,
['"use client";', "export default function Widget() { return null; }"].join("\n"),
);

const response = await serveModule(
new Request("http://localhost:3000/_vf_modules/components/Widget.js"),
{
projectId: "test",
projectUUID: "pinned-uuid",
projectDir,
adapter: denoAdapter,
isLocalProject: true,
allowSSRModuleMode: true,
},
);

assertEquals(response.status, 409);
assertEquals(await response.text(), "Unknown dependency snapshot");
} finally {
deleteEnv(DEPENDENCY_PINNING_PROJECTS_ENV);
clearReactVersionCache();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
clearImportMapCache();
await Deno.remove(projectDir, { recursive: true });
}
});

it("keeps flag-off prefix graphs byte-compatible and query-free", async () => {
const projectDir = await Deno.makeTempDir({ prefix: "vf-module-prefix-off-" });
try {
Expand Down
13 changes: 12 additions & 1 deletion src/modules/server/module-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
resolveProjectReactVersion,
resolveRequestedDependencyPinningSnapshot,
} from "#veryfront/transforms/esm/package-registry.ts";
import { isProjectInDependencyPinningCohort } from "#veryfront/transforms/esm/dependency-pinning-cohort.ts";
import {
appendDependencyPinningPathKey,
extractDependencyPinningPathKey,
Expand Down Expand Up @@ -414,7 +415,17 @@ export function serveModule(req: Request, options: ModuleServerOptions): Promise
requestedPinCount > 1 ||
(hasRequestedPinKey &&
(!requestedPinKey || !isCanonicalDependencyPinningCacheKey(requestedPinKey))) ||
(!hasRequestedPinKey && dependencyPinningEnabled)
// The flag arms the rollout; the cohort decides who is in it. Gating on
// the flag alone conflicted every module of every project while the
// rollout sat at 0%, because an out-of-cohort document correctly emits
// no key -- so the armed flag was never inert, which is the property it
// is supposed to have.
// effectiveProjectId, not options.projectId: the UUID is the identity a
// hosted multi-project request is pinned under, and it is what the
// snapshot resolve below uses. Bucketing on the directory-path id would
// put this check in a different cohort than the snapshot it guards.
(!hasRequestedPinKey && dependencyPinningEnabled &&
isProjectInDependencyPinningCohort(effectiveProjectId))
) {
return unknownDependencySnapshotModuleResponse(method);
}
Expand Down