mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 07:40:06 +00:00
fix(cli): preserve capture failure diagnostics
This commit is contained in:
@@ -309,6 +309,35 @@ describe("captionImagesWithGemini — OpenRouter provider", () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("classifies non-provider pipeline failures without leaking the local path", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-caption-no-assets-"));
|
||||
dirs.push(dir);
|
||||
vi.stubEnv("OPENROUTER_API_KEY", "or-unused-key");
|
||||
|
||||
const warnings: string[] = [];
|
||||
let outcome: VisionCaptionOutcome | undefined;
|
||||
const captions = await captionImagesWithGemini(dir, () => {}, warnings, {
|
||||
onOutcome: (value) => {
|
||||
outcome = value;
|
||||
},
|
||||
});
|
||||
|
||||
expect(captions).toEqual({});
|
||||
expect(warnings).toEqual(["OpenRouter captioning failed internally; captions omitted."]);
|
||||
expect(warnings.join(" ")).not.toContain(dir);
|
||||
expect(outcome).toEqual({
|
||||
timedOutRequests: 0,
|
||||
failedRequests: 0,
|
||||
budgetExhausted: false,
|
||||
internalError: true,
|
||||
});
|
||||
if (!outcome) throw new Error("Expected vision caption outcome");
|
||||
expect(resolveVisionPhaseCompletion(outcome, 10_000)).toEqual({
|
||||
status: "degraded",
|
||||
reason: "internal-error",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips captioning entirely when no provider key is present", async () => {
|
||||
const dir = makeProjectWithImages();
|
||||
dirs.push(dir);
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface VisionCaptionOutcome {
|
||||
timedOutRequests: number;
|
||||
failedRequests: number;
|
||||
budgetExhausted: boolean;
|
||||
/** Failure outside provider request handling (filesystem, module, or programming error). */
|
||||
internalError?: boolean;
|
||||
}
|
||||
|
||||
interface VisionCaptionOptions {
|
||||
@@ -36,11 +38,14 @@ export function resolveVisionPhaseCompletion(
|
||||
| { status: "completed" }
|
||||
| {
|
||||
status: "degraded";
|
||||
reason: "budget-exhausted" | "request-timeout" | "provider-error";
|
||||
reason: "budget-exhausted" | "request-timeout" | "provider-error" | "internal-error";
|
||||
} {
|
||||
if (outcome.budgetExhausted || remainingMs <= 0) {
|
||||
return { status: "degraded", reason: "budget-exhausted" };
|
||||
}
|
||||
if (outcome.internalError) {
|
||||
return { status: "degraded", reason: "internal-error" };
|
||||
}
|
||||
if (outcome.timedOutRequests > 0) {
|
||||
return { status: "degraded", reason: "request-timeout" };
|
||||
}
|
||||
@@ -245,12 +250,15 @@ export async function captionImagesWithGemini(
|
||||
let timedOutCount = 0;
|
||||
let failedRequestCount = 0;
|
||||
let budgetExhausted = false;
|
||||
let internalError = false;
|
||||
const reportOutcome = (): void => {
|
||||
options.onOutcome?.({
|
||||
const outcome: VisionCaptionOutcome = {
|
||||
timedOutRequests: timedOutCount,
|
||||
failedRequests: failedRequestCount,
|
||||
budgetExhausted,
|
||||
});
|
||||
};
|
||||
if (internalError) outcome.internalError = true;
|
||||
options.onOutcome?.(outcome);
|
||||
};
|
||||
if (options.skipVision) {
|
||||
reportOutcome();
|
||||
@@ -546,8 +554,8 @@ export async function captionImagesWithGemini(
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
failedRequestCount = Math.max(1, failedRequestCount);
|
||||
warnings.push(`${providerName} captioning failed; captions omitted.`);
|
||||
internalError = true;
|
||||
warnings.push(`${providerName} captioning failed internally; captions omitted.`);
|
||||
}
|
||||
|
||||
reportOutcome();
|
||||
|
||||
@@ -267,7 +267,10 @@ export async function captureWebsite(
|
||||
httpStatus: navigationResponse?.status() ?? null,
|
||||
...pageContentCheck,
|
||||
});
|
||||
if (blockedReason) throw new Error(blockedReason);
|
||||
if (blockedReason) {
|
||||
phase("navigation", "degraded", "blocked");
|
||||
throw new Error(blockedReason);
|
||||
}
|
||||
|
||||
phase("navigation", "completed");
|
||||
phase("core-extraction", "started");
|
||||
|
||||
@@ -23,6 +23,16 @@ describe("detectBlockedPage", () => {
|
||||
hasChallengeElement: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "classic Cloudflare interstitial",
|
||||
evidence: {
|
||||
httpStatus: 503,
|
||||
title: "Attention Required! | Cloudflare",
|
||||
textLength: 120,
|
||||
bodyChildCount: 3,
|
||||
hasChallengeElement: false,
|
||||
},
|
||||
},
|
||||
])("rejects a minimal protection page: $label", ({ evidence }) => {
|
||||
expect(detectBlockedPage(evidence)).toMatch(/capture blocked/i);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export function detectBlockedPage(evidence: PageLoadEvidence): string | undefine
|
||||
const hasBlockedStatus =
|
||||
evidence.httpStatus === 401 || evidence.httpStatus === 403 || evidence.httpStatus === 429;
|
||||
const hasBlockedTitle =
|
||||
/^(?:(?:error\s*)?(?:401|403|429)(?:\s*(?:[-:—]\s*)?(?:forbidden|unauthorized|access denied|too many requests))?|forbidden|access denied|attention required|just a moment(?:\.{3})?)(?:\s*[|—-]\s*(?:cloudflare|sucuri website firewall))?$/i.test(
|
||||
/^(?:(?:error\s*)?(?:401|403|429)(?:\s*(?:[-:—]\s*)?(?:forbidden|unauthorized|access denied|too many requests))?|forbidden|access denied|attention required!?|just a moment(?:\.{3})?)(?:\s*[|—-]\s*(?:cloudflare|sucuri website firewall))?$/i.test(
|
||||
evidence.title.trim(),
|
||||
);
|
||||
|
||||
|
||||
@@ -26,7 +26,13 @@ export interface CapturePhaseProgress {
|
||||
status: "started" | "completed" | "degraded";
|
||||
/** Null before the post-navigation budget begins. */
|
||||
remainingMs: number | null;
|
||||
reason?: "budget-exhausted" | "disabled" | "request-timeout" | "provider-error";
|
||||
reason?:
|
||||
| "budget-exhausted"
|
||||
| "disabled"
|
||||
| "request-timeout"
|
||||
| "provider-error"
|
||||
| "internal-error"
|
||||
| "blocked";
|
||||
}
|
||||
|
||||
export interface CaptureOptions {
|
||||
|
||||
Reference in New Issue
Block a user