diff --git a/packages/cli/src/capture/assetDownloader.ts b/packages/cli/src/capture/assetDownloader.ts index 9e783b934..21a881f3b 100644 --- a/packages/cli/src/capture/assetDownloader.ts +++ b/packages/cli/src/capture/assetDownloader.ts @@ -264,8 +264,10 @@ export async function downloadAndRewriteFonts( if (fontUrls.size === 0) return css; - // Limit font downloads to avoid bloat. Google Fonts serves 20+ unicode-range - // subsets per weight — we only need a few per family for video production. + // Limit font download attempts to bound worst-case egress and latency. Google Fonts serves + // 20+ unicode-range subsets per weight, so successes alone cannot be the bound: six transient + // failures can intentionally suppress later URLs in that family. Latin-priority sorting below + // makes the limited attempts useful while keeping this failure tradeoff explicit. const MAX_FONTS_PER_FAMILY = 6; const MAX_TOTAL_FONTS = 30; const familyCounts = new Map(); diff --git a/packages/cli/src/commands/capture.test.ts b/packages/cli/src/commands/capture.test.ts index 2a8ac84ed..c85ff09f9 100644 --- a/packages/cli/src/commands/capture.test.ts +++ b/packages/cli/src/commands/capture.test.ts @@ -57,7 +57,11 @@ describe("capture command — vision control", () => { ? Reflect.get(captureCommand.args, "capture-budget") : undefined; expect(captureBudgetArg).toMatchObject({ type: "string" }); - expect(captureBudgetArg?.description.toLowerCase()).toContain("post-navigation"); + const description = captureBudgetArg?.description.toLowerCase(); + expect(description).toContain("post-navigation"); + expect(description).toContain("cooperative"); + expect(description).toContain("not a hard wall-clock timeout"); + expect(description).toContain("already-started native/core work"); expect(captureBudgetArg?.description).toContain("--timeout"); }); @@ -81,28 +85,34 @@ describe("capture command — vision control", () => { ); }); - it("plumbs a positive --capture-budget into the post-navigation budget", async () => { - vi.spyOn(console, "log").mockImplementation(() => {}); - vi.spyOn(console, "error").mockImplementation(() => {}); + it.each([ + ["1", 1], + ["45000", 45_000], + ])( + "plumbs positive integer --capture-budget %s into the post-navigation budget", + async (captureBudget, expectedBudget) => { + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); - await captureCommand.run!({ - args: { - url: "https://example.com", - output: "/tmp/hf-capture-budget-test", - "skip-assets": false, - "skip-vision": false, - "capture-budget": "45000", - json: true, - }, - } as never); + await captureCommand.run!({ + args: { + url: "https://example.com", + output: "/tmp/hf-capture-budget-test", + "skip-assets": false, + "skip-vision": false, + "capture-budget": captureBudget, + json: true, + }, + } as never); - expect(captureWebsiteMock).toHaveBeenCalledWith( - expect.objectContaining({ postNavigationBudgetMs: 45_000 }), - undefined, - ); - }); + expect(captureWebsiteMock).toHaveBeenCalledWith( + expect.objectContaining({ postNavigationBudgetMs: expectedBudget }), + undefined, + ); + }, + ); - it.each(["0", "-1", "Infinity", "not-a-number"])( + it.each(["0", "-1", "0.5", "Infinity", "not-a-number"])( "rejects invalid --capture-budget %s before capture starts", async (captureBudget) => { vi.spyOn(console, "log").mockImplementation(() => {}); diff --git a/packages/cli/src/commands/capture.ts b/packages/cli/src/commands/capture.ts index 4456c650f..f9b4c11ad 100644 --- a/packages/cli/src/commands/capture.ts +++ b/packages/cli/src/commands/capture.ts @@ -15,8 +15,8 @@ function emitCapturePhase(event: CapturePhaseProgress): void { function parseCaptureBudget(raw: string | undefined): number | undefined { if (raw === undefined) return undefined; const parsed = Number(raw); - if (!Number.isFinite(parsed) || parsed <= 0) { - console.error("--capture-budget must be a positive finite number of milliseconds."); + if (!Number.isInteger(parsed) || parsed <= 0) { + console.error("--capture-budget must be a positive integer in milliseconds."); failCommand(); } return parsed; @@ -73,7 +73,7 @@ export default defineCommand({ "capture-budget": { type: "string", description: - "Post-navigation capture budget in ms (default: 120000); separate from page-load --timeout", + "Cooperative post-navigation budget in ms (default: 120000), separate from page-load --timeout; not a hard wall-clock timeout and cannot interrupt already-started native/core work", }, json: { type: "boolean",