Skip to content
Merged
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
119 changes: 119 additions & 0 deletions src/rendering/orchestrator/css-candidate-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@ import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { createStyleScopeProfile } from "#veryfront/html/styles-builder/style-scope-profile.ts";
import {
__registerLogRecordEmitter,
__resetLogRecordEmitterForTests,
type LogEntry,
} from "#veryfront/utils/logger/logger.ts";
import {
getCandidateManifestCacheStats,
getProjectCandidates,
getRouteCandidates,
invalidateProjectCandidateManifests,
} from "./css-candidate-manifest.ts";

function captureLogs(): { entries: LogEntry[]; restore: () => void } {
const entries: LogEntry[] = [];
__registerLogRecordEmitter((entry) => entries.push(entry));
return {
entries,
restore: () => {
__resetLogRecordEmitterForTests();
},
};
}

describe("rendering/orchestrator/css-candidate-manifest", () => {
describe("invalidateProjectCandidateManifests", () => {
it("should clear all caches when no scope provided", () => {
Expand Down Expand Up @@ -229,6 +245,109 @@ describe("rendering/orchestrator/css-candidate-manifest", () => {
assertEquals(result.has("text-blue-500"), false);
});

it("degrades a file that exceeds the candidate-count admission cap instead of throwing", () => {
invalidateProjectCandidateManifests();
// >MAX_CSS_SELECTOR_TOKENS (100_000) distinct candidates in one file —
// the shape of a large minified vendor bundle in project sources.
const poisonContent = Array.from({ length: 100_001 }, (_, i) => `tok-${i}`).join(" ");
const options = {
projectScope: "project-poison-count",
projectVersion: "v1",
projectDir: "/project",
files: [
{ path: "/project/vendor/minified.js", content: poisonContent },
{
path: "/project/pages/index.tsx",
content: '<div className="text-red-500">Home</div>',
},
],
developmentMode: false,
};

const result = getProjectCandidates(options);

assertEquals(result.has("text-red-500"), true);
assertEquals(result.has("tok-0"), false);

// The completed manifest must be cached so the pathological file is not
// re-scanned (and cannot re-fail) on every request.
const statsAfterFirst = getCandidateManifestCacheStats();
assertEquals(statsAfterFirst.manifests.entries, 1);
const second = getProjectCandidates(options);
assertEquals(second.has("text-red-500"), true);
});

it("logs rejected source files without exposing absolute project paths", () => {
invalidateProjectCandidateManifests();
const captured = captureLogs();
try {
const projectDir = "/Users/someone/private/path/my-project";
const absoluteSourcePath = `${projectDir}/vendor/minified.js`;
const outsideSourcePath = "/Users/someone/other-parent/minified.js";
const poisonContent = Array.from({ length: 100_001 }, (_, i) => `tok-${i}`).join(" ");

getProjectCandidates({
projectScope: "project-poison-log-redaction",
projectVersion: "v1",
projectDir,
files: [
{ path: absoluteSourcePath, content: poisonContent },
{ path: outsideSourcePath, content: poisonContent },
],
developmentMode: false,
});

const projectWarning = captured.entries.find((entry) =>
entry.message === "Skipping file rejected by candidate extraction" &&
entry.context?.path === "vendor/minified.js"
);
assertEquals(projectWarning !== undefined, true, "the in-project warning must be emitted");
assertEquals(JSON.stringify(projectWarning!.context).includes(projectDir), false);
assertEquals(JSON.stringify(projectWarning!.context).includes(absoluteSourcePath), false);

const outsideWarning = captured.entries.find((entry) =>
entry.message === "Skipping file rejected by candidate extraction" &&
entry.context?.path === "[outside-project]/minified.js"
);
assertEquals(
outsideWarning !== undefined,
true,
"the outside-project warning must be emitted",
);
assertEquals(outsideWarning!.context?.path, "[outside-project]/minified.js");
const outsideContext = JSON.stringify(outsideWarning!.context);
assertEquals(outsideContext.includes(outsideSourcePath), false);
assertEquals(outsideContext.includes("/Users/someone"), false);
assertEquals(outsideContext.includes("other-parent"), false);
} finally {
captured.restore();
}
});
Comment thread
kojiwakayama marked this conversation as resolved.

it("degrades a file that exceeds the byte-size admission cap instead of throwing", () => {
invalidateProjectCandidateManifests();
// >MAX_CSS_FILE_BYTES (16MB) — e.g. a giant generated asset in sources.
const oversized = "text-blue-500 ".repeat(
Math.ceil((16 * 1024 * 1024 + 1) / "text-blue-500 ".length),
);
const result = getProjectCandidates({
projectScope: "project-poison-bytes",
projectVersion: "v1",
projectDir: "/project",
files: [
{ path: "/project/generated/blob.js", content: oversized },
{
path: "/project/pages/index.tsx",
content: '<div className="text-red-500">Home</div>',
},
],
developmentMode: false,
});

assertEquals(result.has("text-red-500"), true);
assertEquals(result.has("text-blue-500"), false);
});
Comment thread
kojiwakayama marked this conversation as resolved.

it("keeps configured runtime roots in the candidate graph", () => {
invalidateProjectCandidateManifests();
const result = getProjectCandidates({
Expand Down
31 changes: 30 additions & 1 deletion src/rendering/orchestrator/css-candidate-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ function toRelativeProjectPath(path: string, projectDir: string): string {
return normalized.replace(/^\/+/, "");
}

function toDiagnosticProjectPath(path: string, projectDir: string): string {
const normalized = normalizePath(path);
const normalizedProjectDir = normalizePath(projectDir).replace(/\/+$/, "");
if (normalized === normalizedProjectDir) return ".";
if (normalized.startsWith(`${normalizedProjectDir}/`)) {
return normalized.slice(normalizedProjectDir.length + 1);
}
if (/^(?:[A-Za-z]:)?\//.test(normalized)) {
return `[outside-project]/${normalized.split("/").pop() ?? "unknown"}`;
}
return normalized.replace(/^\/+/, "");
}

function buildManifestCacheKey(
projectScope: string,
projectVersion: string,
Expand Down Expand Up @@ -123,7 +136,23 @@ function buildCandidateManifest(files: SourceFileLike[], projectDir: string): Ca
if (!file.content) continue;
if (!SOURCE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) continue;

const candidates = new Set(extractCandidates(file.content));
// A file the tokenizer refuses to admit (over the byte or candidate-count
// cap) must degrade to "contributes no candidates", not abort the manifest:
// an escaping throw here propagates to the SSR boundary and, because it
// happens before manifestCache.set, is rebuilt and re-thrown on every
// request to the project (VERYFRONT-SERVER-F).
let extracted: string[];
try {
extracted = extractCandidates(file.content);
} catch (error) {
logger.warn("Skipping file rejected by candidate extraction", {
path: toDiagnosticProjectPath(file.path, projectDir),
error: error instanceof Error ? error.message : String(error),
});
continue;
}

const candidates = new Set(extracted);
const relativePath = toRelativeProjectPath(file.path, projectDir);
const absolutePath = normalizePath(file.path);

Expand Down